エンハンスされた型のメソッドとアノテーション

Javassistやcglibでクラスをエンハンスする場合、元の型をスーパータイプとして指定するので、拡張される前の型情報は引継がれるはずだが、アノテーションに関してはそうではないので注意が必要だ。

以下のようなアノテーションを公開しているとしよう。

@Documented
@Inherited
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataBinding {
    UpdateStrategy UpdateStrategy() default UpdateStrategy.READ_WRITE;
    String ComponentName() default "";
    String PropertyName() default "text";
    String LabelText() default "";
    Class ColumnType() default Object.class;
}

これは私がフレームワークで実際に使用している、BeansBindingを利用してJavaBeans <-> JComponent間のデータバインドを自動化するためのアノテーションだ。記述の方法は、以下のようにJavaBeansのゲッター又はセッターに行う(ゲッター/セッターのどちらか一方)

public class HogeBean {
    protected String fullName;
    public HogeBean() {
        super();
    }
    public HogeBean(String fullName) {
        this();
        this.fullName = fullName;
    }
    @DataBinding(ComponentName="fullName", LabelText="名前")
    public String getFullName() {
        return this.fullName;
    }
    public void setFullName(String fullName) {
        this.fullName = fullName;
    }
}

メタアノテーション@RetentionにRetentionPolicy.RUNTIMEを指定しているので、情報はコンパイル時に保存され、ランタイム時に参照が可能だ。アノテーションはAnnotatedElementを実装するクラスからアクセスすることが可能だが、実行時にこのDataBindingアノテーションの記述の有無を調べるには、以下のようにMethod#isAnnotationPresentメソッドを使えばよい。(ClassやFieldから取得する際も同様だ)

Method method = HogeBean.class.getMethod("getFullName", null); 
System.out.println( "DataBinding annotation present : " 
    + method.isAnnotationPresent(DataBinding.class)); 
:
実行結果
:
DataBinding annotation present : true

これは全く問題無いだろう。

次は同様にアノテーションが記述されたHogeBeanクラスをJavassistで拡張した型からメソッドのアノテーション情報を取得してみよう。スーパークラスにHogeBeanを指定しているのだから、型情報と共にアノテーションも引継がれて、上と同様の結果になると期待したのだが、

ProxyFactory factory = new ProxyFactory();
factory.setSuperclass(HogeBean.class);
Class enhancedClass = factory.createClass();
Method method = enhancedClass.getMethod("getFullName", null); 
System.out.println( "DataBinding annotation present : " 
    + method.isAnnotationPresent(DataBinding.class)); 
:
実行結果
:
DataBinding annotation present : false

と実際にはアノテーションは記述されていないという検査結果になる。

AOP loses non-spring annotations in proxied class - Spring Framework Support Forums

この辺を見るとcglibでも同じ状況のようだ。
バイトコードエンハンサのポピュラな用途としてはAOPによる実行アドバイスの編み込みがあるが、その際に拡張した型をオンザフライで生成するタイミングで、元の型のオブジェクトを雛形として必要とするケースとしないケースがある。前者の雛形を必要とするケースでは元の型情報を完璧に再現できるが、余計なオブジェクトを作るため、リソースの無駄遣いになってしまう。従って後者の拡張された型だけを使う方法がベターなのだが、既に書いた通り元の型情報が欠落するので、実際にはそうもいかないのが現実だ。

今回の問題を回避するには、

  • 元の型の雛形オブジェクトを生成して、必要に応じて使う
  • 元の型情報をどこかに退避しておいて、拡張された型から必要に応じて参照する
  • なんらかの方法で、元の型のアノテーション情報を拡張した型にコピーする

これらの方法別途必要になるだろう。

例えばJavassistであれば、javassist.bytecode.MethodInfoとjavassist.bytecode.AnnotationsAttributeを使うことによって、型に新たにアノテーションを付加できるらしいが、それには低レベルのクラス(Ct〜クラス)を扱う必要があるので、ProxyFactoryによる手軽な型の拡張が使えなくなってしまう可能性がある。(ProxyFactoryを自分で拡張してしまうってのも良いかもしれない)