§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 System 關閉期間以順序執行這些任務。
協調關閉在內部處理 Play 2.7 Play 的生命週期,且可注入 CoordinatedShutdown
的執行個體。協調關閉提供細緻的階段 - 組織成 有向非循環圖 (DAG) - 您可以在其中註冊任務,而不用像 Play 的應用程式生命週期那樣只有一個階段。例如,您可以新增任務在伺服器繫結之前或之後執行,或在所有當前要求完成之後執行。此外,您將能與 Akka Cluster 更好地整合。
您可以在 Play 手冊的 協調關閉 新區段中找到更多詳細資訊,或查看 Akka 在 協調關閉 上的參考文件。
§Guice 已升級至 4.2.2
Guice 是 Play 使用的預設依賴注入架構,已從 4.1.0 升級至 4.2.2。請查看 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 表單。如果您沒有使用 自訂 multipart 檔案部分主體剖析器,您只需將 TemporaryFile
類型的 FilePart
新增至您的表單
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");
注意:如果您使用 自訂 multipart 檔案部分主體剖析器,您只需將
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 的 Payloads validate
和 isValid
方法
使用 進階驗證功能 時,現在可以傳遞包含驗證程序有時需要的有用資訊的 ValidationPayload
物件給 Java 的 validate
或 isValid
方法。
要將此類型的 payload 傳遞給 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
)將 payload 傳遞給 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 隨機數和雜湊。
先前預設啟用 CSP 並將其設定為 default-src 'self'
的設定太過嚴格,且會干擾外掛程式。CSP 過濾器預設不會啟用,而且 SecurityHeaders 過濾器 中的 contentSecurityPolicy
現在已過時,並預設設定為 null
。
CSP 過濾器預設使用 Google 的 嚴格 CSP 政策,這是一種基於隨機數的政策。建議將此作為起點,並使用隨附的 CSPReport 主體剖析器和動作來記錄 CSP 違規,然後在製作環境中強制執行 CSP。
§HikariCP 已升級
HikariCP 已更新至其最新主要版本。請參閱 遷移指南 以查看已變更的內容。
§適用於 Java 的 Play WS curl
過濾器
Play WS 讓您可以建立 play.libs.ws.WSRequestFilter
來檢查或豐富所發出的請求。Play 提供「以 curl
記錄」過濾器,但 Java 開發人員缺少此功能。現在您可以撰寫類似以下內容:
ws.url("https://play.dev.org.tw")
.setRequestFilter(new AhcCurlRequestLogger())
.addHeader("My-Header", "Header value")
.get();
然後會印出以下記錄
curl \
--verbose \
--request GET \
--header 'My-Header: Header Value' \
'https://play.dev.org.tw'
如果您想單獨重製請求,同時變更 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 中,沒有方便的方法可以回傳串流結果,而且不用 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 的混合,就像在現代網路應用程式中很常見的那樣,它會是更好的選項。
您可以在 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
中定義的內容。
下一步:遷移指南
在此文件中發現錯誤?此頁面的原始碼可以在 這裡 找到。閱讀 文件指南 後,請隨時提交拉取請求。有問題或建議要分享嗎?請前往 我們的社群論壇 與社群展開對話。