エンハンスされた型のメソッドとアノテーション
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を自分で拡張してしまうってのも良いかもしれない)