Play Framework 2.6 新特性全解析:从 Scala 2.12 到 JWT Cookie、请求属性与安全默认配置
后端Web框架【免费下载链接】playframeworkThe Community Maintained High Velocity Web Framework For Java and Scala.项目地址https://gitcode.com/gh_mirrors/pl/playframework点击查看免费下载本指南基于仓库中的 documentation/manual/releases/release26/Highlights26.md系统梳理 Play Framework 2.6 版本的核心新特性。Play 2.6 是 Play 历史上首个同时支持 Scala 2.12 与 2.11 的版本完成了去全局状态的架构改造引入了基于TypedMap的请求属性、路由修饰符、可注入的 Twirl 模板、默认启用的安全过滤器、JWT Cookie、SLF4J Marker 日志等一揽子能力。读完本文你将掌握 2.6 各关键特性的配置方式、API 用法与底层实现原理并了解这些特性与仓库源码的对应关系。一、版本背景首个交叉构建 Scala 2.12 / 2.11 的 Play 版本Play 2.6 是第一个同时面向 Scala 2.12 与 2.11 交叉构建的 Play 发布版。为支持两个 Scala 版本Play 更新了一大批依赖项包括 Akka、sbt 插件等。你可以在build.sbt中通过scalaVersion设置来选择目标 Scala 版本// Scala 2.12 scalaVersion : 2.12.20 // Scala 2.11 scalaVersion : 2.11.12从当前仓库结构也能直观看到这一交叉构建的工程实践core/play/src/main下同时存在 scala-2 与 scala-3 两个版本相关源码目录而dev-mode/play-docs-sbt-plugin、dev-mode/sbt-plugin等项目同样采用了scala-2.12与scala-3分目录的组织方式用于放置依赖具体 Scala 编译器版本的兼容代码。二、PlayService sbt 插件面向微服务的极简工程实验性从 Play 2.6.8 起Play 提供了PlayService插件——一个面向微服务的极简配置。它使用标准 Maven 目录布局而非传统 Play 布局不包含 Twirl 模板与 sbt-web 相关功能lazy val root (project in file(.)) .enablePlugins(PlayService) .enablePlugins(RoutesCompiler) // 将 routes 文件放在 src/main/resources 下或在使用 SIRD/RoutingDsl 时移除 .settings( scalaVersion : 2.12.20, libraryDependencies Seq( guice, // 不使用 Play 的 Guice loader 时可移除 akkaHttpServer, // 或使用 nettyServer 切换 Netty logback // 添加 Play 日志支持 ) )注意该插件被标记为实验性API 可能发生变化官方预期在 Play 2.7.0 中趋于稳定。三、无全局状态Global-State-Free应用2.6 最大的底层架构变化是Play 不再依赖全局状态。虽然你仍可通过play.api.Play.current/play.Play.application()访问全局应用实例但这些 API 已被标记为废弃deprecated为 Play 3.0 彻底移除全局状态做铺垫。如果希望彻底禁用全局应用访问可在application.conf中设置play.allowGlobalApplicationfalse设置后任何对Play.current的调用都会抛出异常。四、Akka HTTP 服务端后端与 HTTP/2实验性1. 默认后端切换为 Akka HTTPPlay 2.6 将 Akka-HTTP 显式配置项目使用 Netty。2. HTTP/2 支持实验性借助PlayAkkaHttp2Support模块Akka HTTP 服务器可启用 HTTP/2lazy val root (project in file(.)) .enablePlugins(PlayJava, PlayAkkaHttp2Support)该模块自动化了 HTTP/2 的大部分配置过程但默认不支持run命令。当前仓库中同样保留着 transport/server/play-pekko-http2-support 模块Play 后续版本迁移到 Pekko 后的对应实现可供参考其工程组织方式。五、请求属性Request Attributes用 TypedMap 承载类型安全的数据1. 核心概念Play 2.6 的请求对象开始包含attributes属性允许你在请求对象中存放额外信息。典型场景是过滤器在请求中写入一个属性后续在 Action 内读取。属性的底层存储是一个附加到每个请求上的TypedMap——一种保存类型安全键值对的不可变 Map。属性由 key 索引且 key 的类型即表明属性值的类型。从源码看TypedKey.scala 中的TypedKey[A]使用引用相等reference equality做比较因此每个新实例都是不同的 keydisplayName仅用于调试打印同名的 key 并不相等。它提供bindValue(value)等价于-运算符将 key 绑定到值生成TypedEntry并有asJava方法转换成 Java 版TypedKey。2. Java 用法// 创建一个用于存放 User 对象的 TypedKey class Attrs { public static final TypedKeyUser USER TypedKey.Usercreate(user); } // 从请求中获取 User 对象 User user req.attrs().get(Attrs.USER); // 将 User 对象写入请求 Request newReq req.addAttr(Attrs.USER, newUser);3. Scala 用法// 创建一个用于存放 User 对象的 TypedKey object Attrs { val User: TypedKey[User] TypedKey.applyUser } // 从请求中获取 User 对象 val user: User req.attrs(Attrs.User) // 将 User 对象写入请求 val newReq req.addAttr(Attrs.User, newUser)4. 与请求标签Request Tags的关系请求标签tags已被废弃应迁移到属性机制具体迁移说明见 Migration26 文档。六、路由修饰符Route Modifier Tags1. 基本语法routes 文件语法现在允许为每条路由添加修饰符modifiers以提供自定义行为。官方内置实现了一个修饰符——CSRF 过滤器使用的nocsrf。默认情况下下面这条路由不会应用 CSRF 过滤器 nocsrf # Dont CSRF protect this route POST /api/foo/bar ApiController.foobar符号后面可以跟任意多个以空白分隔的修饰符即你可以创建自己的修饰符。2. 在 HandlerDef 中读取修饰符修饰符通过HandlerDef请求属性暴露该属性还包含路由定义的其他元数据Java:import java.util.List; import play.routing.HandlerDef; import play.routing.Router; HandlerDef handler req.attrs().get(Router.Attrs.HANDLER_DEF); ListString 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)3. 使用限制HandlerDef请求属性仅在 Play 根据routes文件生成的路由器上存在。若路由是在代码中定义的如 Scala SIRD 或 JavaRoutingDsl该属性不会被添加此时request.attrs.get(HandlerDef)在 Scala 中返回None、Java 中返回null。编写过滤器时务必注意这一点。从源码层面看路由修饰符的解析实现在 RoutesFileParser.scala 与 RoutesModels.scala 中编译器定义modifiers解析器并将解析结果存入Route模型的modifiers字段Seq[Modifier]。CSRF 过滤器正是通过play.filters.csrf配置中routeModifiers.whiteList [nocsrf]见 reference.conf来识别带nocsrf修饰符的路由并跳过检查的。七、可注入的 Twirl 模板Injectable Twirl Templates1. this 构造器注入语法Twirl 模板现在支持使用this构造器注解。这意味着模板可以声明自己的依赖并由容器注入而不再需要控制器同时管理自身与模板的依赖。例如假设模板依赖一个控制器并不使用的组件TemplateRenderingComponent。先创建IndexTemplate.scala.html注意构造器必须放在模板参数()即apply方法的参数之前this(trc: TemplateRenderingComponent) (item: Item) {trc.render(item)}2. 默认构造器注解与自定义默认情况下Play 中所有用this语法生成的 Scala 模板类都会自动标注javax.inject.Inject()。如需修改可在build.sbt中配置// 追加一个或多个注解 TwirlKeys.constructorAnnotations java.lang.Deprecated() // 或用你自己的注解完全替换默认注解 TwirlKeys.constructorAnnotations : Seq(com.google.inject.Inject())3. 在控制器中注入模板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。八、过滤器增强安全默认配置Secure by Default1. 默认启用的过滤器Play 2.6 默认启用一组过滤器通过配置定义为新建应用提供开箱即安全的体验同时收紧了既有应用的安全性。默认启用以下三个过滤器过滤器用途相关文档play.filters.csrf.CSRFFilter防止 CSRF 攻击CspFilter 相关安全过滤文档、Scala 指南play.filters.headers.SecurityHeadersFilter防止 XSS 与 frame origin 攻击SecurityHeadersplay.filters.hosts.AllowedHostsFilter防止 DNS rebinding 攻击AllowedHostsFilter从源码看这三个过滤器的默认启用配置位于 web/play-filters-helpers/src/main/resources/reference.confplay.filters { # Default list of enabled filters, configured by play.api.http.EnabledFilters enabled play.filters.csrf.CSRFFilter enabled play.filters.headers.SecurityHeadersFilter enabled play.filters.hosts.AllowedHostsFilter ... }同时该文件还声明了对应的模块CSRFModule、SecurityHeadersModule、AllowedHostsModule等并给出了各过滤器的详细默认配置如 CSRF 的 token 名csrfToken、protectHeaders默认检查带 Cookie/Authorization 的请求、SecurityHeaders 的frameOptions DENY、AllowedHosts 默认allowed [localhost, .local, 127.0.0.1]等可作为参数取值与默认值的一手参考。2. 通过配置追加 / 禁用过滤器过滤器现在可以在application.conf中配置。追加到默认列表使用play.filters.enabledMyFilter在测试中若需禁用某个过滤器也可以从配置中完成play.filters.disabledMyFilter注意如果是从未使用CSRF.formField等 CSRF 表单辅助方法的老项目迁移可能因 CSRF 过滤器在 PUT/POST 请求上看到 403 Forbidden。排查时可在logback.xml添加logger nameplay.filters.csrf valueTRACE/检查行为。另外如果 Play 应用运行在 localhost 之外的主机上必须配置 AllowedHostsFilter 显式放行你连接的 hostname/IP。3. gzip 过滤器按 Content-Type 控制压缩gzip 过滤器启用后现在可以通过application.conf按响应内容类型控制哪些响应被 gzip 压缩而无需编写自定义Filters类play.filters.gzip { contentType { # 若非空则仅当响应的 content type 在此列表中时才压缩 whiteList [ text/*, application/javascript, application/json ] # black list 仅在 white list 为空时使用 # 压缩所有响应除了 content type 在此列表中的 blackList [] } }更多细节见 GzipEncoding 文档。仓库的reference.conf中 gzip 还提供bufferSize 8k、chunkedThreshold 100k、compressionLevel -1、threshold 0等默认值。九、JWT Cookies标准化的 Session / Flash Cookie 格式Play 2.6 开始使用 JSON Web Token 或 JavaSessionFlash。十、日志 Marker API 与安全日志1. MarkerContext 隐式参数SLF4J Marker 支持已被加入play.Logger与play.api.Logger。Java API 直接移植了 SLF4J Logger API。Scala API 则通过MarkerContexttrait 将 marker 作为日志方法的隐式参数传入import play.api._ logger.info(some info message)(MarkerContext(someMarker))从源码看play/api/Logger.scala 中的trace/debug/info/warn/error等方法的签名均带(implicit mc: MarkerContext)印证了该设计。隐式 marker 可以在多条日志语句间传递让为日志添加上下文变得非常容易无需使用 MDC。例如配合 Logstash Logback Encoder 和隐式转换链可以自动把请求信息编码进日志语句示例见 ScalaLoggingSpec.scala并在控制器中携带它穿过可能使用不同执行上下文的Future。MarkerContext 也特别适合曳光弹式tracer bullet日志只想在满足特定条件时记录某次请求而不显式改变日志级别。例如仅当条件满足时添加 marker然后在logback.xml中配置如下 TurboFilter 触发日志turboFilter classchqos.logback.classic.turbo.MarkerFilter NameTRACER_FILTER/Name MarkerTRACER/Marker OnMatchACCEPT/OnMatch /turboFilter2. SECURITY 标记与 WARN 级安全日志Play 为安全相关操作添加了SECURITYmarker安全校验失败现在以 WARN 级别记录并携带该 marker确保开发者始终知道请求失败的原因——这在安全过滤器默认开启的背景下尤为重要。SECURITY marker 也允许将安全失败日志与普通日志区分触发或过滤。例如在logback.xml中禁用所有带 SECURITY marker 的日志turboFilter classchqos.logback.classic.turbo.MarkerFilter MarkerSECURITY/Marker OnMatchDENY/OnMatch /turboFilter此外带 SECURITY marker 的日志事件还可触发消息推送至安全信息与事件管理SIEM引擎做进一步处理。十一、配置改进Java 用标准 Config、Scala 用 ConfigLoader1. Java APIJava API 迁移到 Lightbend Config 库的标准Config对象替代play.Configuration行为与标准 config 保持一致——方法现在默认要求所有 key 都存在。迁移细节见 JavaConfigMigration26。2. Scala APIScala API 的play.api.Configuration新增了简化 API 与自定义类型加载能力现在可以使用隐式ConfigLoader加载任意自定义类型。与ConfigAPI 类似新的Configuration#get[T]默认要求 key 存在并返回T类型值同时提供了允许 null 配置值的ConfigLoader[Option[T]]。十二、Java 自定义日志框架LoggerConfigurator 接口以前即使你的项目是 Java 项目使用自定义日志框架也只能用 Scala 配置。现在可以用 Java 或 Scala 实现自定义的LoggerConfigurator。下面是一个用 Java 配置 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) { MapString, 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) { MapString, 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, MapString, String optionalProperties) { // LoggerConfigurator.generateProperties 会启用 play.logger.includeConfigPropertiestrue MapString, String properties LoggerConfigurator.generateProperties(env, configuration, optionalProperties); URL resourceUrl env.resource(log4j2.xml); configure(properties, Optional.ofNullable(resourceUrl)); } Override public void configure(MapString, String properties, OptionalURL 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 Forms 模块与 PlayMinimalJava 插件Java forms 功能被拆分到独立模块中。forms 依赖若干 Spring 模块与 Hibernate validator如果你不使用 forms可以移除该模块以规避这些不必要的依赖。PlayJava插件会自动包含该模块但可以通过改用PlayMinimalJava插件禁用它lazy val root (project in file(.)) .enablePlugins(PlayMinimalJava)当前仓库中的 web/play-java-forms 与 web/play-joda-forms 即体现了 forms 模块独立化的工程形态。十四、Java 编译期组件Compile Time Components与 Scala 一样Play 现在提供组件以支持 Java 编译期依赖注入。这些组件被定义为需要implements的接口并提供默认实现覆盖了运行时依赖注入中可注入的所有类型。使用编译期 DI 创建应用只需提供一个使用自定义play.BuiltInComponents实现的play.ApplicationLoader例如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.ApplicationLoaderimport play.ApplicationLoader; public class MyApplicationLoader implements ApplicationLoader { Override public Application load(Context context) { return new MyComponents(context).application(); } }然后按 Java 编译期依赖注入文档 中应用入口一节的说明配置MyApplicationLoader。仓库中 BuiltInComponentsFromContext.java 与play-filters-helpers下的HttpFiltersComponents等组件接口正是该能力的落地实现。十五、表单 I18N 支持改进与 MessagesActionBuilderMessagesApi与Lang是 Play 国际化的核心类表单错误消息的展示依赖它们。2.6 细化了 I18N API新增MessagesProvidertrait、与请求直接绑定的隐式值并改进了 forms 文档。新增的MessagesActionBuilder提供MessagesRequest——一个扩展WrappedRequest并实现MessagesProvider的请求包装类。这样模板只需要一个隐式参数且无需让控制器混入I18nSupport。更重要的是要在表单上使用 CSRF模板中必须同时有Request严格说是RequestHeader与Messages对象而MessagesRequest恰好同时满足两者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 * - 需要 RequestHeader * helper.inputText(userForm(name)) * - 需要 MessagesProvider * helper.inputText(userForm(age)) * - 需要 MessagesProvider * }测试支持创建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 超时与延迟支持Futures traitPlay 对异步操作中 Future 的支持得到改进通过Futurestrait 提供非阻塞超时与延迟能力。Java 中可使用play.libs.concurrent.Futures接口给CompletionStage包裹非阻塞超时class MyClass { Inject public MyClass(Futures futures) { this.futures futures; } CompletionStageDouble callWithOneSecondTimeout() { return futures.timeout(computePIAsynchronously(), Duration.ofSeconds(1)); } }Scala 中使用play.api.libs.concurrent.Futurestraitimport play.api.libs.concurrent.Futures._ class MyController Inject()(cc: ControllerComponents)(implicit futures: Futures) extends AbstractController(cc) { def index Action.async { // withTimeout 是导入 Futures._ 后提供的隐式类型增强 intensiveComputation().withTimeout(1.seconds).map { i Ok(Got result: i) }.recover { case e: TimeoutException InternalServerError(timeout) } } }另有delayed方法只在指定延迟后执行Future与 timeout 工作机制类似。从源码看Futures.scalaDefaultFutures基于ActorSystem实现timeout通过pekko.pattern.after构造一个超时失败 Future再与业务 Future 用Future.firstCompletedOf竞争delayed则直接用after延后执行。值得注意的是超时并不等于取消——即使超时发生原 Future 仍会继续完成只是其结果不再被返回。Futures对象还提供了FutureOps隐式增强withTimeout/withDelay与actorSystemToFutures隐式转换便于直接在Future上调用。十七、CustomExecutionContext 与线程池调优CustomExecutionContext定义了一个委托给akka.actor.ActorSystem的自定义执行上下文。当不应使用默认执行上下文时例如访问数据库或做阻塞 I/O非常有用。完整说明见 ThreadPools 文档。Play 2.6.x 新增的CustomExecutionContext负责底层 Akka dispatcher 查找。所有使用阻塞 API如 Anorm、JPA的官方示例模板都已按需改用自定义执行上下文。关于线程池大小涉及 JDBC 连接池时建议使用与连接池匹配的固定线程池大小thread pool executor。参考 HikariCP 的池大小建议JDBC 连接池应配置为物理核心数×2 加磁盘 spindle 数# 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继承CustomExecutionContext并传入 dispatcher 名称Singleton class DatabaseExecutionContext Inject()(system: ActorSystem) extends CustomExecutionContext(system, database.dispatcher)然后将执行上下文作为隐式参数传入class DatabaseService Inject()(implicit executionContext: DatabaseExecutionContext) { ... }Java 中定义 CustomExecutionContext同样继承play.libs.concurrent.CustomExecutionContext并传入 dispatcher 名称import akka.actor.ActorSystem; import play.libs.concurrent.CustomExecutionContext; public class DatabaseExecutionContext extends CustomExecutionContext { javax.inject.Inject public DatabaseExecutionContext(ActorSystem actorSystem) { // 使用 application.conf 中定义的自定义线程池 super(actorSystem, database.dispatcher); } }然后显式传入该执行上下文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; } ... }仓库中的 CustomExecutionContext.scala 与 CustomExecutionContext.java 即为该类的 Scala/Java 双实现。十八、WSClient 改进独立 play-ws、Netty 遮蔽与 HTTP 缓存PlayWSClient有大量改进它现在包装了可脱离 Play 独立使用的 play-ws 实现play-ws 涉及的底层库经过了遮蔽shading处理使其内部使用的 Netty 不会与 Spark、Play 或其他使用不同 Netty 版本的库冲突。此外如果存在缓存实现现在支持 HTTP Caching 与 WS 迁移指南。十九、Play JSON 改进本版本 JSON 库包含大量改进。1. 元组序列化play-json 现在可以序列化元组隐式作用域内提供对应的Reads/Writes实现。元组被序列化为数组因此(foo, 2, bar)渲染为[foo, 2, bar]。2. Scala.js 支持Play JSON 2.6.0 支持 Scala.js依赖声明方式libraryDependencies com.typesafe.play %%% play-json % version其中version为你希望使用的版本。该库在 Scala.js 上与 JVM 行为一致仅不支持 JVM 专有类型。3. 自定义命名策略可以定制Json宏reads、writes、format生成的处理器通过定义命名策略将 JSON 字段按需映射。使用自定义命名策略需要定义JsonConfiguration与JsonNaming的隐式实例。内置两种策略默认策略原样使用类属性名与JsonNaming.SnakeCase。使用非默认策略的示例import play.api.libs.json._ implicit val config JsonConfiguration(SnakeCase) implicit val userFormat: OFormat[PlayUser] Json.format[PlayUser]此外也可以提供自定义JsonNaming实现来定义自己的命名策略。二十、测试改进Injecting、StubControllerComponents 与 StubBodyParser2.6.x 在play.api.test包中新增了一些工具类让依赖注入组件的功能测试更简单。1. Injecting许多功能测试通过隐式app直接使用 injectortest in new WithApplication() { val executionContext app.injector.instanceOf[ExecutionContext] ... }现在借助Injectingtrait 可以省略这层间接test in new WithApplication() with Injecting { val executionContext inject[ExecutionContext] ... }2. StubControllerComponentsStubControllerComponentsFactory创建一个可对控制器做单元测试的桩ControllerComponentsval controller new MyController(stubControllerComponents())3. StubBodyParserStubBodyParserFactory创建用于单元测试内容解析的桩BodyParserval stubParser stubBodyParser(AnyContent(hello))二十一、文件上传改进TemporaryFile 与 TemporaryFileReaper1. 背景与 2.6 的重构文件上传使用TemporaryFileAPI依赖把文件存入临时文件系统详见 ScalaFileUpload / JavaFileUpload通过ref属性访问。文件上传本质上是危险操作——无界上传可能塞满文件系统因此TemporaryFile的设计理念是它只在完成阶段存在于作用域内应尽快移出临时文件系统未被移动的临时文件会被删除。2.5.x 中临时文件在文件引用被 GC 时通过finalize删除但某些条件下 GC 无法及时触发。2.6 的后台清理改用 Guava 的FinalizableReferenceQueue与 PhantomReference不再使用finalize。Java 与 Scala 的TemporaryFileAPI 也做了重构所有TemporaryFile引用都来自TemporaryFileCreatortrait实现可按需替换并新增atomicMoveWithFallback方法——可用时优先使用StandardCopyOption.ATOMIC_MOVE。2. TemporaryFileReaper 定时清理新增的play.api.libs.Files.TemporaryFileReaper可以通过 Akka scheduler 按计划删除临时文件独立于 GC 机制。reaper 默认禁用通过application.conf启用play.temporaryFile { reaper { enabled true initialDelay 5 minutes interval 30 seconds olderThan 30 minutes } }上述配置会删除超过 30 分钟olderThan的文件应用启动 5 分钟后启动 reaper此后每 30 秒检查一次文件系统。注意 reaper 对进行中的上传一无所知如果系统配置不当长时间上传可能撞上 reaper。从源码看Files.scala 中的TemporaryFileReaperConfiguration从play.temporaryFile.reaper.enabled/olderThan/initialDelay/interval四个配置键读取参数并通过DefaultTemporaryFileReaper结合ActorSystem调度实现定时清理。二十二、从 2.6 回望新特性在仓库中的印证本仓库正是 Play Framework 的开源实现后续版本已迁移到 Pekko 生态如org.apache.pekko.actor.ActorSystem出现在 Futures.scala 与 CustomExecutionContext.scala 中但 2.6 引入的核心设计在今天依然延续请求属性TypedMap/TypedKeyTypedKey.scala路由修饰符解析RoutesFileParser.scala默认安全过滤器配置web/play-filters-helpers/src/main/resources/reference.conf日志 MarkerContextplay/api/Logger.scala编译期组件BuiltInComponentsFromContext.java临时文件清理play/api/libs/Files.scala若要迁移到更高版本可继续阅读本仓库 documentation/manual/releases 下的后续版本 Highlights 与 Migration 文档其中也包含 Migration26 的完整迁移指导。赞分享后端Web框架【免费下载链接】playframeworkThe Community Maintained High Velocity Web Framework For Java and Scala.项目地址https://gitcode.com/gh_mirrors/pl/playframework点击查看免费下载相关推荐Play Framework Session Cookie 配置完全指南JWT 编码、超时机制与安全参数详解Play Framework Session Cookie 配置完全指南JWT 编码、超时机制与安全参数详解 Session Cookie 是 Play Fr后端Web框架Data-Science-For-Beginners 实战用 Pandas 完成 COVID-19 传播建模与医学论文共现分析Data Science For Beginners 实战用 Pandas 完成 COVID 19 传播建模与医学论文共现分析 本篇技术指南基于 Data S后端Web框架Play Framework Scala 配置 API 指南Configuration 与 ConfigLoader 的类型安全配置读取Play Framework Scala 配置 API 指南Configuration 与 ConfigLoader 的类型安全配置读取 在 Play Fra后端Web框架创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

RK3588边缘AI实战:GStreamer硬解RTSP流与NPU推理融合管道搭建

RK3588边缘AI实战:GStreamer硬解RTSP流与NPU推理融合管道搭建

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/24 4:21:05 阅读更多 →
Product Hunt 每日热榜 | 2026-09-21

Product Hunt 每日热榜 | 2026-09-21

1. Mycel 标语:带来一个以前的成果。Mycel将为所有未来的成果起草。 介绍:Mycel 管理着你服务型企业的整个运作——包括客户、交付物、审批和发票。每个项目都在一个临时的沙箱中进行,该沙箱不会保存任何凭证,直到你审批后才能将…

2026/9/24 4:21:05 阅读更多 →
UFS Link Startup全解析:从链路启动到高速模式切换与故障排查

UFS Link Startup全解析:从链路启动到高速模式切换与故障排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/24 4:21:05 阅读更多 →

最新新闻

重试与退避:失败会反复发生,重试要有边界

重试与退避:失败会反复发生,重试要有边界

调远程接口的时候,一次就成功的请求其实是少数。网络抖动、下游短暂过载、锁冲突、连接被重置,这些都属于"再发一次可能就好了"的故障。工程上应对它们的常规手段是重试,但重试很容易写成隐患:没有上限的重试会把局部故…

2026/9/24 4:58:34 阅读更多 →
华为B610-4E光猫刷机指南:补全Shell与升级050固件实战

华为B610-4E光猫刷机指南:补全Shell与升级050固件实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/24 4:58:34 阅读更多 →
ESP32-S3内置USB-JTAG:一根线搞定下载、日志与调试

ESP32-S3内置USB-JTAG:一根线搞定下载、日志与调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/24 4:58:34 阅读更多 →
RK3588+OpenHarmony 4.0部署YOLOv8:从环境搭建到NPU推理实战

RK3588+OpenHarmony 4.0部署YOLOv8:从环境搭建到NPU推理实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/24 4:58:34 阅读更多 →
STM32F407+LAN8720跑EtherCAT主站:SOEM移植全流程与踩坑指南

STM32F407+LAN8720跑EtherCAT主站:SOEM移植全流程与踩坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/24 4:57:33 阅读更多 →
微信小程序|案例 3.3 生命周期函数完整学习

微信小程序|案例 3.3 生命周期函数完整学习

一、什么是生命周期 生命周期就是程序/页面从创建、显示、隐藏,到销毁整个完整过程,在不同阶段框架会自动调用对应的回调函数,我们就可以在回调里面写业务代码。 分为两大类: 应用生命周期:控制整个小程序,…

2026/9/24 4:57:33 阅读更多 →

日新闻

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:19 阅读更多 →
单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:19 阅读更多 →
C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/24 0:00:19 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/23 4:55:02 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/23 4:49:06 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/23 9:53:41 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/23 9:53:40 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/23 9:53:40 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/23 9:53:40 阅读更多 →