作成 2005/1/12
EclipseもそろそろJ2SE5対応になってきたので、J2SE5の新機能に、いろいろ触りはじめてみる。まずはメタデータから。
J2SE5から、Javaソースコードにアノテーションを使って、メタデータを埋め込むことができるようになりました。
メタデータは、データに関するデータです。Javaでいうと、Javaソースコード自体についてのデータになります。英語で、メタ○○というと○○をあらわす○○という風の意味らしいです(メタモデルはモデルのモデル、メタメタモデルはモデルのモデルのモデル)。
Javaソースコードにメタデータを埋め込むには、アノテーション(注釈)を記述します。例えば、似たような仕組みに、JavaDocのタグがあります。@param、@returnといった記述がアノテーション。アノテーションによって埋め込まれたデータがメタデータになります。
ソースコードにメタデータを埋め込む仕組みは、いろいろと便利で、XDocletなどのツールでよく利用されていました。しかし、JavaDocにドキュメントではないものをいろいろ埋め込むのもナニですし、ツールによって、書式が違うのは後で困るかもということで、JSR-175でJavaの標準仕様になりました。
ソースコードのメタデータは、もともと.NETなどでよく利用されていた仕組みらしく、EoDに役立ちます。なお、現在、JDKで提供されているメタデータの仕組みは、「単にアノテーションを埋め込む」仕組みだけなので、アプリケーション開発者が使えるのようになるには、埋め込まれたアノテーションをどうこうする、というツールの登場が必要になります(一応、コンパイラやJavaDocジェネレータでちょっとした機能は提供されています。またリフレクションでメタデータを読み取ることはできます)。
ここでは、簡単なアノテーションを作成し、リフレクションで読み込んでみます。
アノテーションクラス(インターフェイス)はこれです。interfaceの前に@がついているのがアノテーションだよ、ということを表します。なお、@Retentionは必須ではありませんが、リフレクションで読み込む場合は指定する必要があります(指定しないデフォルトでは、バイトコードにアノテーションは組み込まれない)。
package test1;
import java.lang.annotation.*;;
@Retention(RetentionPolicy.RUNTIME)
public @interface Hoge {
    public String value1();
    
    public String value2();
}
次のクラスは普通のクラスですが、@でクラスにアノテーションを付加しています。Hogeインターフェイスで指定したメンバが、アノテーションの引数で指定できます。アノテーションをサポートしたIDEであれば、指定できるできないのチェックぐらいはしてくれるでしょう(Eclipse3.1は一応できるようです)。
package test1;
@Hoge(value1="m1",value2="m2")
public class Main {
    public static void main(String[] args) {
        Hoge hoge = Main.class.getAnnotation(Hoge.class);
        System.out.println(
                "value1=" + hoge.value1()
                + ",value2=" + hoge.value2()
        );
    }
}
実行すると、こうなります。
value1=m1,value2=m2
もう一歩進んで、アノテーション記述で、アスペクトみたいなものをかけてみましょう。
リフレクションで実行時にアノテーションをひろって、あれこれします。これをメソッド実行をアノテーションにしただけです。
アノテーションを読み込むところはこんな感じになります。Method#getAnnotatios()してるだけですが。。。なお、ダイナミックプロキシでinvokeにわたってくるMethodは、ターゲットクラスでなく、プロキシインターフェイスのMethodなので、ターゲットクラスにアノテーションをかける場合は、そのクラスのメソッドをひろってくる必要があります(ここではそうしています)。
AnnotationProxy.java
package test2;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class AnnotationProxy implements InvocationHandler{
    private Object target;
    public static Object newInstance(Object obj) {
        return java.lang.reflect.Proxy.newProxyInstance(
            obj.getClass().getClassLoader(),
            obj.getClass().getInterfaces(),
            new AnnotationProxy(obj));
    }
    private AnnotationProxy(Object obj) {
        this.target = obj;
    }
    public Object invoke(Object proxy, Method m, Object[] args)
        throws Throwable {
        Object result;
        try {
            Method targetMethod = target.getClass().getMethod(m.getName(), (Class[])m.getParameterTypes());
            
            //アノテーションの種類によりあれこれする
            Annotation[] annotations = targetMethod.getAnnotations();
            for(int i=0; i<annotations.length; i++){
                Annotation annotation = annotations[i];
                if(annotation.annotationType() == Log.class){
                    System.out.println("before");
                }else{
                    //...
                }
            }
            result = m.invoke(target, args);
            return result;
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        } catch (Throwable e) {
            
            throw e;
        } finally {
            
        }
    }
    
}
次はJavassistを使った例です。クラスロード時にアノテーションを読み込んであれこれしています。
実際にアノテーションをよむところは前の例と同じです。Javassistよく分かってないので、使い方変だったらごめんなさい。
AnnotationedLoader.java
package test3;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
public class AnnotationedLoader extends ClassLoader{
    
    protected synchronized Class< ? > loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
    
        Class original = super.loadClass(name, resolve);
        
        if(name.startsWith("java.")){
            return original;
        }
        try{
            ClassPool pool = ClassPool.getDefault();
            CtClass cc = pool.get(name); 
            
            Method[] methods = original.getMethods();
            for(int i=0; i<methods.length; i++){
                Method method = methods[i];
                Annotation[] annotations = method.getAnnotations();
                for(int j=0; j<annotations.length; j++){
                    Annotation annotation = annotations[j];
                    if(annotation.annotationType() == Log.class){
                        CtMethod ctMethod = cc.getDeclaredMethod(method.getName());
                        ctMethod.insertBefore("System.out.println(\"before\");");
                    }else{
                        //...
                    }
                }
            }
            
            Class clazz = cc.toClass();
            return clazz;
            
        }catch(Exception e){
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }
}
アスペクトにソースコードメタデータを利用するのは、ちょっちアドホックな方法です。ポイントカットできませんし。まあ、簡単に書けるのは便利なので、アノテーションをサポートしたAOPフレームワークもいくつか出てきているようです。
あと、普通のアプリケーション開発者は自分でアノテーションを書くのでなく、ツール屋さんが作ったアノテーションを利用することの方が多いでしょう。あと、ここでは実行時のアノテーションの例を行いましたが、どっちかというと、コンパイル時などにあれこれするツールの方が主流になってくるかもしれません(いや、よくわからんけど)。
J2SE虎の穴 Metadata は魔法の言葉
http://www5.airnet.ne.jp/sakuraba/java/laboratory/J2SE1.5/LangSpec/Metadata/Metadata.html
dW Tigerでのアノテーション第1回: Javaコードにメタデータを追加する
http://www-6.ibm.com/jp/developerworks/java/041001/j_j-annotate1.pdf
JAVA言語入門 アノテーション
http://wisdom.sakura.ne.jp/programming/java/java5_9.html
JDK5 APIリファレンス
http://java.sun.com/j2se/1.5.0/ja/docs/ja/api/java/lang/annotation/package-summary.html