§Java Http.Context
變更
play.mvc.Http.Context
是 Java HTTP 和 MVC API 的重要部分,但它並非這些 API 應如何運作的良好抽象。它有一些可以更好地建模的概念,或者在像 Play 這樣的多執行緒架構中,實作細節複雜到難以測試和推理。例如,Http.Context
使用執行緒本機來擷取和存取目前的請求,但它給人的印象是目前的請求可以從任何地方存取,如果你正在使用 Actor 或自訂執行緒池,這並非事實。
關於 API 建模,有一些重複的概念(例如 play.mvc.Result
與 play.mvc.Http.Response
),而有些方法看起來格格不入(例如 play.mvc.Http.Context.id
,而不是使用 play.mvc.Http.RequestHeader.id
)。有鑑於此,已對 Http.Context
進行多項變更,而構想是要遠離它。然後我們提供新的 API,未來在測試、推理和維護方面會更簡單。
由於 play.mvc.Http.Context
是現有 API 的核心部分,因此棄用它會影響依賴它的多個地方,例如 play.mvc.Controller
。此頁面記載這些變更和如何遷移,但你也可以查看每個方法的已棄用 Javadoc。
§Http.Context.current()
和 Http.Context.request()
已棄用
這表示直接依賴這兩個方法的其他方法也已棄用
play.mvc.Controller.ctx()
play.mvc.Controller.request()
在 Play 2.7 之前,使用 Play 搭配 Java 時,存取 Http.Request
的唯一方式是 Http.Context.current()
,而 Controller.request()
方法會在內部使用它。Http.Context.current()
的問題在於它是使用執行緒區域變數實作的,這使得測試、與其他地方所做的變更保持同步,以及在其他執行緒存取要求變得更加困難。
使用 Play 2.7,你現在可以透過將目前的要求新增為路由和動作的參數,來存取目前的要求。
例如,路由檔案包含
GET / controllers.HomeController.index(request: Request)
以及對應的動作方法
import play.mvc.*;
public class HomeController extends Controller {
public Result index(Http.Request request) {
return ok("Hello, your request path " + request.path());
}
}
Play 會自動偵測類型為 Request
的路由參數(這是 play.mvc.Http.Request
的匯入),並將實際的要求傳遞到對應的動作方法的參數。
注意:雖然不太可能,但你可能會有一個自訂的
QueryStringBindable
或PathBindable
,其名稱為Request
。如果是這樣,它現在會與 Play 偵測要求參數發生衝突。
因此,您應該使用Request
類型的完全限定名稱,例如。GET / controllers.HomeController.index(myRequest: com.mycompany.Request)
如果您在控制器以外的其他地方使用 Http.Context.current()
,則現在必須透過方法參數將所需資料傳遞到這些地方。請看這個範例,它會檢查目前要求的遠端位址是否在黑名單中
§之前
import play.mvc.Http;
public class SecurityHelper {
public static boolean isBlacklisted() {
String remoteAddress = Http.Context.current().request().remoteAddress();
return blacklist.contains(remoteAddress);
}
}
對應的控制器
import play.mvc.*;
public class HomeController extends Controller {
public Result index() {
if (SecurityHelper.isBlacklisted()) {
return badRequest();
}
return ok("Hello, your request path " + request().path());
}
}
§之後
public class SecurityHelper {
public static boolean isBlacklisted(String remoteAddress) {
return blacklist.contains(remoteAddress);
}
}
對應的控制器
import play.mvc.*;
public class HomeController extends Controller {
public Result index(Http.Request request) {
if (SecurityHelper.isBlacklisted(request.remoteAddress())) {
return badRequest();
}
return ok("Hello, your request path " + request.path());
}
}
§Security.Authenticated
變更
若要保護動作以防止未經驗證的存取,您可以使用 @Security.Authenticated
。
§之前
import play.mvc.Http;
import play.mvc.Result;
import play.mvc.Security;
public class Secured extends Security.Authenticator {
@Override
public String getUsername(Http.Context ctx) {
return ctx.session().get("id");
}
@Override
public Result onUnauthorized(Http.Context ctx) {
ctx.flash().put("danger", "You need to login before access the application.");
return redirect(controllers.routes.HomeController.login());
}
}
§之後
import play.mvc.Http;
import play.mvc.Result;
import play.mvc.Security;
import java.util.Optional;
public class Secured extends Security.Authenticator {
@Override
public Optional getUsername(Http.Request req) {
return req.session().getOptional("id");
}
@Override
public Result onUnauthorized(Http.Request req) {
return redirect(controllers.routes.HomeController.login()).
flashing("danger", "You need to login before access the application.");
}
}
以及對應的動作方法
@Security.Authenticated(Secured.class)
public Result index(Http.Request request) {
return ok(views.html.index.render(request));
}
§Action.call(Context)
已棄用
如果您正在使用 動作組合,則必須更新動作以避免使用 Http.Context
。
§之前
import play.mvc.Action;
import play.mvc.Http;
import play.mvc.Result;
import java.util.concurrent.CompletionStage;
public class MyAction extends Action.Simple {
public CompletionStage<Result> call(Http.Context ctx) {
return delegate.call(ctx);
}
}
§之後
import play.mvc.Action;
import play.mvc.Http;
import play.mvc.Result;
import java.util.concurrent.CompletionStage;
public class MyAction extends Action.Simple {
public CompletionStage<Result> call(Http.Request req) {
return delegate.call(req);
}
}
§Http.Context.response()
和 Http.Response
類別已棄用
這表示也棄用了其他直接依賴這些方法的方法
play.mvc.Controller.response()
Http.Response
已棄用,以及其他存取它的方法。它主要用於新增標頭和 Cookie,但這些已經在 play.mvc.Result
中提供,因此 API 有點混淆。對於 Play 2.7,您應該將程式碼移轉為像
§之前
import play.mvc.Http;
import play.mvc.Result;
import play.mvc.Controller;
public class FooController extends Controller {
// This uses the deprecated response() APIs
public Result index() {
response().setHeader("Header", "Value");
response().setCookie(Http.Cookie.builder("Cookie", "cookie value").build());
response().discardCookie("CookieName");
return ok("Hello World");
}
}
§之後
上述程式碼應該寫成
import play.mvc.Http;
import play.mvc.Result;
import play.mvc.Controller;
public class FooController extends Controller {
// This uses the deprecated response() APIs
public Result index() {
return ok("Hello World")
.withHeader("Header", "value")
.withCookies(Http.Cookie.builder("Cookie", "cookie value").build())
.discardCookie("CookieName");
}
}
如果您有依賴於 Http.Context.response
的動作組合,也可以將它改寫成像以下程式碼
§之前
import play.mvc.Action;
import play.mvc.Http;
import play.mvc.Result;
import java.util.concurrent.CompletionStage;
public class MyAction extends Action.Simple {
@Override
public CompletionStage<Result> call(Http.Context ctx) {
ctx.response().setHeader("Name", "Value");
return delegate.call(ctx);
}
}
§之後
上述程式碼應該寫成
import play.mvc.Action;
import play.mvc.Http;
import play.mvc.Result;
import java.util.concurrent.CompletionStage;
public class MyAction extends Action.Simple {
@Override
public CompletionStage<Result> call(Http.Request req) {
return delegate.call(req)
.thenApply(result -> result.withHeader("Name", "Value"));
}
}
§Http.Context
中的 Lang 和 Messages 方法已棄用
下列方法已棄用
Http.Context.lang()
Http.Context.changeLang(Lang lang)
Http.Context.changeLang(String code)
Http.Context.clearLang()
Http.Context.setTransientLang(Lang lang)
Http.Context.setTransientLang(String code)
Http.Context.clearTransientLang()
Http.Context.messages()
這表示也棄用了其他直接依賴這些方法的方法
play.mvc.Controller.lang()
play.mvc.Controller.changeLang(Lang lang)
play.mvc.Controller.changeLang(String code)
play.mvc.Controller.clearLang()
現在變更語言的新方法是注入 play.i18n.MessagesApi
的執行個體,並呼叫對應的 play.mvc.Result
方法。例如
§之前
import play.mvc.Result;
import play.mvc.Results;
import play.mvc.Controller;
import play.i18n.MessagesApi;
public class FooController extends Controller {
public Result action() {
changeLang(Lang.forCode("es"));
return Results.ok("Hello");
}
}
§之後
import javax.inject.Inject;
import play.mvc.Result;
import play.mvc.Results;
import play.mvc.Controller;
import play.i18n.MessagesApi;
public class FooController extends Controller {
private final MessagesApi messagesApi;
@Inject
public FooController(MessagesApi messagesApi) {
this.messagesApi = messagesApi;
}
public Result action() {
return Results.ok("Hello").withLang(Lang.forCode("es"), messagesApi);
}
}
如果您使用 changeLang
來變更用於呈現範本的 Lang
,您現在應該將 Messages
本身作為參數傳遞。這將使範本更清晰且更容易閱讀。例如,在動作方法中,您必須建立一個 Messages
執行個體,如下所示
§之前
import play.mvc.Result;
import play.mvc.Controller;
public class MyController extends Controller {
public Result action() {
changeLang(Lang.forCode("es"));
return ok(myview.render(messages));
}
}
§之後
import javax.inject.Inject;
import play.i18n.Messages;
import play.i18n.MessagesApi;
import play.mvc.Result;
import play.mvc.Controller;
public class MyController extends Controller {
private final MessagesApi messagesApi;
@Inject
public MyController(MessagesApi messagesApi) {
this.messagesApi = messagesApi;
}
public Result action() {
Messages messages = this.messagesApi.preferred(Lang.forCode("es"));
return ok(myview.render(messages));
}
}
或者,如果您想要回退到要求的語言,您也可以這樣做
import javax.inject.Inject;
import play.i18n.Messages;
import play.i18n.MessagesApi;
import play.mvc.Result;
import play.mvc.Controller;
public class MyController extends Controller {
private final MessagesApi messagesApi;
@Inject
public MyController(MessagesApi messagesApi) {
this.messagesApi = messagesApi;
}
public Result action() {
Lang lang = Lang.forCode("es");
// Get a Message instance based on the spanish locale, however if that isn't available
// try to choose the best fitting language based on the current request
Messages messages = this.messagesApi.preferred(request.withTransientLang(lang));
return ok(myview.render(messages));
}
}
注意:為了不讓每個動作方法中重複該程式碼,您可以建立 動作組合鏈 中動作的
Messages
執行個體,並將該執行個體儲存在要求屬性中,以便稍後存取。
現在範本
@()(implicit messages: play.i18n.Messages)
@{messages.at("hello")}
注意:將
messages
宣告為implicit
將使其可供隱含需要MessagesProvider
的子檢視使用,而不需要傳遞給它們。
同樣也適用於 clearLang
§之前
import play.mvc.Result;
import play.mvc.Results;
import play.mvc.Controller;
import play.i18n.MessagesApi;
public class FooController extends Controller {
public Result action() {
clearLang();
return Results.ok("Hello");
}
}
§之後
import javax.inject.Inject;
import play.mvc.Result;
import play.mvc.Results;
import play.mvc.Controller;
import play.i18n.MessagesApi;
public class FooController extends Controller {
private final MessagesApi messagesApi;
@Inject
public FooController(MessagesApi messagesApi) {
this.messagesApi = messagesApi;
}
public Result action() {
return Results.ok("Hello").withoutLang(messagesApi);
}
}
§Http.Context.session()
已棄用
這表示直接依賴它的其他方法也已棄用
play.mvc.Controller.session()
play.mvc.Controller.session(String key, String value)
play.mvc.Controller.session(String key)
擷取請求的 session 的新方法是呼叫 Http.Request
執行個體的 session()
方法。操作 session 的新方法是呼叫對應的 play.mvc.Result
方法。例如
§之前
import play.mvc.Result;
import play.mvc.Results;
import play.mvc.Controller;
public class FooController extends Controller {
public Result info() {
String user = session("current_user");
return Results.ok("Hello " + user);
}
public Result login() {
session("current_user", "[email protected]");
return Results.ok("Hello");
}
public Result logout() {
session().remove("current_user");
return Results.ok("Hello");
}
public Result clear() {
session().clear();
return Results.ok("Hello");
}
}
§之後
import play.mvc.Http;
import play.mvc.Result;
import play.mvc.Results;
import play.mvc.Controller;
import java.util.Optional;
public class FooController extends Controller {
public Result info(Http.Request request) {
// Get the current user or then fallback to guest
String user = request.session().getOptional("current_user").orElse("guest");
return Results.ok("Hello " + user);
}
public Result login(Http.Request request) {
return Results.ok("Hello")
.addingToSession(request, "current_user", "[email protected]");
}
public Result logout(Http.Request request) {
return Results.ok("Hello")
.removingFromSession(request, "current_user");
}
public Result clear() {
return Results.ok("Hello")
.withNewSession();
}
}
§Http.Context.flash()
已棄用
這表示直接依賴它的其他方法也已棄用
play.mvc.Controller.flash()
play.mvc.Controller.flash(String key, String value)
play.mvc.Controller.flash(String key)
擷取請求的 flash 的新方法是呼叫 Http.Request
執行個體的 flash()
方法。操作 flash 的新方法是呼叫對應的 play.mvc.Result
方法。例如
§之前
import play.mvc.Result;
import play.mvc.Results;
import play.mvc.Controller;
public class FooController extends Controller {
public Result info() {
String message = flash("message");
return Results.ok("Message: " + message);
}
public Result login() {
flash("message", "Login successful");
return Results.redirect("/dashboard");
}
public Result logout() {
flash().remove("message");
return Results.redirect("/");
}
public Result clear() {
flash().clear();
return Results.redirect("/");
}
}
§之後
import play.mvc.Http;
import play.mvc.Result;
import play.mvc.Results;
import play.mvc.Controller;
public class FooController extends Controller {
public Result info(Http.Request request) {
String message = request.flash().getOptional("message").orElse("The default message");
return Results.ok("Message: " + message);
}
public Result login() {
return Results.redirect("/dashboard")
.flashing("message", "Login successful");
}
public Result logout() {
return Results.redirect("/")
.removingFromFlash("message");
}
public Result clear() {
return Results.redirect("/")
.withNewFlash();
}
}
§範本輔助方法已棄用
在範本內部,Play 為您提供各種依賴於內部 Http.Context
的輔助方法。這些方法已從 Play 2.7 開始棄用。相反地,您現在必須明確地將所需物件傳遞給您的範本。
§之前
@()
@ctx()
@request()
@response()
@flash()
@session()
@lang()
@messages()
@Messages("some_msg_key")
§之後
@(Http.Request request, Lang lang, Messages messages)
@request
@request.flash()
@request.session()
@lang
@messages
@messages("some_msg_key")
ctx()
和 response()
沒有直接替換品。
§某些範本標籤需要隱含的 Request
、Messages
或 Lang
實例
某些範本標籤需要存取 Http.Request
、Messages
或 Lang
實例才能正確運作。到目前為止,這些標籤僅使用 Http.Context.current()
來擷取此類實例。
但是,由於 Http.Context
已棄用,因此此類實例現在應傳遞為 implicit
參數給使用此類標籤的範本。透過將參數標記為 implicit
,您不必總是將它傳遞給實際需要它的標籤,但標籤可以自動從隱含範圍中擷取它。
注意:要更深入了解隱含參數如何運作,請參閱 Scala 常見問題的 隱含參數 和 Scala 在哪裡尋找隱含參數 部分。
下列標籤需要隱含的 Request
實例才能存在
@(arg1, arg2,...)(implicit request: Http.Request)
@*These tags will automatically use the implicit request passed to this template:*@
@helper.jsloader
@helper.script
@helper.style
@helper.javascriptRouter
@CSRF
@CSRF.formField
@CSRF.getToken
@defaultpages.devError
@defaultpages.devNotFound
@defaultpages.error
@defaultpages.badRequest
@defaultpages.notFound
@defaultpages.todo
@defaultpages.unauthorized
因此,如果您有使用某些上述標籤的檢視,例如,如果您有一個檔案 app/views/names.scala.html
如下所示
@(names: List[String])(implicit request: Http.Request)
<html>
<head>
<!-- `scripts` tags requires a request to generate a CSP nonce -->
@script(args = 'type -> "text/javascript") {
alert("Just a single inline script");
}
</head>
<body>
...
</body>
</html>
您的控制器需要將請求傳遞為參數給 render
方法
import java.util.List;
import java.util.ArrayList;
import javax.inject.Inject;
import play.mvc.Http;
import play.mvc.Result;
import play.mvc.Controller;
public class SomeController extends Controller {
public Result action(Http.Request request) {
List<String> names = new ArrayList<>("Jane", "James", "Rich");
return ok(views.html.names.render(names, request));
}
}
還有一些輔助標籤需要隱含的 Messages
實例才能存在
@(arg1, arg2,...)(implicit messages: play.i18n.Messages)
These tags will automatically use the implicit messages passed to this template:
@helper.inputText
@helper.inputDate
@helper.inputCheckboxGroup
@helper.inputFile
@helper.inputRadioGroup
@helper.inputPassword
@helper.textarea
@helper.input
@helper.select
@helper.checkbox
因此,如果您有使用某些上述標籤的檢視,例如,如果您有一個檔案 app/views/userForm.scala.html
如下所示
@(userForm: Form[User])(implicit messages: play.i18n.Messages)
<html>
<head>
<title>User form page</title>
</head>
<body>
@helper.form(action = routes.UsersController.save) {
@helper.inputText(userForm("name"))
@helper.inputText(userForm("email"))
...
})
</body>
</html>
您的控制器將會像這樣
import javax.inject.Inject;
import play.mvc.Http;
import play.mvc.Result;
import play.mvc.Controller;
import play.i18n.Messages;
import play.i18n.MessagesApi;
import play.data.FormFactory;
public class SomeController extends Controller {
private final FormFactory formFactory;
private final MessagesApi messagesApi;
@Inject
public SomeController(FormFactory formFactory, MessagesApi messagesApi) {
this.formFactory = formFactory;
this.messagesApi = messagesApi;
}
public Result action(Http.Request request) {
Form<User> userForm = formFactory.form(User.class);
// Messages instance that will be passed to render the view and
// inside the view will be passed implicitly to helper tags.
Messages messages = messagesApi.preferred(request);
return ok(views.html.userForm.render(userForm, messages));
}
}
注意:某些功能以前由
PlayMagicForJava
提供,且高度依賴於Http.Context.current()
。這就是為什麼您會看到類似這樣的警告method implicitXXX in object PlayMagicForJava is deprecated (since 2.7.0): See https://play.dev.org.tw/documentation/latest/JavaHttpContextMigration27
傳遞參數到您的檢視會讓您更清楚發生了什麼事,以及您的檢視如何取決於其他資料。
Play 本身並未提供需要 Lang
執行個體才能存在的標籤,但第三方模組可能會提供
@(arg1, arg2,...)(implicit lang: play.i18n.Lang)
第三方標籤會自動使用傳遞到這個範本的隱含訊息。您可以傳遞 Lang
的隱含執行個體到您的檢視,就像上述 Http.Request
和 Messages
的範例一樣。
§與 Http.Context
相關的 Java Forms 變更
當擷取 Field
的 Form
(例如透過 myform.field("username")
或範本內的 myform("username")
)時,會使用目前 Http.Context
的語言來格式化欄位的數值。不過,從 Play 2.7 開始,情況不再如此。相反地,您現在可以在擷取欄位時明確設定表單應使用的語言。為了簡化流程,並避免您必須為每個表單明確設定語言,Play 已在繫結期間設定語言
import play.mvc.Http;
import play.mvc.Result;
import play.mvc.Controller;
import play.data.Form;
import play.data.FormFactory;
public class MyController extends Controller {
private final FormFactory formFactory;
@Inject
public MyController(FormFactory formFactory) {
this.formFactory = formFactory;
}
public Result action(Http.Request request) {
// In this example, the language of the form will be set
// to the preferred language of the request.
Form<User> form = formFactory.form(User.class).bindFromRequest(request);
return ok(views.form.render(form));
}
}
您也可以在需要時變更現有表單的語言
import play.mvc.Result;
import play.mvc.Controller;
import play.i18n.Lang;
import play.data.Form;
import play.data.FormFactory;
public class MyController extends Controller {
private final FormFactory formFactory;
@Inject
public MyController(FormFactory formFactory) {
this.formFactory = formFactory;
}
public Result action(Http.Request request) {
// There is first the lang from request
Form<User> form = formFactory.form(User.class).bindFromRequest(request);
// Let's change the language to `es`.
Lang lang = Lang.forCode("es");
Form<User> formWithNewLang = form.withLang(lang);
return ok(views.form.render(formWithNewLang));
}
}
注意:變更目前
Http.Context
的語言(例如透過Http.Context.current().changeLang(...)
或Http.Context.current().setTransientLang(...)
)不再會影響用於擷取表單欄位數值的語言 - 如前所述,請改用form.withLang(...)
。
§從 args
中移除 Http.Context
要求標籤
在 Play 2.6 中已被棄用的請求標籤,已在 Play 2.7 中被移除。因此,Http.Context
實例的 args
地圖也不再包含這些已移除的請求標籤。現在,您可以改用 request.attrs()
方法,它會提供給您相同的請求屬性。
§從 args
中移除 CSRF 令牌
@AddCSRFToken
動作註解會將兩個名為 CSRF_TOKEN
和 CSRF_TOKEN_NAME
的項目新增到 Http.Context
實例的 args
地圖中。這些項目已移除。請使用取得令牌的新正確方法。
§RoutingDSL 變更
在 Play 2.6 之前,在使用 Java routing DSL 時,除了 Http.Context.current()
之外,沒有其他方法可以存取目前的 request
。現在,DSL 有新的方法,其中會將請求傳遞至區塊。
§之前
import play.mvc.Http;
import play.routing.Router;
import play.routing.RoutingDsl;
import javax.inject.Inject;
import static play.mvc.Results.ok;
public class MyRouter {
private final RoutingDsl routingDsl;
@Inject
public MyRouter(RoutingDsl routingDsl) {
this.routingDsl = routingDsl;
}
public Router router() {
return routingDsl
.GET("/hello/:to")
.routeTo(to -> {
Http.Request request = Http.Context.current().request();
return ok("Hello " + to + ". Here is the request path: " + request.path());
})
.build();
}
}
在上述範例中,我們需要使用 Http.Context.current()
來存取請求。從現在開始,您可以改寫成下列程式碼
§之後
import play.routing.Router;
import play.routing.RoutingDsl;
import javax.inject.Inject;
import static play.mvc.Results.ok;
public class MyRouter {
private final RoutingDsl routingDsl;
@Inject
public MyRouter(RoutingDsl routingDsl) {
this.routingDsl = routingDsl;
}
public Router router() {
return routingDsl
.GET("/hello/:to")
.routingTo((request, to) ->
ok("Hello " + to + ". Here is the request path: " + request.path())
)
.build();
}
}
一個需要注意的重要面向是,在新的 API 中,Http.Request
永遠會是函數區塊的第一個參數。
§停用 Http.Context
和 JPA 執行緒區域變數
如果您遵循上述遷移注意事項,並變更所有程式碼,使其不會使用依賴於 Http.Context
的 API(表示您不再收到編譯器警告),則可以停用 Http.Context
執行緒區域變數。
只要將下列程式碼行新增至 application.conf
檔案即可
play.allowHttpContext = false
若要停用 play.db.jpa.JPAEntityManagerContext
執行緒區域變數,請新增
play.jpa.allowJPAEntityManagerContext = false
下一步:Play 2.6
在此文件發現錯誤?此頁面的原始程式碼可在此處找到 here。閱讀 文件指南 後,請隨時提交拉取請求。有問題或建議要分享嗎?請前往 我們的社群論壇 與社群展開對話。