文件

§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.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 表單。如果您沒有使用 自訂 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 validateisValid 方法

使用 進階驗證功能 時,現在可以傳遞包含驗證程序有時需要的有用資訊的 ValidationPayload 物件給 Java 的 validateisValid 方法。
要將此類型的 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);
    }

}

注意:不要將 ValidationPayloadConstraintValidatorContext 混淆:前者類別由 Play 提供,也是您在處理 Play 表單時日常工作中所使用的類別。後者類別由 Bean Validation 規範 定義,且僅在 Play 內部使用,只有一個例外:當您撰寫自己的自訂類別層級限制條件時,此類別就會出現,就像上述最後一個範例一樣,不過您無論如何都只要將它傳遞給 reportValidationStatus 方法即可。

§支援 Caffeine

Play 現在提供基於 Caffeine 的 CacheApi 實作。Caffeine 是建議 Play 使用者使用的快取實作。

要從 EhCache 移轉到 Caffeine,您必須從相依關係中移除 ehcache,並以 caffeine 取代它。若要自訂預設設定,您也需要更新 application.conf 中的組態,如文件所述。

閱讀 Java 快取 APIScala 快取 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 的混合,就像在現代網路應用程式中很常見的那樣,它會是更好的選項。

您可以在 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 中定義的內容。

下一步:遷移指南


在此文件中發現錯誤?此頁面的原始碼可以在 這裡 找到。閱讀 文件指南 後,請隨時提交拉取請求。有問題或建議要分享嗎?請前往 我們的社群論壇 與社群展開對話。