Spring MVC のロケール周りについて調べたのでメモ、以下のバージョンで調べました。
- Spring Boot : 3.3.5
- Spring Framework : 6.1.14
結論からいうと LocaleResolver
がポイントとなっているようです。
Locale
が必要なパターン
どういったときに ロケールを意識するかのユースケースをはじめに紹介します。
MessageSource
を使う
Spring では MessageSource
を使うことでメッセージを取得することができます。
Spring Bootでは messages.properties
を用意しておくことでメッセージのハードコーディングを避けることができます。
以下、利用例です。
hello=こんにちは
@Autowired
private MessageSource messageSource;
@GetMapping("/hello")
public Hello1Response hello() {
final String hello = messageSource.getMessage("hello", null, null);
// 省略
}
上記のように MessageSource#getMessage
メソッドを使うことで 外部ファイルに定義された値を取得することができます。
多言語対応が必要な場合
多言語対応が必要な場合は 言語ごとに messages.properties
を定義します。
デフォルト
hello=こんにちは
英語用
hello=Hello
また、MessageSource#getMessage
はいくつかオーバーロードされたものがありますが、それぞれ引数にLocale
を受け取ることができます
@GetMapping("/hello")
public Hello1Response hello() {
// 日本語(こんにちは)
final String jaHello = messageSource.getMessage("hello", null, Locale.JAPANESE);
// 英語(Hello)
final String enHello = messageSource.getMessage("hello", null, Locale.ENGLISH);
// 省略
}
上記のように Locale
を渡すことで、それぞれの言語のメッセージを取得できます。
(前の例のようにnull
を渡した場合は 実装クラスにもよりますが Locale.getDefault
が利用されます。)
リクエストに紐づいたロケールを使いたい
先の例の方法だと至る所で言語別の分岐が必要になってしまいます。
できれば利用しているユーザーのリクエストに応じたロケールを使いたいものです。
ServletRequest#getLocale
ServletRequest
には getLocale
というメソッドがあり、Accept-language:
リクエストヘッダーに紐づくロケールを取得することができます。
画面のない WebAPI などではこれでもよいかもしれないですが今回はSpringの仕組みの紹介をするので...
Controller
のハンドラメソッドの引数で Locale
が受け取れる
@GetMapping("/hello")
public Hello1Response hello(Locale locale) {
final String hello = messageSource.getMessage("hello", null, locale);
}
上記のように メソッドの引数に Locale
を定義することでリクエストに紐づいた ロケールを取得することができます。
ハンドラメソッドの引数は HandlerMethodArgumentResolver
というインターフェースを使って解決されるのですが、Locale
の場合は ServletRequestMethodArgumentResolver
が利用されています。
ここでは RequestContextUtils.getLocale
メソッドを呼び出しておりその先では LocaleResolver
を使ってロケールの解決が行われています。
これが今回のポイントとなるインターフェースです。
上記で登場したそれぞれのメソッドは以下を参照してください。
LocaleResolver
インターフェース
LocaleResolver#resolveLocale(HttpServletRequest)
メソッドを使うことで現在のリクエストに紐づいたロケールを取得することができます。
このインターフェースには
- AcceptHeaderLocaleResolver
- CookieLocaleResolver
- SessionLocaleResolver
- FixedLocaleResolver
などの実装クラスがあります。 それぞれ、リクエストヘッダー、Cookie, セッション、固定値をもとにロケールを取得するようになっています。
デフォルトでは AcceptHeaderLocaleResolver
が使われているようで, Spring Bootだと WebMvcAutoConfiguration で ConditionalOnMissingBean(name = DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME)
で定義されているので、上書き定義することで LocaleResolver
を変更することができそうです。
セッションに保存すると セッションが切れたときに画面が元の言語に戻ってしまうので今回はCookieを使ったBean定義の例を示します。
@Bean(name = DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME)
public LocaleResolver localeResolver(WebProperties webProperties) {
final AcceptHeaderLocaleResolver headerLocaleResolver = new AcceptHeaderLocaleResolver();
headerLocaleResolver.setDefaultLocale(webProperties.getLocale());
// Cookie名はデフォルトだとSpringを使っていることがわかる名前になっているので返る
final CookieLocaleResolver localeResolver = new CookieLocaleResolver("appLocale");
// Cookieから取得できなかったときは AcceptHeaderLocaleResolverに処理を委譲
localeResolver.setDefaultLocaleFunction(headerLocaleResolver::resolveLocale);
return localeResolver;
}
上記の例は CookieLocaleResolver
をBean定義していますが、もしCookieから取得できなかった場合は AcceptHeaderLocaleResolver
に処理を委譲してリクエストヘッダから取得するようにしています。 setDefaultLocaleFunction は無理にセットしなくてもデフォルトで ServletRequest#getLocale
から取得するようになっています。
ロケールを保存する。
LocaleResolver
を使うことでそれぞれの場所からLocale
を取得できますが、取得元の場所に保存しておかないと役に立ちません。
ドキュメントによると LocaleChangeInterceptor
を使うとできるとありますが 実装 を見ればわかる通り ServletRequest#getParameter
を利用しています。
Thymeleaf
などを使った画面の場合はこれでもよいですが SPAのようにフロントエンドとJSONでやり取りする場合はこれではうまくいきません。このメソッドの実装を参考にしつつ、ロケールを設定する方法を考える必要があります。例えば以下のようにエンドポイントを用意する方法もあります。
@Autowired
private LocaleResolver localeResolver;
@PostMapping("setLocale")
public ResponseEntity<?> setLocale(@RequestBody SetLocaleRequest req, HttpServletRequest request,
HttpServletResponse response) {
// ロケールの設定
// LocaleChangeInterceptor を参考
Locale locale = StringUtils.parseLocale(req.getLang());
localeResolver.setLocale(request, response, locale);
return ResponseEntity.ok().build();
}
あとはログイン成功時などにもユーザーの言語設定に紐づいたロケールを保存しておくと良いかもしれないですね。
注意
AcceptHeaderLocaleResolver
では setLocale
は UnsupportedOperationException
がスローされる実装になっているので注意してください。
おまけ
LocaleContextHolder
について
LocaleContextHolder
というクラスのgetLocale
というstaticメソッドを使うことで LocaleResolver
によって解決されたロケールを取得することができます。
LocaleContextHolder
にロケールが設定されるタイミングは2か所あり
で設定されています。
RequestContextFilter
では ServletRequest#getLocale
で取得したロケールが設定されており、FrameworkServlet
実際は DispatcherServlet
では LocaleResolver
で解決されたロケールが設定されています。
つまり DispatcherServlet
以降の処理である HandlerInterceptor
やハンドラメソッドなどでは LocaleResolver
の値が取れますが、それより手前の処理である サーブレットフィルターではこの値をとることができません。
なので利用箇所については意識しておく必要がありそうです。
(LocaleResolver
から取得した値をセットするサーブレットフィルターを作るのはどうなんだろうか?🤔)
おわりに
-
LocaleResolver
の実装クラスをBean定義することでロケールの取得元を変更できること - ハンドラメソッドの引数に
Locale
を定義したり、LocaleResolver
を直接利用することで現在のリクエストに紐づくロケールを取得することができること - ロケールの変更には
LocaleResolver#setLocale
を実行することで変更できること
を紹介しました。
多言語対応する機会がないと利用しないかもしれないですが考え方だったりは覚えておくと何かの役に立つかもしれません。