結論
Jakarta Validation では validate と同時にメッセージが生成されますが、 ExceptionHandler 側でメッセージ生成する方が設計上すっきりするよね、というお話と、その機能を持つ ecuacion-lib-validation の紹介です。
はじめに
Jakarta Validation は、チェックロジックを annotation で宣言的に書けてとても便利。
ただ、メッセージまわりの設計で悩むことがちょいちょいあります。
その一つが「validate のタイミングでメッセージが生成されてしまう」問題。
今回は、これがなぜ問題になるのか、そしてどう解決するのかを書いてみます。
Jakarta Validation 標準のメッセージ生成タイミング
Jakarta Validation 標準では、validator.validate(...) の呼び出しのタイミングでメッセージが生成されます。
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<Account>> violations = validator.validate(account);
for (ConstraintViolation<Account> v : violations) {
System.out.println(v.getMessage()); // ← すでに生成済みのメッセージが返ってくる
}
v.getMessage() は validate したタイミングで生成済みのメッセージを返すだけ、ということですね。
メッセージ生成に使用する locale は、デフォルトでは JVM のデフォルト locale が使用されます。
どこが問題なのか
ユーザごとに言語を切り替えるような、いわゆる多言語対応のシステムを考えてみます。
locale をユーザごとに変えたい場合、validator 生成時に locale の指定が必要です。
つまり、(作りにはよりますが)validate する場所(service 層など)で locale を取り扱う必要が発生します。。
public void createAccount(Account account, Locale locale) { // ← locale を引数で受け取る羽目に
ValidatorFactory factory = Validation.byDefaultProvider()
.configure()
.messageInterpolator(
new LocaleSpecificMessageInterpolator(locale)) // ← locale をここで使用
.buildValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<Account>> violations = validator.validate(account);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
service 層のメソッドに Locale が引数として出てきており、これだと煩雑になりますよね。
validate とメッセージ生成を分離
解決策はシンプルで、「validate のタイミング」と「メッセージ生成のタイミング」を分離することです。
具体的には、以下の流れにします。
- service 層で
validate(locale 不要) - 検証エラーがあれば
ConstraintViolationExceptionを throw -
ExceptionHandlerで catch -
ExceptionHandlerでlocaleを取得し、メッセージを生成
こうすることで、service 層は locale に一切関与しなくてよくなります。
public void createAccount(Account account) { // ← locale の引数が不要
Set<ConstraintViolation<Account>> violations = validator.validate(account);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<String> handleConstraintViolation(
ConstraintViolationException ex, Locale locale) {
// ここで locale を使ってメッセージ生成
List<String> messages = generateMessages(ex, locale);
...
}
validate した後に ConstraintViolation の Set が ConstraintViolationException に包まれて飛んでくるので、ExceptionHandler でそれを受け取ってメッセージを生成する、というシンプルな設計です。
ecuacion-lib-validation : ExceptionUtil
この「ExceptionHandler 側でメッセージを生成する」を実現するのが、ecuacion-lib-validation の ExceptionUtil です。
ExceptionUtil.getMessageList は、ConstraintViolationException または Set<ConstraintViolation<?>> を受け取り、メッセージの List<String> を返します。
ecuacion-lib-validation の導入方法
導入方法については下記記事をご参照ください。
基本的な使い方
検証するためのオブジェクトが必要なので、こちらを使用します。
public record Account(@NotNull String name) {
}
検証を実行するためのコードはこちら。
private static void 基本的な使い方() {
try {
// 以下、service層のイメージ。ここではlocale不要
Set<ConstraintViolation<Account>> violations = validator.validate(new Account(null));
if (violations.size() > 0) {
throw new ConstraintViolationException(violations);
}
} catch (ConstraintViolationException ex) {
// 以下、exceptionHandler内のイメージ。ここでlocaleを使用
for (String message : ExceptionUtil.getMessageList(ex, Locale.JAPANESE)) {
System.out.println(message);
}
}
}
実行した結果はこちら。
入力必須です
ExceptionUtil.getMessageList を呼んだタイミングでメッセージが生成されます。validate したタイミングではありません。
ExceptionUtil のメッセージ内容
Jakarta Validation の標準機能(というか Hibernate Validator ですかね)だと通常、@NotNull だと null は許可されていません のようなメッセージになっています。
が、エンドユーザが読むには辛い(まず null がわからない^^;)ので、エンドユーザにもわかりやすいと思われるメッセージをデフォルトで保持しています。
もちろん、ValidationMessages.properties を定義することにより変更可能です。
サンプルコード
サンプルコードは以下です。
https://github.com/ecuacion-jp/ecuacion-code-snippets/tree/main/ecuacion-lib-core-JakartaValidationExceptionUtil
※このページから直接ソースの zip を download はできないと思うので、そのページにある ecuacion-code-snippets のリンクをクリックし、そこにある緑の <> Code ボタンから Download ZIP で download してください。
本サンプルのフォルダに移動後、mvn compile exec:java で実行できます。
まとめ
Jakarta Validation は validate とメッセージ生成が同一タイミングで行われる仕様ですが、ecuacion-lib-validation の ExceptionUtil を使って分離することで locale をロジック層に持ち込まなくて済む、というお話でした。