はじめに (対象読者・この記事でわかること)
この記事は、Spring BootでREST APIを開発しているが「バリデーションエラーのメッセージをフィールドごとに出し分けたい」「メッセージの変更がソースにハードコードされていて保守が大変」という悩みを持つ中級Java開発者を対象にしています。
記事を読むことで、Javaの独自アノテーション(カスタムアノテーション)を作成し、ConstraintValidatorとMessageSourceを連携させることで、1行のメタデータ変更だけでエラーメッセージを切り替えられるようになります。さらに、YAMLやプロパティファイルからメッセージを一元管理する方法も習得でき、メッセージ変更による再ビルド・再デプロイの手間をゼロにできます。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Java 11以上の基本的な文法(アノテーションの定義方法)
- Spring Boot 3.xでのRESTコントローラ作成経験
- Bean Validation(javax.validation/jakarta.validation)の基礎知識
なぜ独自アノテーションなのか? ~メッセージ出し分けの壁~
Spring Bootで@NotBlankや@Sizeを使うと、デフォルトメッセージが英語のまま返ってきて「日本語にしたい」「フィールドごとにメッセージを変えたい」と思ったことはありませんか?
application.ymlにspring.messages.basename=messagesを書いても、メッセージキーはjavax.validation.constraints.NotBlank.messageのように固定されてしまい、フィールド単位で出し分けできません。
そこで「フィールド名+ルール」をキーにしてメッセージを引ける独自アノテーションを作ることで、
・メッセージファイルだけで完結
・ソース修正なしで文言変更
・多言語対応も自然にサポート
という3つのメリットを同時に得られます。
カスタムアノテーションでエラーメッセージを自在に操る
ステップ1:アノテーション定義@PasswordValid
まず、対象フィールドに付与するアノテーションを定義します。@PasswordValidという名前で、属性値messageKeyを持たせて、メッセージキーを明示的に指定できるようにします。
Java@Target({ ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = PasswordValidator.class) public @interface PasswordValid { String messageKey() default "password.invalid"; // デフォルトキー Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
ステップ2:ConstraintValidator実装@PasswordValidator
次に、実際の検証ロジックを担当するPasswordValidatorを実装します。ここでMessageSourceをインジェクションして、メッセージキーから文言を解決します。
Javapublic class PasswordValidator implements ConstraintValidator<PasswordValid, String> { @Autowired private MessageSource messageSource; private String messageKey; @Override public void initialize(PasswordValid annotation) { this.messageKey = annotation.messageKey(); } @Override public boolean isValid(String value, ConstraintValidatorContext ctx) { if (value == null || value.length() < 8) { ctx.disableDefaultConstraintViolation(); String msg = messageSource.getMessage(messageKey, null, Locale.getDefault()); ctx.buildConstraintViolationWithTemplate(msg).addConstraintViolation(); return false; } return true; } }
ステップ3:メッセージファイル(messages.yml)を用意
src/main/resources/messages.ymlにフィールドごとのキーを定義します。
Yamlpassword.invalid: "パスワードは8文字以上で入力してください" password.admin.invalid: "管理ユーザーのパスワードは12文字以上で入力してください"
ステップ4:DTOで使ってみる
Javapublic class UserRequest { @PasswordValid(messageKey = "password.admin.invalid") private String adminPassword; @PasswordValid // デフォルトキーが適用される private String userPassword; }
ハマった点:MessageSourceがnullになる
Bean ValidationはデフォルトでSpringのDIコンテナ外で動作するため、@AutowiredしたMessageSourceが注入されずにNullPointerExceptionが発生します。
解決策:LocalValidatorFactoryBeanを明示的に定義
@Configurationクラスで以下のようにBeanを定義することで、Springのコンテナを使ったValidatorを強制します。
Java@Configuration public class ValidationConfig { @Bean public LocalValidatorFactoryBean validator(MessageSource messageSource) { LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean(); bean.setValidationMessageSource(messageSource); return bean; } }
これでPasswordValidator内でmessageSource.getMessage()が正常に動作し、フィールドごとに異なるメッセージを返せるようになります。
まとめ
本記事では、Java独自アノテーションを使ってBean Validationのエラーメッセージをフィールド単位で出し分ける方法を解説しました。
- カスタムアノテーションを定義し
messageKey属性を持たせるだけでYAML/プロパティファイルからメッセージを引ける LocalValidatorFactoryBeanを明示的に定義することでSpringのMessageSourceと連携できる- ソースを修正せずにメッセージファイルだけで文言変更・多言語対応が可能に
この記事を通して、メッセージ変更による再ビルド・再デプロイの手間をゼロにし、保守性を大幅に向上させられました。
次回は、同手法を@EmailValidや@PhoneValidなどに拡張し、共通ライブラリとして社内で共有する方法を紹介します。
参考資料
- Spring Framework Reference - Validation
- Hibernate Validator 8 docs
- Javaカスタムアノテーション徹底解説 ――Bean Validation編
