検証アノテーションを合成する
Java でフィールドバリデーションをする場合、検証アノテーションを使うことになりますが、言語仕様によりアノテーションは派生クラスを作成できません。しかし、検証アノテーションでは近い意味合いの注釈を表現するために、既存のアノテーションを再利用したいと考えるシーンは少なくありません。そんなときにどう書くか、というのを何度か調べ直しているので残しておきます。
環境要件
- Java 8
java -version
openjdk version "1.8.0_222"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_222-b10)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.222-b10, mixed mode)
検証アノテーションの合成
知っていれば簡単で、大きくは以下2つの要件を満たすだけです。
- アノテーションに合成したいアノテーションで注釈する
- 検証用アノテーションとしての要件を満たす
@Constraint
注釈されているString message()
要素が定義されているClass<?>[] groups()
要素が定義されているClass<? extends Payload>[] payload()
要素が定義されている
これらを踏まえたサンプルコードが以下です。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Constraint(validatedBy = {})
@Min(0)
@Max(23)
@interface Hour {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Constraint(validatedBy = {})
@Min(value = 0, message = "{value}以上にしてください!!")
@Max(value = 59, message = "{value}以下にしてください!!")
@interface Minute {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
実際に使ってみると以下のようになります。
public class CompositeValidation {
public static void main(String[] args) {
System.out.println(CompositeValidation.class.getSimpleName());
Time t = new Time(26, -1); // 検証をかけるとバリデーションエラーが発生する Validation.buildDefaultValidatorFactory().getValidator().validate(t).forEach(vio -> {
System.out.println(vio.getPropertyPath());
System.out.println(vio.getMessage());
});
}
static class Time {
public Time(int hour, int minute) {
this.hour = hour;
this.minute = minute;
}
@Hour
private int hour;
@Minute
private int minute;
}
}
実行結果は以下のようになります。
CompositeValidation
4 08, 2020 3:21:31 午前 org.hibernate.validator.internal.util.Version <clinit>
INFO: HV000001: Hibernate Validator 6.1.2.Final
minute
0以上にしてください!!hour
23 以下の値にしてください
バリデーションメッセージのカスタマイズ
実はここまでの状態では、String message()
要素をアノテーションに定義しているにも関わらず、設定したメッセージがバリデーションエラー発生時に使用されません。これを解消するには、@ReportAsSingleViolation
注釈を追加します。これでアノテーションに設定したメッセージが使われるようになります。
先程のアノテーションは以下のように修正されます。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Constraint(validatedBy = {})
@ReportAsSingleViolation@Min(0)
@Max(23)
@interface Hour {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Constraint(validatedBy = {})
@ReportAsSingleViolation@Min(value = 0, message = "{value}以上にしてください!!")
@Max(value = 59, message = "{value}以下にしてください!!")
@interface Minute {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
実際に使ってみると以下のようになります。
public class CompositeValidation {
public static void main(String[] args) {
System.out.println(CompositeValidation.class.getSimpleName());
Time t = new Time(26, -1);
Validation.buildDefaultValidatorFactory().getValidator().validate(t).forEach(vio -> {
System.out.println(vio.getPropertyPath());
System.out.println(vio.getMessage());
});
}
static class Time {
public Time(int hour, int minute) {
this.hour = hour;
this.minute = minute;
}
@Hour(message = "不正な時間の入力です") private int hour;
@Minute(message = "不正な分の入力です") private int minute;
}
}
実行結果は以下のようになります。
CompositeValidation
4 08, 2020 3:36:46 午前 org.hibernate.validator.internal.util.Version <clinit>
INFO: HV000001: Hibernate Validator 6.1.2.Final
hour
不正な時間の入力ですminute
不正な分の入力です
設定済み要素を再利用したり、複数のアノテーションを合成したりできるのが確認できました。アノテーションを再利用することで、汎用的な名称のアノテーションにも、サンプルのHour
, Minute
のように、意味を持つ名前を簡単に与えることができ、モデルの意図をより表現しやすくなります。