文档

§Play 2.6 迁移指南

本指南介绍如何从 Play 2.5 迁移到 Play 2.6。如果您需要从更早版本的 Play 迁移,则必须首先按照 Play 2.5 迁移指南 进行操作。

§如何迁移

在您可以在 sbt 中加载/运行 Play 项目之前,需要执行以下步骤来更新您的 sbt 构建。

§Play 升级

project/plugins.sbt 中更新 Play 版本号以升级 Play

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

其中 2.6.x 中的“x”是您要使用的 Play 的次要版本,例如 2.6.0

§sbt 升级到 0.13.15

Play 2.6 需要升级到至少 sbt 0.13.15。sbt 的 0.13.15 版本包含许多 改进和错误修复(另请参阅 sbt 0.13.13 中的更改)。

从 Play 2.6.6 开始支持 sbt 1.x。如果您使用其他 sbt 插件,您可能需要检查是否有与 sbt 1.x 兼容的更新版本

要更新,请更改您的 project/build.properties,使其读取

sbt.version=0.13.15

§Guice DI 支持已移至单独的模块

在 Play 2.6 中,核心 Play 模块不再包含 Guice。您需要通过将 guice 添加到您的 libraryDependencies 来配置 Guice 模块

libraryDependencies += guice

§OpenID 支持已移至单独的模块

在 Play 2.6 中,核心 Play 模块不再包含 play.api.libs.openid(Scala)和 play.libs.openid(Java)中的 OpenID 支持。要使用这些包,请将 openId 添加到您的 libraryDependencies

libraryDependencies += openId

§Play JSON 已移至单独的项目

Play JSON 已经迁移到一个单独的库,托管在 https://github.com/playframework/play-json。由于 Play JSON 不依赖于 Play 的其他部分,因此主要变化是 PlayImport 中的 json 值将不再在您的 sbt 构建中起作用。相反,您需要手动指定库。

libraryDependencies += "com.typesafe.play" %% "play-json" % "2.6.0"

此外,play-json 具有与核心 Play 库不同的发布周期,因此版本不再与 Play 版本同步。

§Play Iteratees 迁移到单独的项目

Play Iteratees 已经迁移到一个单独的库,托管在 https://github.com/playframework/play-iteratees。由于 Play Iteratees 不依赖于 Play 的其他部分,因此主要变化是您需要手动指定库。

libraryDependencies += "com.typesafe.play" %% "play-iteratees" % "2.6.1"

该项目还有一个子项目,将 Iteratees 与 Reactive Streams 集成。您可能还需要添加以下依赖项。

libraryDependencies += "com.typesafe.play" %% "play-iteratees-reactive-streams" % "2.6.1"

注意:辅助类 play.api.libs.streams.Streams 已移至 play-iteratees-reactive-streams,现在称为 play.api.libs.iteratee.streams.IterateeStreams。因此,您可能需要添加 Iteratees 依赖项,并在必要时使用新类。

最后,Play Iteratees 具有单独的版本控制方案,因此版本不再与 Play 版本同步。

§Akka HTTP 作为默认服务器引擎

Play 现在使用 Akka-HTTP 服务器引擎作为默认后端。如果您需要出于某种原因将其更改回 Netty(例如,如果您使用的是 Netty 的 本机传输),请参阅 Netty 服务器 文档中的操作方法。

您可以在 Akka HTTP 服务器后端 中了解更多信息。

§Akka HTTP 服务器超时

Play 2.5.x 版本没有为 Netty 服务器 提供请求超时配置,而 Netty 服务器是默认的服务器后端。但是 Akka HTTP 针对空闲连接和请求都设置了超时时间(详情请参见 Akka HTTP 设置 文档)。Akka HTTP 文档 指出

Akka HTTP 提供了多种内置超时机制,以保护您的服务器免受恶意攻击或编程错误的影响。

您可以查看 此处akka.http.server.idle-timeoutakka.http.server.request-timeoutakka.http.server.bind-timeout 的默认值。Play 有 自己的配置来定义超时时间,因此,如果您开始看到大量的 503 服务不可用 错误,您可以将配置更改为更适合您的应用程序的值,例如

play.server.http.idleTimeout = 60s
play.server.akka.requestTimeout = 40s

§Scala Mode 更改

Scala Mode 已从枚举重构为案例对象层次结构。由于此重构,大多数 Scala 代码不会发生变化。但是,如果您在 Java 代码中访问 Scala Mode 值,则需要将其从

// Consider this Java code
play.api.Mode scalaMode = play.api.Mode.Test();

必须重写为

// Consider this Java code
play.api.Mode scalaMode = play.Mode.TEST.asScala();

在 Java 和 Scala 模式之间进行转换也更加容易

// In your Java code
play.api.Mode scalaMode = play.Mode.DEV.asScala();

或者在您的 Scala 代码中

play.Mode javaMode = play.api.Mode.Dev.asJava

此外,play.api.Mode.Mode 现在已弃用,您应该使用 play.api.Mode 代替。

§Writeable[JsValue] 更改

以前,默认的 Scala Writeable[JsValue] 允许您定义隐式 Codec,这将允许您使用不同的字符集进行写入。这可能是一个问题,因为 application/json 不像基于文本的内容类型那样工作。它只允许 Unicode 字符集(UTF-8UTF-16UTF-32),并且不像许多基于文本的内容类型那样定义 charset 参数。

现在,默认的 Writeable[JsValue] 不接受任何隐式参数,并且始终写入 UTF-8。这涵盖了大多数情况,因为大多数用户希望对 JSON 使用 UTF-8。它还允许我们轻松地使用更有效的内置方法将 JSON 写入字节数组。

如果您需要恢复旧的行为,可以使用 play.api.http.Writeable.writeableOf_JsValue(codec, contentType) 为您的所需 Codec 和 Content-Type 定义一个带有任意编解码器的 Writeable

§Scala 控制器变更

过去,惯用的 Play 控制器需要全局状态。主要需要全局状态的地方是全局的 play.api.mvc.Action 对象和 BodyParsers#parse 方法。

我们提供了几个新的控制器类,它们提供了新的注入状态的方式,并提供相同的语法。
- BaseController: 一个带有抽象 ControllerComponents 的特质,可以由实现类提供。
- AbstractController: 一个扩展 BaseController 的抽象类,带有 ControllerComponents 构造函数参数,可以使用构造函数注入进行注入。
- InjectedController: 一个扩展 BaseController 的特质,通过方法注入(调用 setControllerComponents 方法)获取 ControllerComponents。如果您使用的是像 Guice 这样的运行时 DI 框架,则会自动完成此操作。

ControllerComponents 只是为了将通常在控制器中使用的组件捆绑在一起。您也可以通过扩展 ControllerHelpers 并注入您自己的组件捆绑包来创建您自己的应用程序基本控制器。Play 不要求您的控制器实现任何特定的特质。

请注意,BaseController 使 Actionparse 引用注入的实例而不是全局对象,这通常是您想要做的。

以下是用 AbstractController 的代码示例

class FooController @Inject() (components: ControllerComponents)
    extends AbstractController(components) {

  // Action and parse now use the injected components
  def foo = Action(parse.json) {
    Ok
  }
}

以及使用 BaseController

class FooController @Inject() (val controllerComponents: ControllerComponents) extends BaseController {

  // Action and parse now use the injected components
  def foo = Action(parse.json) {
    Ok
  }
}

以及 InjectedController

class FooController @Inject() () extends InjectedController {

  // Action and parse now use the injected components
  def foo = Action(parse.json) {
    Ok
  }
}

InjectedController 通过调用 setControllerComponents 方法获取其 ControllerComponents,该方法由符合 JSR-330 的依赖注入自动调用。我们不建议将 InjectedController 与编译时注入一起使用。如果您计划手动对控制器进行广泛的单元测试,我们也建议避免使用 InjectedController,因为它隐藏了依赖关系。

如果您希望手动传递单个依赖项,您可以这样做,并扩展 ControllerHelpers,它没有依赖项或状态。以下是一个示例

class Controller @Inject() (
    action: DefaultActionBuilder,
    parse: PlayBodyParsers,
    messagesApi: MessagesApi
  ) extends ControllerHelpers {
  def index = action(parse.text) { request =>
    Ok(messagesApi.preferred(request)("hello.world"))
  }
}

§Scala ActionBuilder 和 BodyParser 更改

Scala ActionBuilder 特性已被修改,以指定主体类型作为类型参数,并添加一个抽象的 parser 成员作为默认主体解析器。您需要修改您的 ActionBuilders 并直接传递主体解析器。

play.api.mvc.Action 全局对象和 BodyParsers#parse 现在已弃用。它们分别被可注入的特性 DefaultActionBuilderPlayBodyParsers 取代。如果您在控制器内部,它们会由新的 BaseController 特性自动提供(请参阅上面的 控制器更改)。

§Cookies

对于 Java 用户,我们现在建议使用 play.mvc.Http.Cookie.builder 创建新的 cookie,例如

Http.Cookie cookie = Cookie.builder("color", "blue")
  .withMaxAge(3600)
  .withSecure(true)
  .withHttpOnly(true)
  .withSameSite(SameSite.STRICT)
  .build();

这比直接构造函数调用更易读,并且如果我们在将来添加/删除 cookie 属性,将保持源代码兼容。

§SameSite 属性,已为会话和闪存启用

Cookie 现在可以具有额外的 SameSite 属性,该属性可用于防止 CSRF。有三种可能的状态

此外,我们已将会话和闪存 cookie 默认设置为使用 `SameSite=Lax`。您可以使用配置调整此设置。例如

play.http.session.sameSite = null // no same-site for session
play.http.flash.sameSite = "strict" // strict same-site for flash

注意:此功能目前 不受许多浏览器支持,因此您不应该依赖它。Chrome 和 Opera 是目前唯一支持 SameSite 的主要浏览器。

§__Host 和 __Secure 前缀

我们还添加了对 __Host 和 __Secure cookie 名称前缀 的支持。

这只会影响您是否碰巧使用这些前缀作为 cookie 名称。如果您是,Play 会在序列化和反序列化这些 cookie 时发出警告,如果未设置适当的属性,则会自动设置这些属性。要消除警告,请停止使用这些前缀作为您的 cookie,或者确保按如下方式设置属性

§资产

§使用编译时 DI 绑定资产

如果您使用编译时 DI,您应该混合使用 controllers.AssetsComponents 并使用它来获取 assets: Assets 控制器实例

class MyComponents(context: Context) extends BuiltInComponentsFromContext(context) with AssetsComponents {
  lazy val router = new Routes(httpErrorHandler, assets)
}

如果您有现有的 lazy val assets: Assets,您可以将其删除。

§资产配置

现有的面向用户的 API 尚未更改,但我们建议迁移到 AssetsFinder API 以查找资产并在配置中设置您的资产目录

play.assets {
  path = "/public"
  urlPrefix = "/assets"
}

然后在路由中,您可以执行

# prefix must match `play.assets.urlPrefix`
GET /assets/*file           controllers.Assets.at(file)
GET /versionedAssets/*file  controllers.Assets.versioned(file)

您不再需要在参数列表的开头提供资产路径,因为现在从配置中读取。

然后在您的模板中,您可以使用 AssetsFinder#path 查找资产的最终路径

@(assets: AssetsFinder)

<img alt="hamburger" src="@assets.path("images/hamburger.jpg")">

您仍然可以使用 Assets.versioned 来继续使用反向路由,但需要一些全局状态来将您提供的资产名称转换为最终的资产名称,如果您想同时运行多个应用程序,这可能会出现问题。

§表单更改

从 Play 2.6 开始,当使用 bindFromRequest()POSTPUTPATCH 请求结合使用时,查询字符串参数将不再绑定到表单实例。

在 2.5 中已弃用的静态方法(例如 DynamicForm.form())在此版本中已删除。有关如何迁移的详细信息,请参阅 Play 2.5 迁移指南,如果您仍然使用它们。

§Java 表单更改

errors() 方法现在已弃用,该方法是 play.data.Form 实例。您现在应该使用 allErrors(),它返回一个简单的 List<ValidationError> 而不是 Map<String,List<ValidationError>>。在 Play 2.6 之前,您调用 .errors().get("key"),现在您可以简单地调用 .errors("key")

从现在开始,在表单类中实现的 validate 方法(通常用于跨字段验证)是类级别约束的一部分。有关如何使用此类约束的更多信息,请查看 高级验证 文档。
通过使用 @Validate 注释受影响的表单类,以及根据 validate 方法的返回类型,通过使用适用的类型参数实现 Validatable 接口(所有这些都定义在 play.data.validation.Constraints 中),可以轻松迁移现有的 validate 方法。

返回类型 要实现的接口
String Validatable<String>
ValidationError Validatable<ValidationError>
List<ValidationError> Validatable<List<ValidationError>>
Map<String,List<ValidationError>>
(不再支持;请改用 List
Validatable<List<ValidationError>>

例如,现有的表单,例如

public class MyForm {
    //...
    public String validate() {
        //...
    }
}

必须更改为

import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;

@Validate
public class MyForm implements Validatable<String> {
    //...
    @Override
    public String validate() {
        //...
    }
}

注意:以前,validate 方法只有在所有其他约束都成功后才会被调用。默认情况下,类级别的约束会与其他约束注解同时调用 - 无论它们是否通过或失败。要(也)在约束之间定义顺序,现在可以使用 约束组

§JPA 迁移说明

请参阅 JPA 迁移说明

§I18n 迁移说明

请参阅 I18N API 迁移

§缓存 API 迁移说明

请参阅 缓存 API 迁移

§Java 配置 API 迁移说明

请参阅 Java 配置迁移

§Scala 配置 API

Scala play.api.Configuration API 现在有新的方法,允许使用 ConfigLoader 加载任何类型。这些新方法期望配置键存在于配置文件中。例如,以下旧代码

val myConfig: String = configuration.getString("my.config.key").getOrElse("default")

应更改为

val myConfig: String = configuration.get[String]("my.config.key")

并且值“default”应在配置中设置为 my.config.key = default

或者,如果代码中需要自定义逻辑来获取默认值,您可以在配置文件中将默认值设置为 null(my.config.key = null),并读取 Option[T]

val myConfigOption: Option[String] = configuration.get[Option[String]]("my.config.key")
val myConfig: String = myConfigOption.getOrElse(computeDefaultValue())

此外,旧的 play.api.Configuration 中有几个方法返回 Java 类型,例如 getBooleanList。我们建议尽可能使用 Scala 版本 get[Seq[Boolean]]。如果无法做到,您可以访问 underlying Config 对象并从中调用 getBooleanList

现有方法上的弃用消息也解释了如何迁移每个方法。有关 play.api.Configuration 的正确使用,请参阅 Scala 配置文档

§Play JSON API 更改

§JSON 数组索引查找

如果您使用的是 Scala play-json API,则 JsLookup 隐式类的工作方式略有变化。例如,如果您有类似以下的代码

val bar = (jsarray(index) \ "bar").as[Bar]

其中index是数组索引,jsarrayJsArray,现在您应该编写

val bar = (jsarray \ index \ "bar").as[Bar]

这样做是为了使JsArray上的索引行为与Scala中其他集合的行为保持一致。现在jsarray(index)方法将返回索引处的value,如果不存在则抛出异常。

此外,play-json API 中也有一些小的变化,play.api.data.validation.ValidationError已更改为play.api.libs.json.JsonValidationError。例如,如果您有以下代码

ValidationError("Validation Error")

其中“Validation Error”是消息,现在您应该编写

JsonValidationError("Validation Error")

§已删除的 API

§已删除的加密 API

加密 API 已删除弃用的类play.api.libs.Cryptoplay.libs.CryptoAESCTRCrypter。CSRF 对Crypto的引用已被CSRFTokenSigner替换。会话 cookie 对Crypto的引用已被CookieSigner替换。有关更多信息,请参阅CryptoMigration25

§Akka 已删除的弃用方法

已删除弃用的静态方法play.libs.Akka.systemplay.api.libs.concurrent.Akka.system。使用依赖注入获取ActorSystem的实例并访问actor系统。

对于 Scala

class MyComponent @Inject() (system: ActorSystem) {

}

对于 Java

public class MyComponent {

    private final ActorSystem system;

    @Inject
    public MyComponent(ActorSystem system) {
        this.system = system;
    }
}

此外,Play 2.6.x 现在使用 Akka 2.5.x 版本系列。阅读 Akka 从 2.4.x 迁移到 2.5.x 的迁移指南,以了解如何在必要时调整自己的代码。

§已删除的 Yaml API

我们删除了play.libs.Yaml,因为它在 Play 中不再使用。如果您仍然需要 Play YAML 集成的支持,您需要在 build.sbt 中添加 snakeyaml

libraryDependencies += "org.yaml" % "snakeyaml" % "1.17"

并在您的代码中创建以下包装器

public class Yaml {

    private final play.Environment environment;

    @Inject
    public Yaml(play.Environment environment) {
        this.environment = environment;
    }

    /**
     * Load a Yaml file from the classpath.
     */
    public Object load(String resourceName) {
        return load(
            environment.resourceAsStream(resourceName),
            environment.classLoader()
        );
    }

    /**
     * Load the specified InputStream as Yaml.
     *
     * @param classloader The classloader to use to instantiate Java objects.
     */
    public Object load(InputStream is, ClassLoader classloader) {
        org.yaml.snakeyaml.Yaml yaml = new org.yaml.snakeyaml.Yaml(new CustomClassLoaderConstructor(classloader));
        return yaml.load(is);
    }

}

或在 Scala 中

class Yaml @Inject()(environment: play.api.Environment) {
  def load(resourceName: String) = {
    load(environment.resourceAsStream(resourceName), environment.classLoader)
  }

  def load(inputStream: InputStream, classLoader: ClassLoader) = {
    new org.yaml.snakeyaml.Yaml(new CustomClassLoaderConstructor(classloader)).load(inputStream)
  }
}

如果您明确依赖于 Play 的备用 DI 库,或者定义了自己的自定义应用程序加载器,则无需进行任何更改。

提供 Play DI 支持的库应定义play.application.loader 配置键。如果没有提供外部 DI 库,Play 将拒绝启动,除非您将其指向ApplicationLoader

§已移除过时的 play.Routes

已移除用于创建 JavaScript 路由器的过时 play.Routes 类。您现在必须使用新的 Java 或 Scala 帮助程序

§已移除的库

为了使默认的 Play 发行版更小,我们移除了一些库。以下库不再是 Play 2.6 中的依赖项,因此如果您使用它们,则需要手动将它们添加到您的构建中。

§移除 Joda-Time

我们建议使用 java.time API,因此我们正在从 Play 的核心移除 joda-time 支持。

Play 的 Scala 表单库包含一些 Joda 格式。如果您不想迁移,可以在您的 build.sbt 中添加 jodaForms 模块

libraryDependencies += jodaForms

然后导入相应的对象

import play.api.data.JodaForms._

如果您需要在 play-json 中使用 Joda 支持,可以添加以下依赖项

libraryDependencies += "com.typesafe.play" %% "play-json-joda" % playJsonVersion

其中 playJsonVersion 是您要使用的 play-json 版本。Play 2.6.x 应该与 play-json 2.6.x 兼容。请注意,play-json 现在是一个独立的项目(稍后描述)。

import play.api.libs.json.JodaWrites._
import play.api.libs.json.JodaReads._

§移除 Joda-Convert

如果您在项目中使用 joda-convert,Play 在内部使用了一些 joda-convert,您需要将其添加到您的 build.sbt

libraryDependencies += "org.joda" % "joda-convert" % "1.8.1"

§移除 XercesImpl

对于 XML 处理,Play 使用了 Xerces XML 库。由于现代 JVM 使用 Xerces 作为参考实现,因此我们将其移除。如果您的项目依赖于外部包,您可以将其添加到您的 build.sbt

libraryDependencies += "xerces" % "xercesImpl" % "2.11.0"

§移除 H2

Play 的早期版本预先打包了 H2 数据库。但为了使 Play 的核心更小,我们将其移除。如果您使用 H2,可以将其添加到您的 build.sbt

libraryDependencies += "com.h2database" % "h2" % "1.4.193"

如果您只在测试中使用它,您也可以使用 Test 范围

libraryDependencies += "com.h2database" % "h2" % "1.4.193" % Test

在您添加依赖项后,H2 浏览器 仍然可以使用。

§移除 snakeyaml

Play 移除 play.libs.Yaml,因此对 snakeyaml 的依赖项被删除。如果您仍然使用它,请将其添加到您的 build.sbt

libraryDependencies += "org.yaml" % "snakeyaml" % "1.17"

另请参阅 有关移除 Yaml API 的说明

§Tomcat-servlet-api 移除

Play 移除 tomcat-servlet-api,因为它已经没有用了。如果您仍然使用它,请将其添加到您的 build.sbt 中。

libraryDependencies += "org.apache.tomcat" % "tomcat-servlet-api" % "8.0.33"

§fork-run 移除

sbt-fork-run-plugin 将不再生成,因为它只在现在已停产的 activator 工具中需要。由于它在 2.6 中将不再解析,因此可以安全地完全删除。

§请求属性

所有请求对象现在都包含属性。请求属性是请求标签的替代品。标签现在已被弃用,您应该升级到属性。属性比标签更强大;您可以使用属性在请求中存储对象,而标签只支持存储字符串。

§请求标签弃用

标签已被弃用,因此您应该开始从使用标签迁移到使用属性。迁移应该相当简单。

最简单的迁移路径是从带有 String 类型的标签迁移到属性。

Java 之前

// Getting a tag from a Request or RequestHeader
String userName = req.tags().get("userName");
// Setting a tag on a Request or RequestHeader
req.tags().put("userName", newName);
// Setting a tag with a RequestBuilder
Request builtReq = requestBuilder.tag("userName", newName).build();

Java 之后

class Attrs {
  public static final TypedKey<String> USER_NAME = TypedKey.<String>create("userName");
}

// Getting an attribute from a Request or RequestHeader
String userName = req.attrs().get(Attrs.USER_NAME);
String userName = req.attrs().getOptional(Attrs.USER_NAME);
// Setting an attribute on a Request or RequestHeader
Request newReq = req.withTags(req.tags().put(Attrs.USER_NAME, newName));
// Setting an attribute with a RequestBuilder
Request builtReq = requestBuilder.attr(Attrs.USER_NAME, newName).build();

Scala 之前

// Getting a tag from a Request or RequestHeader
val userName: String = req.tags("userName")
val optUserName: Option[String] = req.tags.get("userName")
// Setting a tag on a Request or RequestHeader
val newReq = req.copy(tags = req.tags.updated("userName", newName))

Scala 之后

object Attrs {
  val UserName: TypedKey[String] = TypedKey("userName")
}
// Getting an attribute from a Request or RequestHeader
val userName: String = req.attrs(Attrs.UserName)
val optUserName: [String] = req.attrs.get(Attrs.UserName)
// Setting an attribute on a Request or RequestHeader
val newReq = req.addAttr(Attrs.UserName, newName)

但是,如果合适,我们建议您将 String 标签转换为具有非 String 值的属性。将标签转换为非 String 对象有几个好处。首先,您将使代码更类型安全。这将提高代码的可靠性,并使其更容易理解。其次,您存储在属性中的对象可以包含多个属性,允许您将多个标签聚合到一个值中。第三,将标签转换为属性意味着您不需要对 String 进行编码和解码,这可能会提高性能。

class Attrs {
  public static final TypedKey<User> USER = TypedKey.<User>create("user");
}

Scala 之后

object Attrs {
  val UserName: TypedKey[User] = TypedKey("user")
}

§调用 FakeRequest.withCookies 不再更新 Cookies 头部

请求 cookie 现在存储在请求属性中。以前它们存储在请求的 Cookie 头部 String 中。这需要在 cookie 发生变化时对 cookie 进行编码和解码到头部。

现在 cookie 存储在请求属性中,更新 cookie 将更改新的 cookie 属性,但不会更改 Cookie HTTP 头部。这只会影响您的测试,如果您依赖于调用 withCookies 将更新头部的这一事实。

如果您仍然需要旧的行为,您仍然可以使用 Cookies.encodeCookieHeaderCookie 对象转换为 HTTP 头部,然后使用 FakeRequest.withHeaders 存储头部。

§play.api.mvc.Security.username (Scala API),session.username 更改

play.api.mvc.Security.username (Scala API),session.username 配置键和依赖的操作助手已弃用。Security.username 仅从配置中检索 session.username 键,该键定义用于获取用户名 的会话键。它已被删除,因为它需要静态才能工作,并且自己实现相同或类似的行为相当容易。

您可以使用 configuration.get[String]("session.username") 自己从配置中读取用户名会话键。

如果您使用的是 Authenticated(String => EssentialAction) 方法,您可以轻松创建自己的操作来执行类似的操作

def AuthenticatedWithUsername(action: String => EssentialAction) =
  WithAuthentication[String](_.session.get(UsernameKey))(action)

其中 UsernameKey 代表您要用于用户名的会话键。

§请求安全 (Java API) 用户名属性现在是属性

Java 请求对象包含一个 username 属性,该属性在将 Security.Authenticated 注释添加到 Java 操作时设置。在 Play 2.6 中,用户名属性已弃用。用户名属性方法已更新为将用户名存储在 Security.USERNAME 属性中。您应该更新您的代码以直接使用 Security.USERNAME 属性。在 Play 的未来版本中,我们将删除用户名属性。

进行此更改的原因是,用户名属性是作为 Security.Authenticated 注释的特例提供的。现在我们有了属性,我们不再需要特例。

现有 Java 代码

// Set the username
Request reqWithUsername = req.withUsername("admin");
// Get the username
String username = req1.username();
// Set the username with a builder
Request reqWithUsername = new RequestBuilder().username("admin").build();

更新后的 Java 代码

import play.mvc.Security.USERNAME;

// Set the username
Request reqWithUsername = req.withAttr(USERNAME, "admin");
// Get the username
String username = req1.attr(USERNAME);
// Set the username with a builder
Request reqWithUsername = new RequestBuilder().putAttr(USERNAME, "admin").build();

§路由标签现在是属性

如果您使用了任何 Router.Tags.* 标签,您应该更改您的代码以使用新的 Router.Attrs.HandlerDef (Scala) 或 Router.Attrs.HANDLER_DEF (Java) 属性。现有的标签仍然可用,但已弃用,将在 Play 的未来版本中删除。

此新属性包含一个 HandlerDef 对象,其中包含当前在标签中的所有信息。当前的标签都对应于 HandlerDef 对象中的一个字段

Java 标签名称 Scala 标签名称 HandlerDef 方法
ROUTE_PATTERN 路由模式 路径
路由动词 路由动词 动词
路由控制器 路由控制器 控制器
路由动作方法 路由动作方法 方法
路由注释 路由注释 注释

注意:作为此更改的一部分,HandlerDef 对象已从 play.core.routing 内部包移动到 play.api.routing 公共 API 包。

§play.api.libs.concurrent.Execution 已弃用

play.api.libs.concurrent.Execution 类已弃用,因为它在幕后使用全局可变状态来获取“当前”应用程序的 ExecutionContext。

如果您想指定以前使用的隐式行为,则应使用 依赖注入 在构造函数中隐式传入执行上下文

class MyController @Inject()(implicit ec: ExecutionContext) {

}

或者如果您使用的是 编译时依赖注入,则从 BuiltInComponents 中传入。

class MyComponentsFromContext(context: ApplicationLoader.Context)
  extends BuiltInComponentsFromContext(context) {
  val myComponent: MyComponent = new MyComponent(executionContext)
}

但是,在一般情况下,您可能不想导入执行上下文,这有一些很好的理由。在一般情况下,应用程序的执行上下文适合渲染操作和执行不涉及阻塞 API 调用或 I/O 活动的 CPU 密集型活动。如果您要调用数据库或进行网络调用,则可能需要定义自己的自定义执行上下文。

创建自定义执行上下文的推荐方法是通过 CustomExecutionContext,它使用 Akka 调度程序系统 (java / scala),以便可以通过配置定义执行器。

要使用您自己的执行上下文,请使用 application.conf 文件中的调度程序的完整路径扩展 CustomExecutionContext 抽象类

import play.api.libs.concurrent.CustomExecutionContext

class MyExecutionContext @Inject()(actorSystem: ActorSystem)
 extends CustomExecutionContext(actorSystem, "my.dispatcher.name")
import play.libs.concurrent.CustomExecutionContext;
class MyExecutionContext extends CustomExecutionContext {
   @Inject
   public MyExecutionContext(ActorSystem actorSystem) {
     super(actorSystem, "my.dispatcher.name");
   }
}

然后根据需要注入您的自定义执行上下文

class MyBlockingRepository @Inject()(implicit myExecutionContext: MyExecutionContext) {
   // do things with custom execution context
}

有关自定义线程池配置的更多信息,请参见 线程池 页面,有关使用 CustomExecutionContext 的信息,请参见 JavaAsync / ScalaAsync

§对 play.api.test Helpers 的更改

以下已弃用的测试帮助程序已在 2.6.x 中删除

§Java API

§模板助手更改

views/helper/requireJs.scala.html 中的 requireJs 模板助手使用 Play.maybeApplication 访问配置。

requireJs 模板助手添加了一个额外的参数 isProd,用于指示是否应使用该助手的压缩版本。

@requireJs(core = routes.Assets.at("javascripts/require.js").url, module = routes.Assets.at("javascripts/main").url, isProd = true)

§文件扩展名到 MIME 类型映射的更改

文件扩展名到 MIME 类型的映射已移至 reference.conf,因此它完全通过配置覆盖,位于 play.http.fileMimeTypes 设置下。以前,该列表在 play.api.libs.MimeTypes 中是硬编码的。

请注意,play.http.fileMimeTypes 配置设置使用三个引号定义为单个字符串 - 这是因为几个文件扩展名具有破坏 HOCON 语法的语法,例如 c++

要追加自定义 MIME 类型,请使用 HOCON 字符串值连接

play.http.fileMimeTypes = ${play.http.fileMimeTypes} """
  foo=text/bar
"""

有一种语法允许将配置定义为 mimetype.foo=text/bar 用于其他 MIME 类型。这已弃用,建议您使用上述配置。

§Java API

Http.Context.current().fileMimeTypes() 方法在幕后提供给 Results.sendFile 和其他从文件扩展名查找内容类型的方法。无需迁移。

§Scala API

play.api.libs.MimeTypes 类已更改为 play.api.http.FileMimeTypes 接口,并且实现已更改为 play.api.http.DefaultFileMimeTypes.

所有发送文件或资源的结果现在隐式地接受 FileMimeTypes,即

implicit val fileMimeTypes: FileMimeTypes = ...
Ok(file) // <-- takes implicit FileMimeTypes

BaseController(及其子类 AbstractControllerInjectedController)通过 ControllerComponents 类提供了一个隐式 FileMimeTypes 实例,以提供方便的绑定。

class SendFileController @Inject() (cc: ControllerComponents) extends AbstractController(cc) {

  def index() = Action { implicit request =>
     val file = readFile()
     Ok(file)  // <-- takes implicit FileMimeTypes
  }
}

您也可以在单元测试中直接获取一个完全配置的 FileMimeTypes 实例。

val httpConfiguration = new HttpConfigurationProvider(Configuration.load(Environment.simple)).get
val fileMimeTypes = new DefaultFileMimeTypesProvider(httpConfiguration.fileMimeTypes).get

或者获取一个自定义的实例。

val fileMimeTypes = new DefaultFileMimeTypesProvider(FileMimeTypesConfiguration(Map("foo" -> "text/bar"))).get

§默认过滤器

Play 现在附带了一组默认启用的过滤器,通过配置定义。如果属性 play.http.filters 为空,则默认值为 play.api.http.EnabledFilters,它会加载 play.filters.enabled 配置属性中完全限定的类名定义的过滤器。

在 Play 本身中,play.filters.enabled 是一个空列表。但是,过滤器库会作为名为 PlayFilters 的 AutoPlugin 自动加载到 sbt 中,并将以下值追加到 play.filters.enabled 属性。

这意味着在新的项目中,CSRF 保护(ScalaCsrf / JavaCsrf)、SecurityHeadersAllowedHostsFilter 都被自动定义。

§默认过滤器的影响

默认过滤器被配置为为项目提供“默认安全”配置。

您应该保持这些过滤器启用:它们使您的应用程序更安全。

如果您在现有项目中没有启用这些过滤器,则需要进行一些配置,并且您可能不熟悉所涉及的错误和故障。为了帮助迁移,我们将介绍每个过滤器,它的作用以及所需的配置。

§CSRFFilter

CSRF 过滤器在 ScalaCsrfJavaCsrf 中有描述。它通过在表单中添加一个 CSRF 令牌来防止 跨站点请求伪造 攻击,该令牌在 POST 请求中进行检查。

§为什么默认情况下启用它

CSRF 是一种非常常见的攻击,实施起来非常容易。您可以在 https://github.com/Manc/play-scala-csrf 上看到使用 Play 进行 CSRF 攻击的示例。

§我需要做哪些更改?

如果您正在从不使用 CSRF 表单助手(例如 CSRF.formField)的现有项目迁移,那么您可能会在来自 CSRF 过滤器的 PUT 和 POST 请求上看到“403 Forbidden”。

如果您使用 AJAX 发出请求,则将 CSRF.formField 添加到您的表单模板将解决错误,您可以将 CSRF 令牌放在 HTML 页面中,然后使用 Csrf-Token 标头将其添加到请求中。

要检查此行为,请将 <logger name="play.filters.csrf" value="TRACE"/> 添加到您的 logback.xml 中。

您可能还想在 Play 中启用 SameSite Cookie,这为防御 CSRF 攻击提供了额外的保护。

§SecurityHeadersFilter

SecurityHeadersFilter 通过向请求添加额外的 HTTP 标头来防止 跨站点脚本点击劫持 攻击。

§为什么默认启用?

基于浏览器的攻击非常普遍,安全标头可以提供纵深防御,帮助挫败这些攻击。

§我需要做哪些更改?

默认的“Content-Security-Policy”设置非常严格,您可能需要对其进行试验才能找到最有效的设置。Content-Security-Policy 设置将改变 Javascript 和远程框架在浏览器中的显示方式。在您修改 Content-Security-Policy 标头之前,嵌入的 Javascript 或 CSS 不会加载到您的网页中。

如果您确定不想启用它,您可以按如下方式禁用 Content-Security-Policy

play.filters.headers.contentSecurityPolicy=null

CSP-Useful 是关于 Content-Security-Policy 的一个很好的资源。请注意,还有其他潜在的解决方案来解决嵌入式 Javascript,例如在每个请求上添加自定义 CSP nonce。

其他标头侵入性较小,在普通网站上不太可能造成问题,但在单页应用程序上可能会导致 Cookie 或渲染问题。Mozilla 有详细描述每个标头的文档,使用 URL 中的标头名称:例如,对于 X-Frame-Options,请访问 https://mdn.org.cn/en-US/docs/Web/HTTP/Headers/X-Frame-Options

play.filters.headers {

    # The X-Frame-Options header. If null, the header is not set.
    frameOptions = "DENY"

    # The X-XSS-Protection header. If null, the header is not set.
    xssProtection = "1; mode=block"

    # The X-Content-Type-Options header. If null, the header is not set.
    contentTypeOptions = "nosniff"

    # The X-Permitted-Cross-Domain-Policies header. If null, the header is not set.
    permittedCrossDomainPolicies = "master-only"

    # The Content-Security-Policy header. If null, the header is not set.
    contentSecurityPolicy = "default-src 'self'"

    # The Referrer-Policy header. If null, the header is not set.
    referrerPolicy = "origin-when-cross-origin, strict-origin-when-cross-origin"

    # If true, allow an action to use .withHeaders to replace one or more of the above headers
    allowActionSpecificHeaders = false
}

§AllowedHostsFilter

AllowedHostsFilter 添加了一个允许的主机白名单,并向所有主机不匹配白名单的请求发送 400(错误请求)响应。

§为什么默认情况下启用它

这是开发中使用的一个重要过滤器,因为 DNS 重绑定攻击可以针对开发人员的 Play 实例:请参阅 Rails Webconsole DNS 重绑定,了解短时 DNS 重绑定如何攻击在 localhost 上运行的服务器的示例。

§我需要做哪些更改?

如果您在 localhost 以外的地方运行 Play 应用程序,则必须配置 AllowedHostsFilter 以专门允许您连接到的主机名/IP。当您更改环境时,这一点尤其重要,因为通常您会在开发中在 localhost 上运行,但在暂存和生产中会远程运行。

play.filters.hosts {
  # Allow requests to example.com, its subdomains, and localhost:9000.
  allowed = [".example.com", "localhost:9000"]
}

§追加到过滤器

要追加到默认列表,请使用 +=

play.filters.enabled+=MyFilter

如果您通过扩展 play.api.http.DefaultHttpFilters 定义了自己的过滤器,那么您也可以在代码中将 EnabledFilters 与您自己的列表组合,因此如果您之前定义了项目,它们仍然像往常一样工作

class Filters @Inject()(enabledFilters: EnabledFilters, corsFilter: CORSFilter)
  extends DefaultHttpFilters(enabledFilters.filters :+ corsFilter: _*)

§测试默认过滤器

由于启用了多个过滤器,因此功能测试可能需要略微更改以确保所有测试通过且请求有效。例如,没有将 Host HTTP 标头设置为 localhost 的请求将无法通过 AllowedHostsFilter,并将返回 400 Forbidden 响应。

§使用 AllowedHostsFilter 进行测试

由于 AllowedHostsFilter 过滤器是自动添加的,因此功能测试需要添加 Host HTTP 标头。

如果您使用的是 FakeRequestHelpers.fakeRequest,则会自动为您添加 Host HTTP 标头。如果您使用的是 play.mvc.Http.RequestBuilder,则可能需要手动添加自己的行来添加标头。

RequestBuilder request = new RequestBuilder()
        .method(GET)
        .header(HeaderNames.HOST, "localhost")
        .uri("/xx/Kiwi");

§使用 CSRFFilter 进行测试

由于 CSRFFilter 过滤器是自动添加的,因此渲染包含 CSRF.formField 的 Twirl 模板的测试,即

@(userForm: Form[UserData])(implicit request: RequestHeader, m: Messages)

<h1>user form</h1>

@request.flash.get("success").getOrElse("")

@helper.form(action = routes.UserController.userPost()) {
  @helper.CSRF.formField
  @helper.inputText(userForm("name"))
  @helper.inputText(userForm("age"))
  <input type="submit" value="submit"/>
}

必须在请求中包含 CSRF 令牌。在 Scala API 中,这是通过导入 play.api.test.CSRFTokenHelper._ 来完成的,它使用 withCSRFToken 方法来丰富 play.api.test.FakeRequest

import play.api.test.CSRFTokenHelper._

class UserControllerSpec extends PlaySpec with GuiceOneAppPerTest {
  "UserController GET" should {

    "render the index page from the application" in {
      val controller = app.injector.instanceOf[UserController]
      val request = FakeRequest().withCSRFToken
      val result = controller.userGet().apply(request)

      status(result) mustBe OK
      contentType(result) mustBe Some("text/html")
    }
  }
}

在 Java API 中,这是通过在 play.mvc.Http.RequestBuilder 实例上调用 CSRFTokenHelper.addCSRFToken 来完成的。

requestBuilder = CSRFTokenHelper.addCSRFToken(requestBuilder);

§禁用默认过滤器

禁用默认过滤器的最简单方法是在 application.conf 中手动设置过滤器列表。

play.filters.enabled=[]

如果您有不想经过默认过滤器的功能测试,这可能很有用。

如果您想删除所有过滤器类,可以通过 disablePlugins 机制禁用它。

lazy val root = project.in(file(".")).enablePlugins(PlayScala).disablePlugins(PlayFilters)

或通过替换 EnabledFilters

play.http.filters=play.api.http.NoHttpFilters

如果您正在编写涉及 GuiceApplicationBuilder 的功能测试,并且想要禁用默认过滤器,那么您可以通过使用 configure 通过配置禁用所有或部分过滤器。

GuiceApplicationBuilder().configure("play.http.filters" -> "play.api.http.NoHttpFilters")

§编译时默认过滤器

如果您使用的是编译时依赖注入,则默认过滤器是在编译时解析的,而不是通过运行时解析的。

这意味着 BuiltInComponents 特性现在包含一个 httpFilters 方法,该方法是抽象的。

trait BuiltInComponents {

  /** A user defined list of filters that is appended to the default filters */
  def httpFilters: Seq[EssentialFilter]
}

默认过滤器列表在 play.filters.HttpFiltersComponents 中定义。

trait HttpFiltersComponents
     extends CSRFComponents
     with SecurityHeadersComponents
     with AllowedHostsComponents {

   def httpFilters: Seq[EssentialFilter] = Seq(csrfFilter, securityHeadersFilter, allowedHostsFilter)
}

在大多数情况下,您将希望混合 HttpFiltersComponents 并追加您自己的过滤器。

class MyComponents(context: ApplicationLoader.Context)
  extends BuiltInComponentsFromContext(context)
  with play.filters.HttpFiltersComponents {

  lazy val loggingFilter = new LoggingFilter()
  override def httpFilters = {
    super.httpFilters :+ loggingFilter
  }
}

如果您想从列表中过滤掉元素,您可以执行以下操作。

class MyComponents(context: ApplicationLoader.Context)
  extends BuiltInComponentsFromContext(context)
  with play.filters.HttpFiltersComponents {
  override def httpFilters = {
    super.httpFilters.filterNot(_.getClass == classOf[CSRFFilter])
  }
}

§禁用编译时默认过滤器

要禁用默认过滤器,请混合 play.api.NoHttpFiltersComponents

class MyComponents(context: ApplicationLoader.Context)
   extends BuiltInComponentsFromContext(context)
   with NoHttpFiltersComponents
   with AssetsComponents {

  lazy val homeController = new HomeController(controllerComponents)
  lazy val router = new Routes(httpErrorHandler, homeController, assets)
}

§JWT 支持

Play 的 cookie 编码已切换为在后台使用 JSON Web Token (JWT)。JWT 具有许多优点,最值得注意的是使用 HMAC-SHA-256 进行自动签名,以及对自动“不早于”和“过期后”日期检查的支持,这些检查确保会话 cookie 无法在给定时间窗口之外重复使用。

有关更多信息,请参阅 配置会话 Cookie 页面。

Play 的 Cookie 编码使用“回退”Cookie 编码机制,该机制读取 JWT 编码的 Cookie,然后在 JWT 解析失败时尝试读取 URL 编码的 Cookie,因此您可以安全地将现有会话 Cookie 迁移到 JWT。此功能位于 FallbackCookieDataCodec 特性中,并由 DefaultSessionCookieBakerDefaultFlashCookieBaker 利用。

§旧版支持

使用 JWT 编码的 Cookie 应该很无缝,但如果您愿意,可以通过在 application.conf 文件中切换到 play.api.mvc.LegacyCookiesModule 来恢复到 URL 编码的 Cookie 编码。

play.modules.disabled+="play.api.mvc.CookiesModule"
play.modules.enabled+="play.api.mvc.LegacyCookiesModule"

§自定义 CookieBaker

如果您在 Play 中使用自定义 Cookie,使用 CookieBaker[T] 特性,那么您需要指定您想要为自定义 Cookie baker 使用的编码类型。

encodedecode 方法将 Map[String, String] 转换到浏览器中找到的格式,并从该格式转换回来,这些方法已提取到 CookieDataCodec 中。有三种实现:FallbackCookieDataCodecJWTCookieDataCodecUrlEncodedCookieDataCodec,它们分别代表使用 HMAC 的 URL 编码、JWT 或“读取签名或 JWT,写入 JWT”编解码器。

您还需要提供一个 JWTConfiguration 案例类,使用 JWTConfigurationParser 和您的配置路径,或使用 JWTConfiguration() 获取默认值。

@Singleton
class UserInfoCookieBaker @Inject()(service: UserInfoService,
                                    val secretConfiguration: SecretConfiguration)
  extends CookieBaker[UserInfo] with JWTCookieDataCodec {

  override val COOKIE_NAME: String = "userInfo"

  override val isSigned = true

  override def emptyCookie: UserInfo = new UserInfo()

  override protected def serialize(userInfo: UserInfo): Map[String, String] = service.encrypt(userInfo)

  override protected def deserialize(data: Map[String, String]): UserInfo = service.decrypt(data)

  override val path: String = "/"

  override val jwtConfiguration: JWTConfiguration = JWTConfigurationParser()
}

§已弃用的 Futures 方法

以下 play.libs.concurrent.Futures 静态方法已弃用

应改为使用 Futures 的依赖注入实例。

class MyClass {
    @Inject
    public MyClass(play.libs.concurrent.Futures futures) {
        this.futures = futures;
    }

    CompletionStage<Double> callWithOneSecondTimeout() {
        return futures.timeout(computePIAsynchronously(), Duration.ofSeconds(1));
    }
}

§更新的库

§Netty 4.1

Netty 已升级到 版本 4.1。这主要是因为版本 4.0 被 play-ws 迁移到独立模块 进行了阴影处理。因此,如果您使用的是 Netty 服务器 和一些依赖于 Netty 4.0 的库,我们建议您尝试升级到库的较新版本,或者您可以开始使用 Akka 服务器

如果您出于某种原因直接使用 Netty 类,则应 将您的代码改编为这个新版本

§FluentLenium 和 Selenium

FluentLenium 库已更新到版本 3.2.0,Selenium 已更新到版本 3.3.1(您可能想查看 此处的变更日志)。如果您之前使用过 Selenium 的 WebDriver API,则无需执行任何操作。请查看 公告以获取更多信息。
如果您使用的是 FluentLenium 库,您可能需要更改一些语法才能使您的测试再次运行。有关如何改编代码的更多详细信息,请参阅 FluentLenium 的 迁移指南

§HikariCP

HikariCP 已更新,并引入了新的配置:initializationFailTimeout。此新配置应用于替换现已弃用的 initializationFailFast。请参阅 HikariCP 变更日志initializationFailTimeout 文档,以更好地了解如何使用此新配置。

§其他配置更改

存在一些配置更改。旧的配置路径通常仍然有效,但如果您使用它们,将在运行时输出弃用警告。以下是更改的键的摘要

旧键 新键
play.crypto.secret play.http.secret.key
play.crypto.provider play.http.secret.provider
play.websocket.buffer.limit play.server.websocket.frame.maxLength

下一步: 消息迁移


发现此文档中的错误?此页面的源代码可以在 此处 找到。在阅读 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?前往 我们的社区论坛 与社区进行交流。