§Java Http.Context
の変更点
play.mvc.Http.Context
はJava HTTP & MVC APIの重要な部分ですが、これらのAPIがどのように動作すべきかの適切な抽象化ではありません。より良くモデル化できるいくつかの概念や、Playのようなマルチスレッドフレームワークでテストや推論が複雑な実装の詳細があります。例えば、Http.Context
はスレッドローカルを使用して現在のリクエストをキャプチャしてアクセスしますが、現在のリクエストはどこからでもアクセスできるという印象を与えます。これは、アクターやカスタムスレッドプールを使用している場合は現在当てはまりません。
APIモデリングに関して、いくつかの重複した概念(例えば、play.mvc.Result
対 play.mvc.Http.Response
)や、場違いに見えるメソッド(例えば、play.mvc.Http.RequestHeader.id
の代わりに play.mvc.Http.Context.id
)があります。そのため、Http.Context
に複数の変更が加えられ、それから離れることが目的です。そこで、テスト、推論、将来の保守がより簡単な新しいAPIを提供します。
play.mvc.Http.Context
は既存のAPIの中心的な部分であるため、非推奨にすると、依存している多くの場所に影響がありました。例えば play.mvc.Controller
などです。このページでは、これらの変更と移行方法について説明しますが、各メソッドの非推奨のJavadocも参照できます。
§Http.Context.current()
および Http.Context.request()
の非推奨
つまり、この2つに直接依存する他のメソッドも非推奨になりました。
play.mvc.Controller.ctx()
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
のインポート)のルートパラメーターを自動的に検出し、実際のリクエストを対応するアクションメソッドのパラメーターに渡します。
注意: カスタムの
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
は、それへの他のアクセスメソッドとともに非推奨になりました。主にヘッダーとクッキーを追加するために使用されましたが、これらはすでに 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);
}
}
テンプレートのレンダリングに使用される Lang
を変更するために changeLang
を使用している場合は、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)
リクエストのセッションを取得する新しい方法は、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()
の非推奨
つまり、それに直接依存する他のメソッドも非推奨になりました。
play.mvc.Controller.flash()
play.mvc.Controller.flash(String key, String value)
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()
の直接的な代替はありません。
§一部のテンプレートタグは、暗黙的な Request
、Messages
、または Lang
インスタンスを必要とする
一部のテンプレートタグは、正しく動作するために、Http.Request
、Messages
、または Lang
インスタンスにアクセスする必要があります。これまで、これらのタグは 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://play.dokyumento.jp/documentation/latest/JavaHttpContextMigration27
パラメータをビューに渡すことで、何が起こっているのか、ビューが他のデータに依存している場所がより明確になります。
Play 自体は、Lang
インスタンスが存在する必要があるタグを提供していませんが、サードパーティのモジュールは提供する可能性があります。
@(arg1, arg2,...)(implicit lang: play.i18n.Lang)
サードパーティのタグは、このテンプレートに渡された暗黙のメッセージを自動的に使用します。上記の Http.Request
と Messages
の例のように、Lang
の暗黙的なインスタンスをビューに渡すことができます。
§Http.Context
に関連する Java フォームの変更
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(...)
を使用してください。
§Http.Context
リクエストタグが args
から削除されました
Play 2.6 で非推奨になったリクエストタグが、Play 2.7 でついに削除されました。したがって、Http.Context
インスタンスの args
マップには、これらの削除されたリクエストタグも含まれなくなりました。代わりに、同じリクエスト属性を提供する request.attrs()
メソッドを使用できるようになりました。
§CSRF トークンが args
から削除されました
@AddCSRFToken
アクションアノテーションは、CSRF_TOKEN
と CSRF_TOKEN_NAME
という名前の 2 つのエントリを Http.Context
インスタンスの args
マップに追加していました。これらのエントリは削除されました。トークンを取得する新しい正しい方法を使用してください。
§RoutingDSL の変更
Play 2.6 まで、Java の ルーティング DSL を使用する場合、現在の request
にアクセスする他の方法は、Http.Context.current()
の他にありませんでした。現在、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
このドキュメントにエラーを見つけましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、お気軽にプルリクエストを送信してください。質問や共有したいアドバイスがありますか?コミュニティフォーラムにアクセスして、コミュニティとの会話を始めましょう。