Javaのアノテーションを独自で定義する

徐々に利用する機会が増えてきたアノテーションだが、アノテーションベースのフレームワークで使われているのを使ったりすることはあっても自分でアノテーションを定義したことが無かった。
「業務用フレームワーク作る際も使えそうだよなぁ」と思っていたので、今後使いこなす為にも改めてちゃんと勉強しなおしてみた。

※「アノテーション」自体は、JUnit、Jaxb、Spring等によって使い慣れていることを前提。

アノテーション

独自のアノテーションを作成する為には、以下のようなアノテーションのクラスを作成する必要がある。

@interface Annotation名{
    キーの型 キーの名前();
    ...
}

クラス名がアノテーション名、メソッド名がキー名となり、メソッドの返り値がキーに指定できる型となる。
アノテーション型はインターフェイス型と似ているが、通常のインターフェイス型に比べ、以下の制限がある。

  • extends節を書けない
  • メソッドは引数とthrows節を持てない
  • メソッドの戻り値で使えるのはプリミティブ型、String型、Class型、enum定数、アノテーション型、それらの配列、のみ
  • ジェネリクスは使えない

なお、@interfaceが指定されているクラスは、暗黙的ににjava.lang.annotation.Annotationインタフェースの継承クラスになる。

独自のアノテーション定義例
public @interface Hatena {
  int priority();
  String user();
}

public enum DiaryType {CATEGORY, DAY, NEW}
@interface Diary {
  DiaryType value();
}

上記の場合は @Hatena@Diaryアノテーションとして指定できるようになる。
この独自アノテーションを利用した例を以下に示す。

上記の独自定義クラスの利用例
@Hatena(priority = 1, user = "valice")
public class SampleClass {
    public SampleClass(){}
    @Diary(DiaryType.CATEGORY)
    public void sampleMethod(){
        ...省略
    }
}
キーが一つしか存在しないアノテーション

@Diaryの例のように、キーが一つしか存在しない場合、そのキーの名前をvalueとすることによってキー名を省略できるようになる。
キーが一つでも名前がvalueで無い場合は省略することはできない。

メタアノテーション型Target

アノテーションを定義する場合に重要な点は2つあり

  • 対象となる構文要素は何か(コンストラクタ、メソッド、ローカル変数等)
  • どのような情報を記述できるのか

を記述する必要があるが、「対象となる構文要素は何か」に関しては省略することができ、省略した場合は構文要素の制限が無い状態になる。
最初の例のアノテーション定義ではそれらを省略しており、@Hatena@Diaryアノテーションを指定できる要素であればいかなる要素に対しても付加できることになる。

アノテーションを付加できる構文要素を制限したい場合は、メタアノテーション型Targetをを付与すればよい。
@Hatenaはクラス、@Diaryはメソッドにのみ付与できるようにしたい場合、以下のようになる。

独自のアノテーション定義例にTagetを利用したもの
@Target(ElementType.TYPE)
public @interface Hatena {
  int priority();
  String user();
}

public enum DiaryType {CATEGORY, DAY, NEW}

@Target(ElementType.METHOD)
@interface Diary {
  DiaryType value();
}
ElementTypeで提供されている定数

ElementTypeは列挙型で、指定できる定数は以下。

定数名 構文要素
ElementType.ANNOTATION_TYPE アノテーション型宣言
ElementType.CONSTRUCTOR コンストラク
ElementType.FIELD フィールド宣言、enum宣言
ElementType.LOCAL_VARIABLE ローカル変数宣言
ElementType.METHOD メソッド宣言
ElementType.PACKAGE パッケージ宣言
ElementType.PARAMETER 引数宣言
ElementType.TYPE クラス宣言、インタフェース宣言、アノテーション型宣言、enum宣言

@Targetアノテーションvalueキーは配列である為、以下の様に複数指定することができる。その場合はそれら全ての構文要素に付与できるようになる。

@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Hatena {
  int priority();
  String user();
}

デフォルト値

キーが複数ある場合でも、デフォルト値を指定することによってそのキーの指定を省略することができる。

public @interface Hatena {
  int priority() default 1;
  String user();
}

@Hatena(user = "valice")
public class SampleClass {
    ...
}

マーカ・アノテーション

アノテーション型の定義においてメソッドが一つも無い場合、そのようなアノテーションマーカ・アノテーションと呼ばれる。
マーカ・アノテーションは使用する際、以下のように括弧を省略することができる。

public @interface MarkerAnnotation {}

@MarkerAnnotation
public class SampleClass {
    ...
}

これらは、標準アノテーションの@Overrideや@Deprecatedと同じ振る舞いである。(@Overrideや@Deprecatedもマーカ・アノテーションである)

その他のメタアノテーション

メタアノテーション型Targetは既に紹介したが、その他の標準メタアノテーションであるRetention、Inherited、Documentedについても記載しておく。

Retention

アノテーションの有効範囲を指定できる。

定数名 有効範囲
RetentionPolicy.CLASS アノテーションの情報をクラス・ファイルに出力するが、実行時JVMにロードされない。省略された場合のデフォルト値。
RetentionPolicy.RUNTIME アノテーションの情報をクラス・ファイルに出力し、実行時JVMにもロードされる。その為、リフレクションAPI経由でその情報にアクセスできる。
RetentionPolicy.SOURCE アノテーションの情報はソース・コード上のみに保持され、コンパイル時によって破棄される。
@Retention(RetentionPolicy.CLASS)
public @interface Hatena {
  int priority();
  String user();
}

※Retentionはキーがvalueしか無いため、キー名の省略が可能

Inherited

アノテーションを利用したクラスにおいて、そのアノテーション指定を子孫クラスに継承させることができる。
指定は必ずクラス定義に対してする必要があり、それ以外に指定した場合は無効となる。
下記の例の場合、SampleSubClassにおいても@Hatenaアノテーションが有効となっている。

@Inherited
public @interface Hatena {
  int priority();
  String user();
}

@Hatena(priority = 1, user = "valice")
public class SampleClass {
    ...
}

public class SampleSubClass extends {
    ...
}

※Inheritedはマーカ・メタアノテーションの為、括弧は省略可能

Documented

アノテーションが付いていることをドキュメントに記載するためのマーカ・メタアノテーション
Javadocなどのツールでドキュメントを生成した場合、このメタアノテーションが指定されていないアノテーションを利用しているクラスには、アノテーションを利用している旨が記載されない。

予告

今回は定義の仕方だけですが、独自のアノテーションの利用方法もそのうち書く予定です。その際はここにリンクを貼ります。