Play Framework 2.5 迁移指南:从 2.4 升级的完整实战手册与源码级解析
2026/9/23 23:30:42 网站建设 项目流程
  • 后端
  • Web框架

【免费下载链接】playframework

The Community Maintained High Velocity Web Framework For Java and Scala.

项目地址:https://gitcode.com/gh_mirrors/pl/playframework
点击查看免费下载

Play Framework 2.5 是一次以「去全局状态、拥抱 Java 8、统一流式处理」为核心的大版本升级,涉及构建配置、Scala 版本、路由生成器、依赖注入、CSRF 安全策略、WS 客户端与 Netty 底层等多个层面。本文以官方 Migration25 指南为主体,结合当前仓库中的实际源码与参考配置,系统讲解从 Play 2.4 迁移到 2.5 的全部步骤、关键破坏性变更及其背后的实现原理,帮助你在升级后获得可运行、可验证、贴近最佳实践的 Play 2.5 应用。

提示:如果你需要从更早的版本升级,请先阅读 Play 2.4 Migration Guide。此外,两个专题迁移指南提供了更细粒度的信息:Streams Migration Guide(迁移到 Akka Streams)与 Java Migration Guide(Java 应用迁移到原生 Java 8 类型)。

第一步:升级构建配置

升级 Play 2.4 → 2.5 的第一步是修改 sbt 构建,使项目能在 sbt 中正常加载与运行。

Play 版本升级

project/plugins.sbt中把 Play 的 sbt 插件版本升级到 2.5.x:

addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.x")

其中2.5.xx表示你想要使用的次版本号,例如2.5.0

sbt 升级到 0.13.11

Play 2.5 仍然兼容 sbt 0.13.8,但官方推荐升级到 0.13.11(该版本包含大量改进与 bug 修复)。将project/build.properties更新为:

sbt.version=0.13.11

Play Slick 升级

如果你的项目使用 Play Slick,需要升级到 2.0.0:

libraryDependencies += "com.typesafe.play" %% "play-slick" % "2.0.0"

如果同时使用 evolutions 支持:

libraryDependencies ++= Seq( "com.typesafe.play" %% "play-slick" % "2.0.0", "com.typesafe.play" %% "play-slick-evolutions" % "2.0.0" )

Play Ebean 升级

如果使用 Play Ebean,升级其 sbt 插件:

addSbtPlugin("com.typesafe.sbt" % "sbt-play-ebean" % "3.0.0")

ScalaTest + Play 升级

如果使用 ScalaTest + Play,升级测试依赖:

libraryDependencies ++= Seq( "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % "test" )

Scala 2.10 支持终止:全面迁移到 Scala 2.11

Play 2.3 与 2.4 同时支持 Scala 2.10 和 2.11,而 Play 2.5 终止了对 Scala 2.10 的支持,仅支持 Scala 2.11。原因有二:

  1. Play 2.5 内部大量使用scala-java8-compat库(该库仅支持 Scala 2.11),它提供了 Scala 与 Java 8 类型之间的转换,例如 ScalaFuture与 JavaCompletionStage的互转。这个库对应用代码同样很有用。
  2. 下一代 Play 计划支持 Scala 2.12,先统一到 2.11 能让后续过渡更平滑。

迁移步骤

Scala 与 Java 用户都必须在 sbt 中配置 Scala 2.11——即使项目没有任何 Scala 代码,Play 本身使用 Scala,必须为其配置正确的 Scala 库。

在 sbt 中设置scalaVersion即可:

scalaVersion := "2.11.8"

单项目构建可直接把该设置放在build.sbt中。多项目构建则必须为每个项目设置,通常放在所有项目共享的公共设置里:

def common = Seq( scalaVersion := "2.11.8" ) lazy val projectA = (project in file("projectA")) .enablePlugins(PlayJava) .settings(common: _*) lazy val projectB = (project in file("projectB")) .enablePlugins(PlayJava) .settings(common: _*)

Logback 配置变更:ColoredLevel 包路径迁移

为了移除 Play 对 Logback 的硬编码依赖(详见 Highlights25 的 Support for other logging frameworks 一节),Logback 配置所用的一个类被移动到了新的包。

迁移步骤

更新logback*.xml中所有对旧类play.api.Logger$ColoredLevel的引用,改为新的play.api.libs.logback.ColoredLevel

<conversionRule conversionWord="coloredLevel" converterClass="play.api.libs.logback.ColoredLevel" />

当前仓库中该类的实现位于 ColoredLevel.scala,它继承自 logback 的ClassicConverter,把日志级别渲染成带颜色的小写文本(TRACE 蓝色、DEBUG 青色、INFO 白色、WARN 黄色、ERROR 红色),对应配置中的%coloredLevel转换词。

如果你使用编译期依赖注入(compile time DI),需要把 application loader 中的Logger.configure(...)替换为:

LoggerConfigurator(context.environment.classLoader).foreach { _.configure(context.environment) }

LoggerConfigurator是 Play 面向日志框架的抽象接口(定义于 LoggerConfigurator.scala),提供多个configure重载,分别接收EnvironmentConfiguration与属性映射。这一设计正是 2.5 支持任意 SLF4J 兼容日志框架的基础:默认仍使用 Logback,但可以通过disablePlugins(PlayLogback)移除,再引入自定义框架的 SLF4J adapter,详见 SettingsLogger 的 Using a Custom Logging Framework 一节。

Play WS 升级到 AsyncHttpClient 2

Play WS 底层升级为 AsyncHttpClient 2(基于 Netty 4.0)。大部分变化在底层,但 AHC 2.0 的一些重大重构带来了 WS API 的破坏性变更:

  • AsyncHttpClientConfigDefaultAsyncHttpClientConfig取代。
  • allowPoolingConnectionallowSslConnectionPool在 AsyncHttpClient 中被合并为单一的keepAlive变量。因此play.ws.ning.allowPoolingConnectionplay.ws.ning.allowSslConnectionPool不再有效,配置它们会抛出异常。
  • webSocketIdleTimeout已移除,AhcWSClientConfig中不再提供。
  • ioThreadMultiplier已移除,AhcWSClientConfig中不再提供。
  • FluentCaseInsensitiveStringsMap类被删除,由 Netty 的HttpHeader类取代。
  • Realm.AuthScheme.None已移除,WSAuthScheme中不再提供。

此外还有一些小变更:

  • 为反映正确的 AsyncHttpClient 库名,包play.api.libs.ws.ning更名为play.api.libs.ws.ahcNing*类更名为Ahc*;AHC 配置前缀也从play.ws.ning改为play.ws.ahc,例如play.ws.ning.maxConnectionsPerHost现在是play.ws.ahc.maxConnectionsPerHost
  • 已废弃的接口play.libs.ws.WSRequestHolder被移除。
  • play.libs.ws.play.WSRequest接口现在返回java.util.concurrent.CompletionStage而非F.Promise(这是 Java 8 化改造的一部分,参见 JavaMigration25)。
  • 依赖Play.currentPlay.application的静态方法被标记为废弃。
  • 2.5 之前 Play WS 会从 Content-Type 推断字符集,并在请求头未设置字符集时自动追加到Content-Type头;该行为曾引发混淆与 bug,因此在 2.5.x 中Content-Type头不再自动携带推断出的字符集。如果显式设置Content-Type头,则按原样生效。

从当前仓库的源码结构看,AHC 相关的客户端实现位于 play-ahc-ws 模块,AhcWSClient的构造函数接收AhcWSClientConfig(见 AhcWSClient.scala),AhcWSModule则通过AhcWSClientConfigParserplay.ws.ahc前缀的配置解析客户端参数(见 AhcWSModule.scala)。

GlobalSettings 被废弃

作为 Play 持续去除全局状态工作的一部分,GlobalSettings与应用Global对象被标记为废弃。如何迁移离开GlobalSettings的详细说明,参见 Play 2.4 迁移指南中的 GlobalSettings 章节。Play 2.4 起推荐的替代方案是将GlobalSettings的职责拆分为HttpErrorHandlerHttpRequestHandlerHttpFilters三个可注入组件(对照表见 Migration24 的 Dependency Injected Components 一节)。

Plugins API 被移除

Plugins API 在 Play 2.4 中被标记为废弃,并在 Play 2.5 中正式移除。它已被 Play 的依赖注入与模块系统取代,后者提供了更干净、更灵活的方式构建可复用组件。从插件迁移到依赖注入的细节参见 Play 2.4 迁移指南的 PluginsToModules 章节。

路由默认使用 InjectedRoutesGenerator

Play 2.5 起,路由默认由依赖注入感知的InjectedRoutesGenerator生成,而非假设控制器是单例对象的StaticRoutesGenerator

从源码看,InjectedRoutesGenerator定义于 RoutesGenerator.scala,其id"injected",用于增量编译时判断路由生成器是否发生变化。它的核心逻辑是按控制器分组生成依赖描述符(Dependency):对每个路由,若使用@语法(instantiate为 true),则依赖jakarta.inject.Provider[控制器类],否则直接依赖控制器类(见 RoutesGenerator.scala 第 126-140 行)。也就是说,@前缀让路由器每次按需通过 Provider 获取控制器实例。在 RoutesCompiler.scala 中,InjectedRoutesGenerator也是默认传入的生成器。

如果代码中仍然使用object MyController这类静态控制器,想恢复旧行为,可以在build.sbt中添加:

routesGenerator := StaticRoutesGenerator

如果使用Build.scala而不是build.sbt,需要导入routesGenerator设置键:

import play.sbt.routes.RoutesCompiler.autoImport._

使用静态控制器配合静态路由生成器并不算废弃,但官方推荐迁移到使用依赖注入的类。

静态控制器替换为依赖注入

controllers.ExternalAssets现在是一个类,不再提供静态等价物。controllers.Assetscontrollers.Default也是类,虽然静态等价物仍然存在,但推荐使用类版本。当前仓库中ExternalAssets的实现位于 ExternalAssets.scala,通过@Inject构造注入,专门用于从外部目录提供静态资源(该控制器不适合在生产模式使用,源码注释明确说明它会在生产模式自动禁用)。

迁移步骤

推荐方案是让所有控制器都使用类。由于InjectedRoutesGenerator现在是默认路由生成器,routes文件中的控制器会被当作类而非对象。

如果仍有静态控制器,可以使用StaticRoutesGenerator(见上文),并在routes文件中给路由加@符号,例如:

GET /assets/*file @controllers.ExternalAssets.at(path = "/public", file)

play.Play 与 play.api.Play 方法被废弃

play.Play中以下方法已被废弃:

  • public static Application application()
  • public static Mode mode()
  • public static boolean isDev()
  • public static boolean isProd()
  • public static boolean isTest()

同样,play.api.Play中接受隐式Application并委托给 Application 的方法(例如def classloader(implicit app: Application))也已被废弃。

迁移步骤

这些方法实际委托给play.Applicationplay.Environment——使用它们的代码应改为通过依赖注入获取相应组件。Play 内置组件的注入方式对照表参见 Play 2.4 迁移指南的 Dependency Injected Components 一节。

例如,下面的 Scala 控制器把 environment 与 configuration 注入进来:

class HomeController @Inject() (environment: play.api.Environment, configuration: play.api.Configuration) extends Controller { def index = Action { Ok(views.html.index("Your new application is ready.")) } def config = Action { Ok(configuration.underlying.getString("some.config")) } def count = Action { val num = environment.resource("application.conf").toSeq.size Ok(num.toString) } }

play.api.Environment的实现位于 Environment.scala,它封装了应用部署的 rootPath、classLoader 与 mode 三个关注点,并提供getFileresource等基于 rootPath/classloader 的资源访问方法——这正是上面示例中environment.resource("application.conf")的底层能力。

处理遗留组件

通常你的组件不需要依赖整个应用,但有时不得不处理要求传入 Application 的遗留组件。可以通过把应用注入到某个组件中来解决:

class FooController @Inject() (appProvider: Provider[Application]) extends Controller { implicit lazy val app = appProvider.get() def bar = Action { Ok(Foo.bar(app)) } }

注意此时通常应使用Provider[Application]以避免循环依赖。

更好的做法是自建一个*Api类,把静态方法包装成实例方法:

class FooApi @Inject() (appProvider: Provider[Application]) { implicit lazy val app = appProvider.get() def bar = Foo.bar(app) def baz = Foo.baz(app) }

这样既能享受依赖注入带来的可测试性,又能继续使用依赖全局状态的库。

Content-Type 字符集变更

在 Play 2.5 之前,Play 会为某些未定义 charset 参数的 Content-Type(特别是application/jsonapplication/x-www-form-urlencoded)自动追加charset参数。现在Content-Type默认不带 charset 发送,无论是对 WS 发送请求,还是从 Play action 返回响应。如果存在不符合规范、要求必须携带 charset 参数的客户端或服务端,可以显式设置Content-Type头。

Guice injector 与 Guice builder 变更

默认情况下 Guice 可以通过代理环中的接口来解析循环依赖。由于循环依赖通常是代码坏味道,且可以通过注入 Provider 打破循环,Play 选择在默认 Guice injector 上禁用该特性。其他 DI 框架大多不具备此特性,保留它也会在编写 Play 模块时引发问题。

现在GuiceInjectorBuilderGuiceApplicationBuilder提供了四个新方法来自定义 Guice 的注入行为:

  • disableCircularProxies:禁用上面提到的通过代理接口解析循环依赖的行为;如需允许代理,使用disableCircularProxies(false)
  • requireExplicitBindings:指示 injector 只注入在模块中显式绑定的类,在测试中可用于校验绑定。
  • requireAtInjectOnConstructors:要求构造器带有@Inject注解才能实例化类。
  • requireExactBindingAnnotations:禁用 Guice 中容易出错的行为——在注入@Named("foo") Foo时用@Named Foo的绑定来替代。

这些方法的实现可以在 GuiceInjectorBuilder.scala 中找到:它们通过BinderOption枚举(DisableCircularProxiesRequireAtInjectOnConstructorsRequireExactBindingAnnotationsRequireExplicitBindings,见第 400-403 行)累积到 builder 中,最终应用到 GuiceBinder。其中disableCircularProxies默认启用(disable: Boolean = true),另外三个选项默认关闭,与迁移指南的描述一致。

CSRF 变更:默认策略大幅收紧

为了让 Play 的 CSRF 过滤器更能抵御浏览器插件漏洞与新扩展,CSRF 过滤器的默认配置变得极为保守。主要变化包括:

  • 不再黑名单化POST请求,而是只白名单GETHEADOPTIONS,其余所有请求都需要 CSRF 检查——这意味着DELETEPUT请求现在也会被检查。
  • 不再黑名单化application/x-www-form-urlencodedmultipart/form-datatext/plain,而是所有 Content-Type(包括无 Content-Type)的请求都需要 CSRF 检查。一个直接后果是:使用application/json的 AJAX 请求现在必须在Csrf-Token头中携带合法 CSRF token。
  • 基于无状态头的绕过机制(如X-Requested-With)默认被禁用。

同时新增了一个配置项,可针对携带特定头的请求绕过新的 CSRF 保护。该配置项默认对 Cookie 与 Authorization 头启用,这样不使用 session 认证的 REST 客户端无需发送 CSRF token 也能正常工作。

但需要注意:由于该配置项会放行所有没有这些头的请求,使用其他认证方案(NTLM、TLS 客户端证书)的应用将面临 CSRF 风险。这类应用应禁用该配置项,使其无 cookie 的已认证请求也受到 CSRF 过滤器保护。

最后还新增了一个选项:对 CORS 过滤器信任的来源跳过 CSRF 检查。注意 CORS 过滤器必须位于 CSRF 过滤器之前才能生效。当前仓库的 CSRF 过滤器实现中,该逻辑体现在 CSRFActions.scala 第 493 行:当csrfConfig.bypassCorsTrustedOrigins为 true 且请求属性中带有 CORS 过滤器写入的Origin时,跳过 CSRF 检查。相关配置类CSRFConfig定义于 csrf.scala,其默认headerName"Csrf-Token"bypassCorsTrustedOrigins默认为true

play-filters-helpers 模块的 reference.conf 展示了 2.5 收紧后的默认配置:

play.filters.csrf { # 由 CORS 过滤器信任的来源可绕过 CSRF 检查 bypassCorsTrustedOrigins = true header { # 接受 CSRF token 的请求头名称 name = "Csrf-Token" # 必须存在才会执行 CSRF 检查的头;默认为 Cookie 与 Authorization, # 设为 null 或空对象则保护所有请求 protectHeaders { Cookie = "*" Authorization = "*" } # 存在即绕过的请求头(默认为空) bypassHeaders {} } method { # 非空时,不在此列表中的方法都会被检查 whiteList = ["GET", "HEAD", "OPTIONS"] # 仅当 whiteList 为空时才使用 blackList = [] } contentType { whiteList = [] blackList = [] } }

如需恢复 Play 旧版的默认行为,可在application.conf中添加如下配置:

play.filters.csrf { header { bypassHeaders { X-Requested-With = "*" Csrf-Token = "nocheck" } protectHeaders = null } bypassCorsTrustedOrigins = false method { whiteList = [] blackList = ["POST"] } contentType.blackList = ["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"] }

获取 CSRF token

此前可以在任意 action 中从 HTTP 请求获取 CSRF token。现在必须有 CSRF 过滤器或 CSRF action,CSRF.getToken才能工作。如果未使用过滤器,可以在 Scala 中使用CSRFAddTokenaction、在 Java 中使用AddCSRFToken注解,确保 session 中有 token。

另外本版本修复了一个小 bug:此前若 token 签名无效,CSRF token 会变为空(导致模板 helper 抛异常);现在它会在同一请求内重新生成,因此模板 helper 与CSRF.getToken仍能取到 token。

Java 与 Scala 的 CSRF 完整文档分别见 JavaCsrf 与 ScalaCsrf。

Crypto 被废弃:拆分为专用签名器

自 Play 1.x 起,Play 就带有一个提供加密操作的Crypto对象(Play 内部使用,文档未正式提及,仅在 scaladoc 中被称为「cryptographic utilities」)。由于多种原因,以便捷工具形式提供加密能力被证明不可行。在 2.5.x 中,Play 专属功能被拆分为CookieSignerCSRFTokenSignerAESSigner三个 trait,Crypto单例对象被废弃。

迁移方法

加密迁移取决于你的使用场景,尤其是是否存在不安全的加密原语构造方式。简而言之:尽量使用 Kalium;否则使用 Tink 或直接使用 JCA。详细方案参见 Crypto Migration Guide。

从当前仓库源码看,拆分后的签名器实现在 CookieSigner.scala 与 CSRFTokenSigner.scala:

  • CookieSigner负责对 cookie 内容计算消息认证码(MAC),DefaultCookieSigner通过SecretConfiguration读取应用密钥(play.crypto.secret),其sign(message: String): String返回十六进制编码的签名;trait 的文档明确警告它「不应作为通用 MAC 工具使用」。
  • CSRFTokenSigner提供signTokenextractSignedTokenverifySignedTokengenerateSignedToken等方法,用于对 CSRF token 进行签名、提取与校验。

CryptoMigration25 中还强调了几个关键安全结论:不要用Crypto.sign或任何 HMAC 做密码哈希(密码哈希应当慢而昂贵,如 scrypt、bcrypt、PBKDF2);Crypto.encryptAES默认使用 AES-CTR 模式,只加密不认证,存在可延展性(malleability)问题;Play 内部对 session cookie 采用「先加密后 MAC」构造,因此不受影响。更多细节可阅读 CryptoMigration25。

Netty 4 升级:Channel 选项配置键变更

Netty 从 3.10 升级到 4.0,一个直接后果是 Netty channel 选项的配置方式发生了变化。完整选项列表以 Netty 4.0 的ChannelOptionAPI 为准。

迁移步骤

将所有play.server.netty.option键修改为ChannelOption中定义的新键。常用映射如下:

旧键新键
play.server.netty.option.backlogplay.server.netty.option.SO_BACKLOG
play.server.netty.option.child.keepAliveplay.server.netty.option.child.SO_KEEPALIVE
play.server.netty.option.child.tcpNoDelayplay.server.netty.option.child.TCP_NODELAY

当前仓库中 play-netty-server 模块的 reference.conf 展示了这一命名体系的当前形态:监听服务器 socket 的选项定义在option顶层,接收客户端连接对应的 socket 选项以child.*前缀区分,例如SO_BACKLOGchild.SO_KEEPALIVEchild.TCP_NODELAY,并支持通过完整限定类名加#指定原生传输专属选项(如io.netty.channel.ChannelOption#TCP_FASTOPEN)。

sendFile / sendPath / sendResource 的默认行为变更

此前 Java(play.mvc.StatusHeader)与 Scala(play.api.mvc.Results.Status)的文件发送 API 在 inline 与 attachment 两种模式下表现不一致:

API方法默认值
Scalaplay.api.mvc.Results.Status.sendResourceinline
Scalaplay.api.mvc.Results.Status.sendPathattachment
Scalaplay.api.mvc.Results.Status.sendFileattachment
Javaplay.mvc.StatusHeader.sendInputStreamnone
Javaplay.mvc.StatusHeader.sendResourceinline
Javaplay.mvc.StatusHeader.sendPathattachment
Javaplay.mvc.StatusHeader.sendFileinline

也就是说,旧版本在发送文件时混用了 inline 与 attachment 模式。现在发送文件、路径与资源时默认统一使用inline行为。当然,你仍可通过这些方法的参数在两种模式间切换。

当前仓库中 Scala 端的实现位于 Results.scala:sendFilesendPathsendResource三个方法的inline: Boolean = true参数默认值均已统一为true(见第 622、642、666 行)。实现细节上,sendPath通过FileIO.fromPath流式读取文件,sendResource通过 classloader 的getResourceAsStream打开资源后以流式发送,二者都会结合Content-Disposition(由Results.contentDispositionHeader(inline, name)生成)与文件名的 MIME 类型推断来构建响应;onClose回调可用于发送后清理临时文件等场景。

迁移自检清单

完成上述步骤后,可以按以下清单快速自查迁移是否完整:

  1. project/plugins.sbt中 sbt-plugin 版本为 2.5.x,project/build.properties中 sbt 版本为 0.13.11;
  2. scalaVersion已设置为 2.11.x,且多项目构建的每个 project 都生效;
  3. logback*.xml中不再引用play.api.Logger$ColoredLevel,编译期 DI 的 loader 使用LoggerConfigurator
  4. 路由生成器为默认的InjectedRoutesGenerator,控制器改为类并注入依赖;仍有静态控制器时使用StaticRoutesGenerator且路由前加@
  5. 代码中不再调用废弃的play.Play/play.api.Play静态方法,改用注入的EnvironmentConfiguration等组件;
  6. 检查play.ws.ning.*配置是否全部迁移为play.ws.ahc.*
  7. CSRF 相关客户端(尤其是application/jsonAJAX 与 REST 客户端)确认携带Csrf-Token头,或按需通过配置调整默认策略;
  8. play.server.netty.option.*已映射为 Netty 4.0 的ChannelOption键名;
  9. 文件发送场景确认sendFile/sendPath/sendResource的 inline 默认值是否符合预期。

Play 2.5 的迁移本质上是「拥抱 Java 8 与依赖注入、移除全局状态、统一流式处理」的一次整体演进:本指南覆盖的构建升级、Scala 2.11 迁移、路由生成器切换、Guice 行为收紧、CSRF 策略保守化与 Netty 4 升级,都是这一主线在具体 API 上的落地。对照上文各节与仓库源码逐项落实,即可平稳完成升级,并为后续版本(如 Scala 2.12 支持)打好基础。

  • 后端
  • Web框架

【免费下载链接】playframework

The Community Maintained High Velocity Web Framework For Java and Scala.

项目地址:https://gitcode.com/gh_mirrors/pl/playframework
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询