文件

§Java Http.Context 變更

play.mvc.Http.Context 是 Java HTTP 和 MVC API 的重要部分,但它並非這些 API 應如何運作的良好抽象。它有一些可以更好地建模的概念,或者在像 Play 這樣的多執行緒架構中,實作細節複雜到難以測試和推理。例如,Http.Context 使用執行緒本機來擷取和存取目前的請求,但它給人的印象是目前的請求可以從任何地方存取,如果你正在使用 Actor 或自訂執行緒池,這並非事實。

關於 API 建模,有一些重複的概念(例如 play.mvc.Resultplay.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() 已棄用

這表示直接依賴這兩個方法的其他方法也已棄用

  1. play.mvc.Controller.ctx()
  2. 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 的匯入),並將實際的要求傳遞到對應的動作方法的參數。

注意:雖然不太可能,但你可能會有一個自訂的 QueryStringBindablePathBindable,其名稱為 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 類別已棄用

這表示也棄用了其他直接依賴這些方法的方法

  1. 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 方法已棄用

下列方法已棄用

  1. Http.Context.lang()
  2. Http.Context.changeLang(Lang lang)
  3. Http.Context.changeLang(String code)
  4. Http.Context.clearLang()
  5. Http.Context.setTransientLang(Lang lang)
  6. Http.Context.setTransientLang(String code)
  7. Http.Context.clearTransientLang()
  8. Http.Context.messages()

這表示也棄用了其他直接依賴這些方法的方法

  1. play.mvc.Controller.lang()
  2. play.mvc.Controller.changeLang(Lang lang)
  3. play.mvc.Controller.changeLang(String code)
  4. 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() 已棄用

這表示直接依賴它的其他方法也已棄用

  1. play.mvc.Controller.session()
  2. play.mvc.Controller.session(String key, String value)
  3. 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() 已棄用

這表示直接依賴它的其他方法也已棄用

  1. play.mvc.Controller.flash()
  2. play.mvc.Controller.flash(String key, String value)
  3. 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() 沒有直接替換品。

§某些範本標籤需要隱含的 RequestMessagesLang 實例

某些範本標籤需要存取 Http.RequestMessagesLang 實例才能正確運作。到目前為止,這些標籤僅使用 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.RequestMessages 的範例一樣。

當擷取 FieldForm(例如透過 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_TOKENCSRF_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。閱讀 文件指南 後,請隨時提交拉取請求。有問題或建議要分享嗎?請前往 我們的社群論壇 與社群展開對話。