java-recipes

ホーム リフレクション › R-02

R-02: カスタムアノテーションの定義・処理

@interface でカスタムアノテーションを定義し、 リフレクションを使って実行時に処理する方法を学びます。 JUnit の @Test や Spring の @Autowired がどのように動いているかを理解できます。

カスタムアノテーションとは

アノテーション(Annotation)とは、コードに付加情報(メタデータ)を埋め込む仕組みです。@Override@Deprecated などは Java 標準のアノテーションですが、@interface を使うと独自のアノテーションを定義できます。

アノテーション定義に必要なメタアノテーション

  • @Retention: アノテーションをいつまで保持するかを指定します。 リフレクションで処理するには RetentionPolicy.RUNTIME が必須です。
  • @Target: アノテーションをどこに付与できるかを制限します。ElementType.METHOD(メソッド)・ElementType.FIELD(フィールド)・ElementType.TYPE(クラス・インターフェース)などを指定できます。

定義したアノテーションは、リフレクションのisAnnotationPresent() で存在確認、getAnnotation() で値の取得ができます。 この仕組みを使うと、JUnit のようなテストランナーやバリデーションフレームワークを自作できます。

サンプルコード

CustomAnnotationSample.java
import java.lang.annotation.*;
import java.lang.reflect.Method;

public class CustomAnnotationSample {

    // カスタムアノテーション定義
    // @Retention: アノテーションをいつまで保持するか
    //   RUNTIME  → 実行時にリフレクションで取得可能(今回のケース)
    //   CLASS    → .class ファイルに残るが実行時取得不可
    //   SOURCE   → コンパイル時のみ(コード生成ツール用)
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD) // メソッドにのみ付与可能
    @interface TestCase {
        String description() default "テストケース";
        int priority() default 1; // 優先度(1=高、2=中、3=低)
    }

    // バリデーション用アノテーション
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD) // フィールドにのみ付与可能
    @interface NotNull {
        String message() default "null は許可されていません";
    }

    // アノテーションを付けたクラス
    static class CalculatorTest {

        @TestCase(description = "正の数の加算", priority = 1)
        void testAddPositive() {
            int result = 3 + 5;
            System.out.println("  testAddPositive: 3 + 5 = " + result);
        }

        @TestCase(description = "ゼロの加算", priority = 2)
        void testAddZero() {
            int result = 0 + 5;
            System.out.println("  testAddZero: 0 + 5 = " + result);
        }

        void notATest() {
            System.out.println("  このメソッドはテストではありません");
        }
    }

    // アノテーションを処理するテストランナー(JUnit の仕組みの簡易版)
    static void runTests(Class<?> testClass) throws Exception {
        System.out.println("=== テスト実行: " + testClass.getSimpleName() + " ===");
        Object instance = testClass.getDeclaredConstructor().newInstance();

        for (Method method : testClass.getDeclaredMethods()) {
            if (method.isAnnotationPresent(TestCase.class)) {
                TestCase annotation = method.getAnnotation(TestCase.class);
                System.out.println("[Priority " + annotation.priority() + "] "
                        + method.getName() + " - " + annotation.description());
                method.invoke(instance);
            }
        }
    }

    // フィールドアノテーションのバリデーション
    static class User {
        @NotNull(message = "ユーザー名は必須です")
        String username;

        String email; // アノテーションなし

        User(String username, String email) {
            this.username = username;
            this.email = email;
        }
    }

    static void validate(Object obj) throws IllegalAccessException {
        for (java.lang.reflect.Field field : obj.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(NotNull.class)) {
                field.setAccessible(true);
                Object value = field.get(obj);
                NotNull annotation = field.getAnnotation(NotNull.class);
                if (value == null) {
                    System.out.println("バリデーションエラー [" + field.getName() + "]: "
                            + annotation.message());
                } else {
                    System.out.println("バリデーション OK [" + field.getName() + "]: " + value);
                }
            }
        }
    }

    public static void main(String[] args) throws Exception {
        // テストランナーの実行
        runTests(CalculatorTest.class);

        System.out.println();

        // バリデーションの実行
        System.out.println("=== バリデーション ===");
        validate(new User("田中太郎", "taro@example.com"));
        validate(new User(null, "taro@example.com"));
    }
}

Java 8 からカスタムアノテーションを定義できます。@Retention(RetentionPolicy.RUNTIME) を付けることで実行時にリフレクションで取得できます。@Target でアノテーションを付与できる場所(メソッド・フィールド・クラスなど)を制限できます。

よくあるミス・注意点

⚠️ @Retention を省略するとリフレクションで取得できない

@Retention を省略した場合、デフォルトはRetentionPolicy.CLASS になります。 これは .class ファイルには残りますが、実行時(RUNTIME)には取得できません。 リフレクションで処理するアノテーションには必ず@Retention(RetentionPolicy.RUNTIME) を付けましょう。

⚠️ アノテーション要素にはデフォルト値を設定するか、使う側で必ず指定する

アノテーション要素に default を付けていない場合、 使う側でその要素を省略するとコンパイルエラーになります。 必須の情報は default なしで定義し、省略可能な情報は default 値を設定するのが一般的です。

⚠️ アノテーション要素の型は制限がある

アノテーション要素に使える型は、プリミティブ型・String・Class・Enum・アノテーション型・これらの配列に限定されています。ListObject などは使えないため、複数の値を渡したいときは配列を使います。

テストする観点

  • @TestCase を付けたメソッドだけがrunTests() によって実行されること
  • アノテーションに設定した descriptionpriority の値が正しく取得できること
  • default 値が指定されている要素を省略したとき、デフォルト値が使われること
  • @NotNull を付けたフィールドに null を設定したとき、バリデーションエラーが出力されること
  • @NotNull を付けたフィールドに null 以外の値を設定したとき、OK が出力されること(境界値)
  • アノテーションが付いていないフィールドがバリデーション対象にならないこと

GitHub でソースコードを見る →