文档

§Play 2.7 中的新功能

本页重点介绍 Play 2.7 的新功能。如果您想了解迁移到 Play 2.7 时需要进行的更改,请查看 Play 2.7 迁移指南

§Scala 2.13 支持

Play 2.7 是第一个针对 Scala 2.13、2.12 和 2.11 进行交叉构建的 Play 版本。为了实现这一点,许多依赖项都进行了更新。

您可以通过在 build.sbt 中设置 scalaVersion 设置来选择要使用的 Scala 版本。

对于 Scala 2.12

scalaVersion := "2.12.19"

对于 Scala 2.11

scalaVersion := "2.11.12"

对于 Scala 2.13

scalaVersion := "2.13.13"

§由 Akka 的协调关闭管理的生命周期

Play 2.6 引入了 Akka 的 协调关闭 的使用,但仍然没有在整个核心框架中使用它或将其暴露给最终用户。协调关闭是 Akka 扩展,它有一个任务注册表,这些任务可以在 Actor 系统关闭期间以有序方式运行。

协调关闭在内部处理 Play 2.7 Play 的生命周期,并且可以注入 CoordinatedShutdown 的实例。协调关闭为您提供了细粒度的阶段 - 组织为 有向无环图 (DAG) - 您可以在其中注册任务,而不是像 Play 的应用程序生命周期那样只有一个阶段。例如,您可以在服务器绑定之前或之后,或在所有当前请求完成之后添加要运行的任务。此外,您将更好地与 Akka 集群 集成。

您可以在 Play 手册的 协调关闭新部分 中找到更多详细信息,或者您可以查看 Akka 的 协调关闭参考文档

§Guice 已升级到 4.2.2

Guice 是 Play 使用的默认依赖注入框架,已升级到 4.2.2(从 4.1.0 开始)。查看 4.2.24.2.14.2.0 版本说明。此新 Guice 版本引入了重大更改,因此请确保您查看 Play 2.7 迁移指南

§Java 表单绑定 multipart/form-data 文件上传

在 Play 2.6 之前,检索通过 multipart/form-data 编码表单上传的文件的唯一方法是 通过调用 request.body().asMultipartFormData().getFile(...) 在操作方法中。

从 Play 2.7 开始,此类上传的文件现在也将绑定到 Java 表单。如果您没有使用 自定义多部分文件部分主体解析器,您只需在表单中添加一个 FilePart 类型为 TemporaryFile 的文件即可

import play.libs.Files.TemporaryFile;
import play.mvc.Http.MultipartFormData.FilePart;

public class MyForm {

  private FilePart<TemporaryFile> myFile;

  public void setMyFile(final FilePart<TemporaryFile> myFile) {
    this.myFile = myFile;
  }

  public FilePart<TemporaryFile> getMyFile() {
    return this.myFile;
  }
}

与之前一样,使用您注入到控制器中的 FormFactory 来创建表单

Form<MyForm> form = formFactory.form(MyForm.class).bindFromRequest(req);

如果绑定成功(表单验证通过),您可以访问该文件

MyForm myform = form.get();
myform.getMyFile();

还添加了一些有用的方法来处理上传的文件

// Get all files of the form
form.files();

// Access the file of a Field instance
Field myFile = form.field("myFile");
field.file();

// To access a file of a DynamicForm instance
dynamicForm.file("myFile");

注意:如果您使用的是 自定义多部分文件部分主体解析器,您只需用主体解析器使用的类型替换 TemporaryFile

§为 Play Java 提供的约束注释现在是 @Repeatable

play.data.validation.Constraints 定义的所有约束注释现在都是 @Repeatable。此更改允许您例如在同一个元素上多次重复使用同一个注释,但每次使用不同的 groups。但是,对于某些约束,让它们重复本身是有意义的,例如 @ValidateWith

@Validate(groups={GroupA.class})
@Validate(groups={GroupB.class})
public class MyForm {

    @ValidateWith(MyValidator.class)
    @ValidateWith(MyOtherValidator.class)
    @Pattern(value="[a-k]", message="Should be a - k")
    @Pattern(value="[c-v]", message="Should be c - v")
    @MinLength(value=4, groups={GroupA.class})
    @MinLength(value=7, groups={GroupB.class})
    private String name;

    //...
}

当然,您也可以使自己的自定义约束 @Repeatable,Play 会自动识别这一点。

§Java validateisValid 方法的有效负载

当使用 高级验证功能 时,您现在可以将包含有用信息的 ValidationPayload 对象(有时在验证过程中需要)传递给 Java 的 validateisValid 方法。
要将此类有效负载传递给 validate 方法,只需使用 @ValidateWithPayload(而不是 @Validate)注解您的表单,并实现 ValidatableWithPayload(而不是 Validatable)。

import java.util.Map;
import com.typesafe.config.Config;
import play.data.validation.Constraints.ValidatableWithPayload;
import play.data.validation.Constraints.ValidateWithPayload;
import play.data.validation.Constraints.ValidationPayload;
import play.i18n.Lang;
import play.i18n.Messages;
import play.libs.typedmap.TypedMap;

@ValidateWithPayload
public class SomeForm implements ValidatableWithPayload<String> {

    @Override
    public String validate(ValidationPayload payload) {
        Lang lang = payload.getLang();
        Messages messages = payload.getMessages();
        Map<String, Object> ctxArgs = payload.getArgs();
        TypedMap attrs = payload.getAttrs();
        Config config = payload.getConfig();
        // ...
    }

}

如果您编写了自己的 自定义类级别约束,您也可以通过实现 PlayConstraintValidatorWithPayload(而不是 PlayConstraintValidator)将有效负载传递给 isValid 方法。

import javax.validation.ConstraintValidatorContext;

import play.data.validation.Constraints.PlayConstraintValidatorWithPayload;
import play.data.validation.Constraints.ValidationPayload;
// ...

public class ValidateWithDBValidator implements PlayConstraintValidatorWithPayload<SomeValidatorAnnotation, SomeValidatableInterface<?>> {

    //...

    @Override
    public boolean isValid(final SomeValidatableInterface<?> value, final ValidationPayload payload, final ConstraintValidatorContext constraintValidatorContext) {
        // You can now pass the payload on to your custom validate(...) method:
        return reportValidationStatus(value.validate(...., payload), constraintValidatorContext);
    }

}

注意: 不要将 ValidationPayloadConstraintValidatorContext 混淆:前者由 Play 提供,是您在 Play 中处理表单时日常工作中使用的类。后者由 Bean Validation 规范 定义,仅在 Play 内部使用 - 只有一个例外:当您编写自己的自定义类级别约束时,此类会出现,如上面的最后一个示例,您只需要将其传递给 reportValidationStatus 方法,但无论如何都需要这样做。

§对 Caffeine 的支持

Play 现在提供了一个基于 Caffeine 的 CacheApi 实现。Caffeine 是 Play 用户推荐的缓存实现。

要从 EhCache 迁移到 Caffeine,您需要从依赖项中删除 ehcache 并将其替换为 caffeine。要自定义默认设置,您还需要更新 application.conf 中的配置,如文档中所述。

阅读 Java 缓存 APIScala 缓存 API 的文档,以了解有关使用 Play 配置缓存的更多信息。

§新的内容安全策略过滤器

现在提供了一个新的 内容安全策略过滤器,它支持嵌入内容的 CSP nonce 和哈希。

以前默认启用 CSP 并将其设置为 default-src 'self' 的设置过于严格,并且会干扰插件。CSP 过滤器默认情况下未启用,并且 SecurityHeaders 过滤器 中的 contentSecurityPolicy 现在已弃用,默认情况下设置为 null

CSP 过滤器默认使用 Google 的 严格 CSP 策略,这是一种基于 nonce 的策略。建议将其用作起点,并使用包含的 CSPReport 主体解析器和操作在生产环境中强制执行 CSP 之前记录 CSP 违规行为。

§HikariCP 升级

HikariCP 已更新至最新主要版本。请查看 迁移指南 以了解更改内容。

§Play WS 的 Java curl 过滤器

Play WS 允许您创建 play.libs.ws.WSRequestFilter 来检查或丰富发出的请求。Play 提供了一个“以 curl 格式记录”过滤器,但它缺少对 Java 开发者的支持。现在您可以编写类似以下内容:

ws.url("https://playframework.com.cn")
  .setRequestFilter(new AhcCurlRequestLogger())
  .addHeader("My-Header", "Header value")
  .get();

然后将打印以下日志

curl \
  --verbose \
  --request GET \
  --header 'My-Header: Header Value' \
  'https://playframework.com.cn'

如果您想单独重现请求,并更改 curl 参数以查看结果,这将特别有用。

§Gzip 过滤器现在支持压缩级别配置

使用 gzip 编码 时,您现在可以配置要使用的压缩级别。您可以使用 play.filters.gzip.compressionLevel 进行配置,例如

play.filters.gzip.compressionLevel = 9

有关更多详细信息,请参阅 GzipEncoding

§API 新增内容

以下是我们在 Play 2.7.0 中进行的一些相关 API 新增内容。

§Result HttpEntity 流式方法

以前版本的 Play 具有使用 HTTP 分块传输编码流式传输结果的便捷方法

Java
public Result chunked() {
  Source<ByteString, NotUsed> body = Source.from(Arrays.asList(ByteString.fromString("first"), ByteString.fromString("second")));
  return ok().chunked(body);
}
Scala
def chunked = Action {
  val body = Source(List("first", "second", "..."))
  Ok.chunked(body)
}

在 Play 2.6 中,没有便捷的方法以相同的方式返回流式传输的 Result,而无需使用 HTTP 分块传输编码。您必须编写以下内容

Java
public Result streamed() {
  Source<ByteString, NotUsed> body = Source.from(Arrays.asList(ByteString.fromString("first"), ByteString.fromString("second")));
  return ok().sendEntity(new HttpEntity.Streamed(body, Optional.empty(), Optional.empty()));
}
Scala
def streamed = Action {
  val body = Source(List("first", "second", "...")).map(s => ByteString.fromString(s))
  Ok.sendEntity(HttpEntity.Streamed(body, None, None))
}

Play 2.7 通过在结果上添加新的 streamed 方法来解决此问题,该方法的工作方式类似于 chunked

Java
public Result streamed() {
  Source<ByteString, NotUsed> body = Source.from(Arrays.asList(ByteString.fromString("first"), ByteString.fromString("second")));
  return ok().streamed(body, Optional.empty(), Optional.empty());
}
Scala
def streamed = Action {
  val body = Source(List("first", "second", "...")).map(s => ByteString.fromString(s))
  Ok.streamed(body, contentLength = None)
}

§新的 HTTP 错误处理程序

Play 2.7 带来了两种新的 play.api.http.HttpErrorHandler 实现。第一个是 JsonHttpErrorHandler,它将以 JSON 格式返回错误,如果您正在开发接受和返回 JSON 负载的 REST API,它是一个更好的选择。第二个是 HtmlOrJsonHttpErrorHandler,它根据客户端 Accept 标头中指定的首选项返回 HTML 或 JSON 错误。如果您应用程序使用 HTML 和 JSON 的混合,这是现代 Web 应用程序中常见的做法,那么它是一个更好的选择。

您可以在 JavaScala 的文档中阅读更多详细信息。

§Router.withPrefix 的更简洁语法

在 Play 2.7 中,我们引入了一些语法糖来使用 play.api.routing.Router.withPrefix。无需编写

val router = apiRouter.withPrefix("/api")

您现在可以编写

val router = "/api" /: apiRouter

甚至可以组合更多路径段

val router = "/api" /: "v1" /: apiRouter

§连接路由器

在 Play 2.7 中,我们引入了一种新的 orElse 方法来以编程方式组合 Routers
现在您可以按以下方式组合路由器

Java
Router router = oneRouter.orElse(anotherRouter)
Scala
val router = oneRouter.orElse(anotherRouter)

§数据库事务的隔离级别

现在,在使用 play.api.db.Database.withTransaction API(Java 用户使用 play.db.Database)时,您可以选择隔离级别。例如

Java
public void someDatabaseOperation() {
  database.withTransaction(TransactionIsolationLevel.ReadUncommitted, connection -> {
    ResultSet resultSet = connection.prepareStatement("select * from users where id = 10").executeQuery();
    // consume the resultSet and return some value
  });
}
Scala
def someDatabaseOperation(): Unit = {
  database.withTransaction(TransactionIsolationLevel.ReadUncommitted) { connection =>
    val resultSet: ResultSet = connection.prepareStatement("select * from users where id = 10").executeQuery();
    // consume the resultSet and return some value
  }
}

可用的事务隔离级别模仿 java.sql.Connection 中定义的级别。

下一步:迁移指南


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