文档

§Play 2.6 中的新功能

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

§Scala 2.12 支持

Play 2.6 是 Play 的第一个针对 Scala 2.12 和 2.11 进行交叉构建的版本。我们更新了一些依赖项,以便我们能够支持这两个版本。

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

对于 Scala 2.12

scalaVersion := "2.12.19"

对于 Scala 2.11

scalaVersion := "2.11.12"

§PlayService sbt 插件(实验性)

从 Play 2.6.8 开始,Play 还提供了一个 PlayService 插件。这是一个更简化的 Play 配置,面向微服务。它使用标准的 Maven 布局,而不是传统的 Play 布局,并且不包含 twirl 模板或 sbt-web 功能。例如

lazy val root = (project in file("."))
  .enablePlugins(PlayService)
  .enablePlugins(RoutesCompiler) // place routes in src/main/resources, or remove if using SIRD/RoutingDsl
  .settings(
    scalaVersion := "2.12.19",
    libraryDependencies ++= Seq(
      guice, // remove if not using Play's Guice loader
      akkaHttpServer, // or use nettyServer for Netty
      logback // add Play logging support
    )
  )

注意:此插件被认为是实验性的,这意味着 API 可能会发生变化。我们预计它将在 Play 2.7.0 中稳定。

§“无全局状态”应用程序

最大的底层变化是 Play 不再依赖于全局状态。您仍然可以在 Play 2.6 中通过 play.api.Play.current / play.Play.application() 访问全局应用程序,但它已弃用。这为 Play 3.0 奠定了基础,在 Play 3.0 中根本没有全局状态。

您可以通过设置以下配置值来完全禁用对全局应用程序的访问

play.allowGlobalApplication=false

上面的设置将在任何调用 Play.current 时引发异常。

§Akka HTTP 服务器后端

Play 现在使用 Akka-HTTP 服务器引擎作为默认后端。有关 Play 与 Akka-HTTP 集成的更多详细信息,请参见 Akka HTTP 服务器页面。还有一个关于 配置 Akka HTTP 的页面。

Netty 后端仍然可用,并且已升级为使用 Netty 4.1。您可以明确地配置您的项目以使用 Netty 在 NettyServer 页面上

§HTTP/2 支持(实验性)

Play 现在在 Akka HTTP 服务器上使用 PlayAkkaHttp2Support 模块支持 HTTP/2。

lazy val root = (project in file("."))
  .enablePlugins(PlayJava, PlayAkkaHttp2Support)

这自动化了设置 HTTP/2 的大部分过程。但是,默认情况下它不适用于 run 命令。有关更多详细信息,请参阅 Akka HTTP 服务器页面

§请求属性

Play 2.6 中的请求现在包含属性。属性允许您在请求对象中存储额外的信息。例如,您可以编写一个过滤器,它在请求中设置一个属性,然后从您的操作中访问该属性值。

属性存储在一个附加到每个请求的 TypedMap 中。TypedMap 是不可变的映射,存储类型安全的键和值。属性按键索引,键的类型指示属性的类型。

Java

// Create a TypedKey to store a User object
class Attrs {
  public static final TypedKey<User> USER = TypedKey.<User>create("user");
}

// Get the User object from the request
User user = req.attrs().get(Attrs.USER);
// Put a User object into the request
Request newReq = req.addAttr(Attrs.USER, newUser);

Scala

// Create a TypedKey to store a User object
object Attrs {
  val User: TypedKey[User] = TypedKey.apply[User]("user")
}

// Get the User object from the request
val user: User = req.attrs(Attrs.User)
// Put a User object into the request
val newReq = req.addAttr(Attrs.User, newUser)

属性存储在 TypedMap 中。您可以在 TypedMap 文档中阅读有关属性的更多信息:JavadocScaladoc

请求标签现在已弃用,您应该迁移到使用属性。有关更多信息,请参阅迁移文档中的 标签部分

§路由修饰符标签

路由文件语法现在允许您为每个路由添加“修饰符”,以提供自定义行为。我们在 CSRF 过滤器中实现了一个这样的标签,“nocsrf”标签。默认情况下,以下路由将不会应用 CSRF 过滤器。

+ nocsrf # Don't CSRF protect this route
POST /api/foo/bar ApiController.foobar

您还可以创建自己的修饰符:+ 符号后面可以跟任意数量的空格分隔的标签。

这些在 HandlerDef 请求属性中可用(该属性还包含有关路由文件中处理程序定义的其他元数据)

Java

import java.util.List;
import play.routing.HandlerDef;
import play.routing.Router;

HandlerDef handler = req.attrs().get(Router.Attrs.HANDLER_DEF);
List<String> modifiers = handler.getModifiers();

Scala

import play.api.routing.{ HandlerDef, Router }
import play.api.mvc.RequestHeader

val handler = request.attrs.get(Router.Attrs.HandlerDef)
val modifiers = handler.map(_.modifiers).getOrElse(List.empty)

请注意,HandlerDef 请求属性仅在使用 Play 从 routes 文件生成的路由器时才存在。
当在代码中定义路由时,例如使用 Scala SIRD 或 Java RoutingDsl,不会添加此属性。在这种情况下,request.attrs.get(HandlerDef) 将在 Scala 中返回 None,在 Java 中返回 null。在创建过滤器时请牢记这一点。

§可注入的 Twirl 模板

Twirl 模板现在可以使用 @this 使用构造函数注释创建。构造函数注释意味着 Twirl 模板可以直接注入到模板中,并可以管理自己的依赖项,而不是控制器必须不仅管理自身依赖项,还要管理它必须渲染的模板的依赖项。

例如,假设一个模板依赖于一个组件TemplateRenderingComponent,而该组件没有被控制器使用。

首先使用@this语法为构造函数创建一个文件IndexTemplate.scala.html。注意,构造函数必须放在用于模板参数的@()语法之前,用于apply方法。

@this(trc: TemplateRenderingComponent)
@(item: Item)

@{trc.render(item)}

默认情况下,所有使用Play中@this语法的生成的Scala模板类都会自动用@javax.inject.Inject()进行注解。如果需要,可以在build.sbt中更改此行为。

// Add one or more annotation(s):
TwirlKeys.constructorAnnotations += "@java.lang.Deprecated()"

// Or completely replace the default one with your own annotation(s):
TwirlKeys.constructorAnnotations := Seq("@com.google.inject.Inject()")

现在通过在构造函数中注入模板来定义控制器。

Java

public class MyController extends Controller {

  private final views.html.indexTemplate template;

  @Inject
  public MyController(views.html.indexTemplate template) {
    this.template = template;
  }

  public Result index() {
    return ok(template.render());
  }

}

Scala

class MyController @Inject()(indexTemplate: views.html.IndexTemplate,
                              cc: ControllerComponents)
  extends AbstractController(cc) {

  def index = Action { implicit request =>
    Ok(indexTemplate())
  }
}

一旦模板及其依赖项被定义,控制器就可以将模板注入到控制器中,但控制器看不到TemplateRenderingComponent

§过滤器增强

Play现在提供了一组默认启用的过滤器,通过配置定义。这为新的Play应用程序提供了“默认安全”体验,并加强了现有Play应用程序的安全。

以下过滤器默认启用

此外,过滤器现在可以通过application.conf进行配置。要追加到默认列表,请使用+=

play.filters.enabled+=MyFilter

如果要专门禁用用于测试的过滤器,也可以从配置中进行操作。

play.filters.disabled+=MyFilter

请参见过滤器页面了解更多详细信息。

注意:如果您正在从不使用CSRF表单助手(如CSRF.formField)的现有项目迁移,那么您可能会在PUT和POST请求中看到“403 Forbidden”,这是来自CSRF过滤器的。要检查此行为,请将<logger name="play.filters.csrf" value="TRACE"/>添加到您的logback.xml中。同样,如果您在除localhost之外的其他地方运行Play应用程序,则必须配置AllowedHostsFilter以专门允许您连接的hostname/ip。

§gzip过滤器

如果您启用了gzip过滤器,您现在还可以通过application.conf控制哪些响应是gzip压缩的,哪些响应不是gzip压缩的(而不是编写您自己的Filters类)。

play.filters.gzip {

    contentType {

        # If non empty, then a response will only be compressed if its content type is in this list.
        whiteList = [ "text/*", "application/javascript", "application/json" ]

        # The black list is only used if the white list is empty.
        # Compress all responses except the ones whose content type is in this list.
        blackList = []
    }
}

请参见gzip过滤器页面了解更多详细信息。

§JWT Cookie

Play现在使用JSON Web Token (JWT)格式来处理会话和闪存Cookie。这允许使用标准化的签名Cookie数据格式、Cookie过期(使重放攻击更难)以及在签名Cookie方面具有更大的灵活性。

请参见ScalaJava页面了解更多详细信息。

§日志标记 API

SLF4J 标记支持已添加到 play.Loggerplay.api.Logger

在 Java API 中,它是 SLF4J Logger API 的直接移植。这很有用,但您可能会发现像 Godaddy Logger 这样的 SLF4J 包装器,以获得更丰富的日志记录体验。

在 Scala API 中,标记是通过 MarkerContext 特性添加的,该特性作为隐式参数添加到日志记录方法中,例如:

import play.api._
logger.info("some info message")(MarkerContext(someMarker))

这为在多个语句中传递隐式标记以进行日志记录打开了大门,这使得在不诉诸 MDC 的情况下更容易向日志记录添加上下文。例如,使用 Logstash Logback 编码器隐式转换链,请求信息可以自动编码到日志记录语句中

trait RequestMarkerContext {
  // Adding 'implicit request' enables implicit conversion chaining
  // See https://docs.scala-lang.org.cn/tutorials/FAQ/chaining-implicits.html
  implicit def requestHeaderToMarkerContext(implicit request: RequestHeader): MarkerContext = {
    import net.logstash.logback.marker.LogstashMarker
    import net.logstash.logback.marker.Markers._

    val requestMarkers: LogstashMarker = append("host", request.host)
      .and(append("path", request.path))

    MarkerContext(requestMarkers)
  }
}

然后在控制器中使用,并通过可能使用不同执行上下文的 Future 传递

def asyncIndex = Action.async { implicit request =>
  Future {
    methodInOtherExecutionContext() // implicit conversion here
  }(otherExecutionContext)
}

def methodInOtherExecutionContext()(implicit mc: MarkerContext): Result = {
  logger.debug("index: ") // same as above
  Ok("testing")
}

请注意,标记上下文对于“追踪弹”式日志记录也非常有用,在这种情况下,您希望在特定请求上进行日志记录,而无需显式更改日志级别。例如,您可以在满足特定条件时才添加标记

trait TracerMarker {
  import TracerMarker._

  implicit def requestHeaderToMarkerContext(implicit request: RequestHeader): MarkerContext = {
    val marker = org.slf4j.MarkerFactory.getDetachedMarker("dynamic") // base do-nothing marker...
    if (request.getQueryString("trace").nonEmpty) {
      marker.add(tracerMarker)
    }
    marker
  }
}

object TracerMarker {
  private val tracerMarker = org.slf4j.MarkerFactory.getMarker("TRACER")
}

class TracerBulletController @Inject() (cc: ControllerComponents) extends AbstractController(cc) with TracerMarker {
  private val logger = play.api.Logger("application")

  def index = Action { implicit request: Request[AnyContent] =>
    logger.trace("Only logged if queryString contains trace=true")

    Ok("hello world")
  }
}

然后使用以下 TurboFilter 在 logback.xml 中触发日志记录

<turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
  <Name>TRACER_FILTER</Name>
  <Marker>TRACER</Marker>
  <OnMatch>ACCEPT</OnMatch>
</turboFilter>

有关更多信息,请参阅 ScalaLoggingJavaLogging

有关在日志记录中使用标记的更多信息,请参阅 Logback 手册中的 TurboFilters基于标记的触发 部分。

§配置改进

在 Java API 中,我们已从 Lightbend 的 Config 库迁移到标准 Config 对象,而不是 play.Configuration。这使行为与标准配置行为一致,因为方法现在期望所有键都存在。有关迁移详细信息,请参阅 Java 配置迁移指南

在 Scala API 中,我们在 play.api.Configuration 类中引入了新方法,以简化 API 并允许加载自定义类型。您现在可以使用隐式 ConfigLoader 加载您想要的任何自定义类型。与 Config API 一样,新的 Configuration#get[T] 默认情况下期望键存在并返回类型为 T 的值,但还有一个 ConfigLoader[Option[T]] 允许 null 配置值。有关更多详细信息,请参阅 Scala 配置文档

§安全日志记录

已为 Play 中的安全相关操作添加了安全标记,并且失败的安全检查现在以 WARN 级别记录,并设置了安全标记。这确保开发人员始终知道特定请求失败的原因,这对于 Play 中默认启用安全过滤器非常重要。

安全标记还允许安全故障触发或过滤,与正常日志记录区分开来。例如,要禁用所有设置了 SECURITY 标记的日志记录,请将以下行添加到 logback.xml 文件中

<turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
    <Marker>SECURITY</Marker>
    <OnMatch>DENY</OnMatch>
</turboFilter>

此外,使用安全标记的日志事件还可以触发向安全信息和事件管理 (SIEM) 引擎发送消息以进行进一步处理。

§在 Java 中配置自定义日志记录框架

以前,如果您想 使用自定义日志记录框架,您必须使用 Scala 进行配置,即使您有 Java 项目。现在,您可以在 Java 和 Scala 中创建自定义 LoggerConfigurator。要在 Java 中创建 LoggerConfigurator,您需要实现给定的接口,例如,要配置 Log4J

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import org.slf4j.ILoggerFactory;
import play.Environment;
import play.LoggerConfigurator;
import play.Mode;
import play.api.PlayException;

import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.*;
import org.apache.logging.log4j.core.config.Configurator;

public class JavaLog4JLoggerConfigurator implements LoggerConfigurator {

    private ILoggerFactory factory;

    @Override
    public void init(File rootPath, Mode mode) {
        Map<String, String> properties = new HashMap<>();
        properties.put("application.home", rootPath.getAbsolutePath());

        String resourceName = "log4j2.xml";
        URL resourceUrl = this.getClass().getClassLoader().getResource(resourceName);
        configure(properties, Optional.ofNullable(resourceUrl));
    }

    @Override
    public void configure(Environment env) {
        Map<String, String> properties = LoggerConfigurator.generateProperties(env, ConfigFactory.empty(), Collections.emptyMap());
        URL resourceUrl = env.resource("log4j2.xml");
        configure(properties, Optional.ofNullable(resourceUrl));
    }

    @Override
    public void configure(Environment env, Config configuration, Map<String, String> optionalProperties) {
        // LoggerConfigurator.generateProperties enables play.logger.includeConfigProperties=true
        Map<String, String> properties = LoggerConfigurator.generateProperties(env, configuration, optionalProperties);
        URL resourceUrl = env.resource("log4j2.xml");
        configure(properties, Optional.ofNullable(resourceUrl));
    }

    @Override
    public void configure(Map<String, String> properties, Optional<URL> config) {
        try {
            LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
            loggerContext.setConfigLocation(config.get().toURI());

            factory = org.slf4j.impl.StaticLoggerBinder.getSingleton().getLoggerFactory();
        } catch (URISyntaxException ex) {
            throw new PlayException(
                "log4j2.xml resource was not found",
                "Could not parse the location for log4j2.xml resource",
                ex
            );
        }
    }

    @Override
    public ILoggerFactory loggerFactory() {
        return factory;
    }

    @Override
    public void shutdown() {
        LoggerContext loggerContext = (LoggerContext) LogManager.getContext();
        Configurator.shutdown(loggerContext);
    }
}

注意:此实现与 Scala 版本 LoggerConfigurator 完全兼容,甚至可以在必要时用于 Scala 项目,这意味着模块创建者可以提供 Java 或 Scala 实现的 LoggerConfigurator,它们可以在 Java 和 Scala 项目中使用。

§分离 Java 表单模块和 PlayMinimalJava 插件

Java 表单 功能已拆分为单独的模块。表单功能依赖于一些 Spring 模块和 Hibernate 验证器,因此,如果您没有使用表单,您可能希望删除 Java 表单模块以避免这些不必要的依赖项。

此模块由 PlayJava 插件自动包含,但可以使用 PlayMinimalJava 插件禁用

lazy val root = (project in file("."))
  .enablePlugins(PlayMinimalJava)

§Java 编译时组件

与 Scala 一样,Play 现在具有组件来启用 Java 编译时依赖注入。这些组件被创建为接口,您应该 implements 它们,并且它们提供默认实现。对于使用 运行时依赖注入 时可以注入的所有类型,都有组件。要使用编译时依赖注入创建应用程序,您只需要提供 play.ApplicationLoader 的实现,该实现使用 play.BuiltInComponents 的自定义实现,例如

import play.routing.Router;
import play.ApplicationLoader;
import play.BuiltInComponentsFromContext;
import play.filters.components.HttpFiltersComponents;

public class MyComponents extends BuiltInComponentsFromContext
        implements HttpFiltersComponents {

    public MyComponents(ApplicationLoader.Context context) {
        super(context);
    }

    @Override
    public Router router() {
        return Router.empty();
    }
}

play.ApplicationLoader

import play.ApplicationLoader;

public class MyApplicationLoader implements ApplicationLoader {

    @Override
    public Application load(Context context) {
        return new MyComponents(context).application();
    }

}

并按照 Java 编译时依赖注入文档 中的说明配置 MyApplicationLoader

§改进的表单处理 I18N 支持

MessagesApiLang 类用于 Play 中的国际化,并且是表单中显示错误消息所必需的。

过去,在 Play 中构建表单需要 多个步骤,并且从请求创建 Messages 实例在表单处理的上下文中没有讨论。

此外,当需要表单处理时,将 Messages 实例传递给所有模板片段很不方便,并且 Messages 隐式支持是通过控制器特征直接提供的。I18N API 已通过添加 MessagesProvider 特征、直接绑定到请求的隐式以及改进的表单文档进行了改进。

已添加 MessagesActionBuilder。此操作构建器提供了一个 MessagesRequest,它是一个 WrappedRequest,扩展了 MessagesProvider,只需要向模板提供一个隐式参数,并且您不需要使用 I18nSupport 扩展 Controller。这也很有用,因为要使用 CSRF 与表单一起使用,模板必须同时提供 Request(技术上是 RequestHeader)和 Messages 对象。

class FormController @Inject()(messagesAction: MessagesActionBuilder, components: ControllerComponents)
  extends AbstractController(components) {

  import play.api.data.Form
  import play.api.data.Forms._

  val userForm = Form(
    mapping(
      "name" -> text,
      "age" -> number
    )(UserData.apply)(UserData.unapply)
  )

  def index = messagesAction { implicit request: MessagesRequest[AnyContent] =>
    Ok(views.html.displayForm(userForm))
  }

  def post = ...
}

其中 displayForm.scala.html 定义为

@(userForm: Form[UserData])(implicit request: MessagesRequestHeader)

@import helper._

@helper.form(action = routes.FormController.post()) {
  @CSRF.formField                     @* <- takes a RequestHeader    *@
  @helper.inputText(userForm("name")) @* <- takes a MessagesProvider *@
  @helper.inputText(userForm("age"))  @* <- takes a MessagesProvider *@
}

有关更多信息,请参见 ScalaI18N

§测试支持

创建 MessagesApi 实例的支持已得到改进。现在,当您要创建 MessagesApi 实例时,您可以使用默认参数创建 DefaultMessagesApi()DefaultLangs()。如果您想从配置或其他来源指定测试消息,您可以传入这些值

val messagesApi: MessagesApi = {
    val env = new Environment(new File("."), this.getClass.getClassLoader, Mode.Dev)
    val config = Configuration.reference ++ Configuration.from(Map("play.i18n.langs" -> Seq("en", "fr", "fr-CH")))
    val langs = new DefaultLangsProvider(config).get
    new DefaultMessagesApi(testMessages, langs)
  }

§Future 超时和延迟支持

Play 对异步操作中 future 的支持已得到改进,使用 Futures 特征。

您可以使用 play.libs.concurrent.Futures 接口将 CompletionStage 包装在非阻塞超时中

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

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

或在 Scala API 中使用 play.api.libs.concurrent.Futures 特征

import play.api.libs.concurrent.Futures._

class MyController @Inject()(cc: ControllerComponents)(implicit futures: Futures) extends AbstractController(cc) {

  def index = Action.async {
    // withTimeout is an implicit type enrichment provided by importing Futures._
    intensiveComputation().withTimeout(1.seconds).map { i =>
      Ok("Got result: " + i)
    }.recover {
      case e: TimeoutException =>
        InternalServerError("timeout")
    }
  }
}

还有一个 delayed 方法,它只在指定延迟后执行 Future,其工作原理类似于超时。

有关更多信息,请参见 ScalaAsyncJavaAsync

§自定义 ExecutionContext 和线程池大小

此类定义了一个自定义执行上下文,它委托给一个akka.actor.ActorSystem。它对于默认执行上下文不应使用的情况非常有用,例如,如果正在使用数据库或阻塞 I/O。有关详细信息,请参阅线程池页面,但 Play 2.6.x 添加了一个CustomExecutionContext类,它处理底层 Akka 调度程序查找。

§使用预配置的自定义执行上下文的更新模板

Play 下载页面上所有使用阻塞 API(例如 Anorm、JPA)的 Play 示例模板都已更新为在适当的地方使用自定义执行上下文。例如,访问https://playframework.com/download#examples,可以看到https://github.com/playframework/play-java-jpa-example/中的JPAPersonRepository类接受一个DatabaseExecutionContext,它包装了所有数据库操作。

对于涉及 JDBC 连接池的线程池大小,您需要一个固定大小的线程池,该线程池的大小与连接池匹配,并使用线程池执行器。遵循HikariCP 的池大小页面中的建议,您应该将 JDBC 连接池配置为物理内核数量的两倍,加上磁盘主轴的数量。

这里使用的调度程序设置来自Akka 调度程序

# db connections = ((physical_core_count * 2) + effective_spindle_count)
fixedConnectionPool = 9

database.dispatcher {
  executor = "thread-pool-executor"
  throughput = 1
  thread-pool-executor {
    fixed-pool-size = ${fixedConnectionPool}
  }
}

§在 Scala 中定义自定义执行上下文

要定义自定义执行上下文,请使用调度程序名称子类化CustomExecutionContext

@Singleton
class DatabaseExecutionContext @Inject()(system: ActorSystem)
   extends CustomExecutionContext(system, "database.dispatcher")

然后将执行上下文作为隐式参数传入

class DatabaseService @Inject()(implicit executionContext: DatabaseExecutionContext) {
  ...
}

§在 Java 中定义自定义执行上下文

要定义自定义执行上下文,请使用调度程序名称子类化CustomExecutionContext

import akka.actor.ActorSystem;
import play.libs.concurrent.CustomExecutionContext;

public class DatabaseExecutionContext
        extends CustomExecutionContext {

    @javax.inject.Inject
    public DatabaseExecutionContext(ActorSystem actorSystem) {
        // uses a custom thread pool defined in application.conf
        super(actorSystem, "database.dispatcher");
    }
}

然后显式传入 JPA 上下文

public class JPAPersonRepository implements PersonRepository {

    private final JPAApi jpaApi;
    private final DatabaseExecutionContext executionContext;

    @Inject
    public JPAPersonRepository(JPAApi jpaApi, DatabaseExecutionContext executionContext) {
        this.jpaApi = jpaApi;
        this.executionContext = executionContext;
    }

    ...
}

§Play WSClient 改进

Play WSClient 有了重大改进。Play WSClient 现在是独立的play-ws实现的包装器,可以在 Play 之外使用。此外,参与play-ws的底层库已阴影化,因此其中使用的 Netty 实现不会与 Spark、Play 或任何其他使用不同版本的 Netty 的库冲突。

最后,如果存在缓存实现,现在支持 HTTP 缓存。使用 HTTP 缓存意味着可以节省对后端 REST 服务的重复请求,并且与弹性功能(如 stale-on-errorstale-while-revalidate)结合使用时尤其有用。

有关更多详细信息,请参阅 WsCacheWS 迁移指南

§Play JSON 改进

此版本的 JSON 库包含许多改进。

§序列化元组的能力

现在,元组可以通过 play-json 进行序列化,并且在隐式范围内存在 ReadsWrites 实现。元组被序列化为数组,因此 ("foo", 2, "bar") 将在 JSON 中呈现为 ["foo", 2, "bar"]

§Scala.js 支持

Play JSON 2.6.0 现在支持 Scala.js。您可以使用以下命令添加依赖项:

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

其中 version 是您要使用的版本。该库应该与 JVM 上的库一样有效,只是不支持 JVM 类型。

§用于自动 JSON 映射的自定义命名策略

可以自定义由 Json 宏(readswritesformat)生成的处理程序。因此,可以定义命名策略以根据需要映射 JSON 字段。

要使用自定义命名策略,您需要为 JsonConfigurationJsonNaming 定义隐式实例。

提供两种命名策略:默认策略,使用类属性的名称原样,以及 JsonNaming.SnakeCase 策略。

可以使用以下方法使用除默认策略之外的其他策略

import play.api.libs.json._

implicit val config = JsonConfiguration(SnakeCase)

implicit val userFormat: OFormat[PlayUser] = Json.format[PlayUser]

此外,可以通过提供 JsonNaming 实现来实现自定义命名策略。

§测试改进

在 2.6.x 中,已向 play.api.test 包中添加了一些实用程序类,以使使用依赖项注入组件的功能测试更容易。

§注入

许多功能测试直接通过隐式 app 使用注入器

"test" in new WithApplication() {
  val executionContext = app.injector.instanceOf[ExecutionContext]
  ...
}

现在,使用 Injecting 特性,您可以省略此操作

"test" in new WithApplication() with Injecting {
  val executionContext = inject[ExecutionContext]
  ...
}

§StubControllerComponents

StubControllerComponentsFactory 创建一个模拟的 ControllerComponents,可用于对控制器进行单元测试。

val controller = new MyController(stubControllerComponents())

§StubBodyParser

StubBodyParserFactory 创建一个模拟的 BodyParser,可用于对内容进行单元测试。

val stubParser = stubBodyParser(AnyContent("hello"))

§文件上传改进

上传文件使用 TemporaryFile API,该 API 依赖于将文件存储在临时文件系统中,如 ScalaFileUpload / JavaFileUpload 中所述,可以通过 ref 属性访问。

上传文件本质上是一项危险的操作,因为无限制的文件上传会导致文件系统填满。因此,TemporaryFile 的理念是,它只在完成时有效,应尽快将其移出临时文件系统。任何未移动的临时文件都将被删除。

在 2.5.x 中,当文件引用被垃圾回收时,使用 finalize 删除 TemporaryFile。但是,在 某些情况下,垃圾回收没有及时发生。后台清理已迁移到使用 FinalizableReferenceQueue 和 PhantomReferences,而不是使用 finalize

TemporaryFile 的 Java 和 Scala API 已重新设计,以便所有 TemporaryFile 引用都来自 TemporaryFileCreator 特性,并且可以根据需要交换实现。现在有一个 atomicMoveWithFallback 方法,如果可用,它将使用 StandardCopyOption.ATOMIC_MOVE

§TemporaryFileReaper

现在还有一个 play.api.libs.Files.TemporaryFileReaper,可以启用它使用 Akka 调度程序按计划删除临时文件,这与垃圾回收方法不同。

默认情况下,清理器处于禁用状态,可以通过 application.conf 启用。

play.temporaryFile {
  reaper {
    enabled = true
    initialDelay = "5 minutes"
    interval = "30 seconds"
    olderThan = "30 minutes"
  }
}

以上配置将使用“olderThan”属性删除超过 30 分钟的文件。它将在应用程序启动后五分钟启动清理器,并此后每 30 秒检查一次文件系统。清理器不知道任何现有的文件上传,因此如果系统配置不当,长时间的文件上传可能会遇到清理器。

下一步:迁移指南


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