§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.2、4.2.1 和 4.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 validate
和 isValid
方法的有效负载
当使用 高级验证功能 时,您现在可以将包含有用信息的 ValidationPayload
对象(有时在验证过程中需要)传递给 Java 的 validate
或 isValid
方法。
要将此类有效负载传递给 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);
}
}
注意: 不要将
ValidationPayload
和ConstraintValidatorContext
混淆:前者由 Play 提供,是您在 Play 中处理表单时日常工作中使用的类。后者由 Bean Validation 规范 定义,仅在 Play 内部使用 - 只有一个例外:当您编写自己的自定义类级别约束时,此类会出现,如上面的最后一个示例,您只需要将其传递给reportValidationStatus
方法,但无论如何都需要这样做。
§对 Caffeine 的支持
Play 现在提供了一个基于 Caffeine 的 CacheApi 实现。Caffeine 是 Play 用户推荐的缓存实现。
要从 EhCache 迁移到 Caffeine,您需要从依赖项中删除 ehcache
并将其替换为 caffeine
。要自定义默认设置,您还需要更新 application.conf 中的配置,如文档中所述。
阅读 Java 缓存 API 和 Scala 缓存 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 应用程序中常见的做法,那么它是一个更好的选择。
您可以在 Java 或 Scala 的文档中阅读更多详细信息。
§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
中定义的级别。
下一步:迁移指南
发现此文档中的错误?此页面的源代码可以在 此处 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?前往 我们的社区论坛 与社区展开讨论。