Spring Validator(でラップされたBean Validation)のメッセージをi18nしたときの覚え書き
   6 min read

やりたかったこと

  • BeanValidation のプロパティファイル ValidationMessages.properties でなく、 Spring のメッセージプロパティに統合したい。
    • Spring のメッセージプロパティとは?
  • Accept-Languageベースでメッセージを国際化したい。

調べた

Spring のメッセージプロパティファイルはどこ?

これはキーワード “site:spring.io i18n message” でググるとすぐ見つかった。

messages.properties だ。

また、メッセージに関するプロパティは、このページで説明されている通り MessageSourceProperties で確認できる。
例えば cacheDuration というフィールドがあるので application.properties ファイルには

spring.messages.cache-duration

という名前で該当値を設定できる(フィールド名は camelCase だが、これに bind するプロパティ名は いわゆるkebab-case が推奨されている)。

Externalized Configurationという仕組みだと思うが、 どうやって実現しているのかは分からんMessageSourcePropertiesConfigurationProperties アノテーションが付いているわけでもなし。

デフォルト設定値を調べる

デバッガで追いかけた。 これ本当ならどうやってリファレンス探せば見つかるんだ?

ValidationAutoConfiguration#defaultValidator() メソッドだ。

プロジェクト名(ディレクトリ名)から想像がつく通り、 Auto-configurationという仕組みだ。
明示的な設定を行っていない場合、フレームワーク側でよしなに設定を行ってくれる。

…が、その設定が気に入らない、というのが今回の問題のひとつの側面だ。

ここで登場するメソッド MessageInterpolatorFactory#getObject()で、お節介にも BeanValidation 側の MessageInterpolator を取ってきている。
このため冒頭で記載したとおりデフォルトで ValidationMessages.properties が使われるようだ。
マジかよ? なんでそんなとこで日和ってんねん!

なおした

今回こうやった

まずはじめに、今回の問題の本質とは無関係だが、fallback 先ロケール設定を変更しておく。デフォルトだとシステムロケール、つまり日本語環境なら messages.properties でなく messages_ja.properties にフォールバックしてしまうので直感に反する。
そこで application.properties ファイルに次を設定。

spring.messages.fallback-to-system-locale=false

Spring Tools 4 for Eclipse なら補完も効くし説明もポップアップ表示される。嬉しい。なお、もしかしたら他の IDE でも同様の機能はあるのかもしらんが、使ったこと無いのでわからない。

さて本題。

上記で登場した auto-configuration であるところの ValidationAutoConfiguration#defaultValidator() を上書きして自分好みにしてしまえばいい。
@ConditionalOnMissingBean(Validator.class) が付与されているので、自前で Validator を提供するメソッドを作ってしまえば良いということだ。
というわけでこんなクラスを作るぞ。

import javax.validation.Validator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class MyConfig {

    @Autowired
    private MessageSource messageSource;

    @Bean
    public Validator localValidatorFactoryBean() {
        final LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        factoryBean.setValidationMessageSource(messageSource);
        return factoryBean;
    }
}

LocalValidatorFactoryBeanjavax.validation.Validatororg.springframework.validation.Validator も実装しているが、戻り値の型として使うべきは(@ConditionalOnMissingBeanで指定している型である)前者だ。間違えて後者を使うと想定どおり動作しないやっかいなバグになるぞ。
ちなみに戻り値の型指定、 javax.validation.Validator の代わりに LocalValidatorFactoryBean でも動作した。
instanceofで評価しているのだと思うが確証はない。

さて MessageSource が登場しているがこれはなにか? わからん
ググってたときにたまたま見つけた。
いやもちろん名前通りメッセージリソースなのだが、ここまで見てきたリファレンスでは一切登場していない。
27. InternationalizationとかMessageSourcePropertiesの javadoc とかで触れられているべきでは!?

こいつはロケール考慮されているようで、デフォルトだと AcceptHeaderLocaleResolverを使って 冒頭に記載した希望通りAccept-Languageを考慮してくれるようだがいい加減調べるのに疲れたので裏はとっていない。
多分このへん読めば良いのだろう。

ところで この部分の調査の過程で LocalValidatorFactoryBean #setValidationMessageSourceに解答っぽい記述があることに気づいた。
が、 この文章見て具体的に何やればいいかわからんのだが? そもそも このメソッドの存在をどうやったら見つけられるんだ?

やっかいなことに

Spring(Boot)のやりかたは上に書いた方法だけではないようだ。次のエントリで同じ目的を達成するための方法が説明されている。

WebMvcConfigurerAdapter(注: 現行バージョンでは代わりに WebMvcConfigurer)って何やねんどこから出てきた?

どっちで設定すべきやねーん!

追記

ロケールについて

上で端折った i18n について調べ直した。デバッガのステップ実行で

LocaleContextHolder#getLocale() でロケールを取得している。

こいつを使っているのが MessageInterpolator(を実装した実体 LocaleContextMessageInterpolatorinterpolate メソッド だ。

ServletFilter であるところの RequestContextFilterHttpServletRequest#getLocale() をロケールとして設定している。

したがって、 HTTP リクエストコンテキストでは (補足: なんか Qiita の他の人の記事とかではコンテキストを無視して説明している文章が多いぞ。別に validation は RequestScope だけで行うわけではなかろう)、ロケールは HttpServletRequest#getLocale() の値となるし、それ以外のコンテキストでもそのコンテキストに応じたロケールを設定してくれていると期待できることが分かった。

WebMvcConfigurer

ここにあった!

何が Spring Boot で何が Spring なのか全然分からん

いや、よく考えると auto-configuration はあくまで付加的な仕組みであって、そこから行うべきことを考えるのは筋が違うな。
ということは WebMvcConfigurer で設定するのが本来の姿なのだろうか。
しかし Validation の、国際化の仕組みの設定が WebMvcConfigurer という名前のものに備わっているというのはどうやって思い至れば良いんだろう?

もしかしたらこういう手順か?

  1. Spring Boot リファレンスの該当しそうな章 37.Validation を見る。
  2. @Validated という validation 専用っぽいアノテーションが使われているぞ。
  3. javadoc の記述を読むと Spring MVC という単語が出てるので、validation の仕組みは Spring MVC の機能のひとつなんだ?
  4. Spring Framework ドキュメントのリストを見ると Spring MVC はWeb Servletに含まれているようだ? Servlet と validation って何か関係があるのか?よく分からんが見てみよう。
  5. 該当する節 1.10.4. Validationが見つかった!なるほど 設定は WebMvcConfigurer で行うのか!
  6. 関係しそうな名前 getValidator()ってメソッドがあるぞ!

マジでこんなこと考えるの?無理ゲーじゃない?
あとこれから更に LocalValidatorFactoryBean インスタンスを生成して返すってのに気付くのにもまた一山超える必要がありそうだし。

まとめ

  • Spring 全然わからん。
  • Qiita のスタイル、バックスラッシュでくくるとハイパーリンク貼ってるのかどうなのかわからんからいまいち。
  • Qiita 等で Spring( Boot)解説エントリを書いている人に向けて: 根拠となる公式リファレンス/ソースコードへの参照も含めてほしい。あなたの書いたその実装方法が妥当なのか入門者は確認できない。