検証アノテーションを合成する

Created: 2020-04-07

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のように、意味を持つ名前を簡単に与えることができ、モデルの意図をより表現しやすくなります。

参考

avatar

Written by yo1000 | YO!CHI KIKUCHI
Loves 🌱 Spring, 🦢 Pelikan fountain pen and 🦁 FINAL FANTASY VIII