文档

§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,这些 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 之前,当使用 Java 与 Play 一起使用时,访问 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 的导入),并将实际请求传递到相应的操作方法的参数中。

注意:这不太可能,但您可能有一个名为 Request 的自定义 QueryStringBindablePathBindable。如果是这样,它现在会与 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)

检索请求会话的新方法是调用 Http.Request 实例的 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)

获取请求闪存的新方法是调用Http.Request实例的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 FAQ 的隐式参数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://playframework.com.cn/documentation/latest/JavaHttpContextMigration27

将参数传递给您的视图将使您更清楚地了解发生了什么以及您的视图依赖于哪些其他数据。

Play 本身不提供需要 Lang 实例才能使用的标签,但第三方模块可能需要。

@(arg1, arg2,...)(implicit lang: play.i18n.Lang)

第三方标签将自动使用传递给此模板的隐式消息。您可以像上面 Http.RequestMessages 的示例一样,将 Lang 的隐式实例传递给您的视图。

检索 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 路由 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


发现此文档中的错误? 此页面的源代码可以在 此处 找到。 阅读完 文档指南 后,请随时贡献拉取请求。 有问题或建议要分享? 转到 我们的社区论坛 与社区进行对话。