メタデータ

作成 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


TOP