概要
Java のアノテーションは、コードにメタデータを付与する仕組みです。@Override や @Deprecated といった標準アノテーションは日常的に目にしますが、独自のアノテーションを定義して実行時に処理する方法となると、一気にハードルが上がります。この記事では、カスタムアノテーションの定義から @Retention・@Target の設定、リフレクションで実行時に値を読み取る方法までを、2つの実装例で整理します。1つ目はメソッドに @TestCase を付けて自動実行する簡易テストランナー、2つ目はフィールドに @NotNull を付けて null チェックを行うバリデーション処理です。いずれも外部ライブラリなしで動作する完結したコードで、アノテーション処理の基本パターンを押さえられます。
使いどころ
共通基盤で独自の @Validate アノテーションを定義し、DTO のフィールドに付けるだけで入力チェックを自動化する
バッチ処理のステップに @Step(order=1) のようなアノテーションを付け、実行順序をメタデータから動的に組み立てる
社内フレームワークで @Endpoint(path="/api/users") を定義し、ルーティングテーブルをアノテーションスキャンで自動生成する
コード例
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
public class CustomAnnotationDemo {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface TestCase {
String description() default "テストケース";
int priority() default 1;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface NotNull {
String message() default "null は許可されていません";
}
static class CalculatorTest {
@TestCase(description = "正の数の加算", priority = 1)
void testAddPositive() {
System.out.println(" 3 + 5 = " + (3 + 5));
}
@TestCase(description = "ゼロの加算", priority = 2)
void testAddZero() {
System.out.println(" 0 + 5 = " + (0 + 5));
}
void notATest() {
System.out.println(" テスト対象外");
}
}
static void runTests(Class<?> testClass) throws Exception {
System.out.println("=== テスト実行: " + testClass.getSimpleName() + " ===");
var instance = testClass.getDeclaredConstructor().newInstance();
for (var method : testClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(TestCase.class)) {
var tc = method.getAnnotation(TestCase.class);
System.out.println("[Priority " + tc.priority() + "] "
+ method.getName() + " - " + tc.description());
method.invoke(instance);
}
}
}
static class UserForm {
@NotNull(message = "ユーザー名は必須です")
String username;
String email; // アノテーションなし
UserForm(String username, String email) {
this.username = username;
this.email = email;
}
}
static void validate(Object obj) throws IllegalAccessException {
System.out.println("=== バリデーション: "
+ obj.getClass().getSimpleName() + " ===");
for (var field : obj.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(NotNull.class)) {
field.setAccessible(true);
var value = field.get(obj);
var annotation = field.getAnnotation(NotNull.class);
if (value == null) {
System.out.println(" NG [" + 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();
// バリデーション実行
validate(new UserForm("田中太郎", "[email protected]"));
validate(new UserForm(null, "[email protected]"));
}
}Version Coverage
var で記述を簡潔にできる。record と組み合わせてバリデーション結果を不変オブジェクトで返すパターンが書きやすい。
// Java 17: var で簡潔に、record でバリデーション結果を表現
var instance = testClass.getDeclaredConstructor().newInstance();
for (var method : testClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(TestCase.class)) {
var tc = method.getAnnotation(TestCase.class);
System.out.println(tc.description());
method.invoke(instance);
}
}Library Comparison
注意点
@Retention(RetentionPolicy.RUNTIME) を付け忘れると、実行時にアノテーションを取得できない。デフォルトは CLASS(.class には残るが実行時取得不可)なので、リフレクションで使う場合は必ず RUNTIME を指定する
@Target を省略するとあらゆる要素に付与可能になり、意図しない箇所への付与を防げない。METHOD、FIELD、TYPE など対象を明示するのが安全
アノテーションの属性に指定できる型は限定されている(プリミティブ、String、Class、enum、アノテーション、およびそれらの配列のみ)。List や Map は使えない
getDeclaredMethods の返却順序は JVM の実装に依存し、ソースコード上の記述順とは限らない。実行順序が重要な場合は priority 属性などで明示的に制御すること
FAQ
リフレクションで実行時に読むなら RUNTIME、バイトコード解析ツール向けなら CLASS、コンパイル時チェックやコード生成のみなら SOURCE です。業務で自作する場合はほぼ RUNTIME 一択です。
default を省略した属性は、アノテーション使用時に必ず値を指定する必要があります。省略するとコンパイルエラーになるため、任意指定にしたい場合は default を付けてください。
Java 8 以降、@Repeatable を付けたアノテーションは同一要素に複数付与できます。コンテナアノテーション(配列を持つアノテーション)の定義が必要です。