Meta-Annotations について

このエントリは、アイディアや情報の提供を呼びかけるためのものです。

結局何がしたいのか

自前のAnnotationProcessorでMeta-Annotation的な機能をサポートしたい。

で、そもそもMeta-Annotationってこんなんで良いんだっけ?というエントリです。

解決したい問題

同じアノテーションの組合せを沢山のクラスに設定したくない

アノテーションの合成

こういう二つのアノテーションがあったとして

public @interface Transactional {}
public @interface Service {}

これらを一つにまとめるアノテーションを定義したい。

@Service
@Transactional
public @interface TransactionalService {}

で、これを使うコードと使ってないコードが同じであるとみなしたい。

@TransactionalService
public class MyService {}

@Service
@Transactional
public class MyService {}

値の設定を伴う合成

値を設定するケースでも同じように書きたい。以下のようなアノテーションを定義する。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface RuntimeType {}

こういう風に使いたい。

@RuntimeType
public @interface MyTypeAnnotation {}

@RuntimeTypeを使った宣言は、以下の宣言と同じであるとみなしたい。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTypeAnnotation {}

ここでの重要なポイントは、@Target@Retentionではデフォルト値が存在しないが、@RuntimeType宣言時に必要な値を埋め込んでいることである。

汎用的な定義を具体的な定義として読み替える

以下のような使い方も想定できる。

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;

import java.lang.annotation.Target;

@Target({ TYPE, METHOD })
public @interface Transactional {
  TxType value();
  enum TxType {
    REQUIRED, REQUIRES_NEW, MANDATORY, SUPPORTS, NOT_SUPPORTED, NEVER;
  }
}

@Transactionalはデフォルト値の無い汎用的な宣言だが、値の設定を伴わない具体的な宣言に定義しなおしたい。

import static tx.Transactional.TxType.REQUIRED;
@Transactional(REQUIRED)
public @interface TxRequired {}

つまり、MyTxServiceMySimpleTxServiceは同じように解釈されてほしい。

import static tx.Transactional.TxType.REQUIRED;

@Transactional(REQUIRED)
public class MyTxService {}
@TxRequired
public class MySimpleTxService {}

値の上書きを伴う合成

public @interface Range {
  long min() default Long.MIN_VALUE;
  long max() default Long.MAX_VALUE;
}

@Rangeに対して@Positiveという値を絞り込んだアノテーションを作る。

@Range(min = 0L)
public @interface Positive {
  long max();
}

@Positiveは、maxを上書きできるが、minは上書きできない。
@Rangeでは、maxを設定しない場合、Long.MAX_VALUEがデフォルト値として適用されるが、@Positiveではmaxを設定しなければならない。

以下の例では、valとposは同じ意味をもつが、デフォルト値のある@Rangeとデフォルト値の無い@Positiveではコンパイルエラーの出力が異なる。

public class MyClass {
  @Range(min = 0, max = 200)
  long val;
  @Positive(max = 200)
  long pos;
}

発生する問題

値がコンフリクトする

@Positiveとは逆のアノテーションを作成する。

@Range(max = 0L)
public @interface Negative {
  long min();
}

以下のように宣言できてしまう。

public class Conflicts {
  @Positive(max = 100)
  @Negative(min = -100)
  long val;
}

これは、以下のように解釈されるべきだ。

public class Conflicts {
  @Range(min = 0, max = 100)
  @Range(min = -100, max = 0)
  long val;
}

これは二つの問題がある。

  • @RangeはRepeatableではないが、暗黙裡にRepeatしている。
    • 展開した状態で評価することでコンパイルエラーとして検出すべき
  • minとmaxの値がそれぞれ矛盾している。どの値を採用すべきか?
    • 値が矛盾しているが果たしてエラーか?

値をマージして良いか不明瞭なケース

二つのアノテーションがある。

import static java.lang.annotation.ElementType.*;
@java.lang.annotation.Target({ TYPE, METHOD })
public @interface TypeAndMethod {}
import static java.lang.annotation.ElementType.*;
@java.lang.annotation.Target({ TYPE, FIELD, PARAMETER })
public @interface FieldAndArgs {}

これらを使って新しいアノテーションを作る。

@TypeAndMethod
@FieldAndArgs
public @interface WideTarget {}

@Targetアノテーションのvalueをマージして良いか不明瞭である。@Merge等のマーカを定義するのだろうか。

定義が循環するケース

アノテーションが相互参照していると解決が終わらなくなる可能性がある。

@Boros
public @interface Ouro {}
@Ouro
public @interface Boros {}

解決の階層にリミットを設けるか、循環を検知してエラーにする必要がある。

どうやって集約するのか

既存のやり方

Groovy

@groovy.transform.AnnotationCollectorを宣言すると、そこを起点に集約処理が為される。

CDI

@Stereotypeがあり一緒に宣言されているアノテーションが集約される

Spring4

リフレクションベースの実装で、何でも合成できるようになっている。
処理コストが随分と高そうな印象。

TomEEの中の人のアイディア

dblevins の書いたエントリでは@Metatypeが付いてるものをアノテートするとまとめられる。

Bean Validation

BeanValidationでは@OverridesAttributeを使うと値の上書きと共に、当該アノテーションに宣言されているアノテーションの継承のような事ができる。

つまり、Bean Validationでは Constraintを宣言する際には合成できるようTargetにANNOTATION_TYPEを付けることが望ましいのか。