akishin999の日記

調べた事などを書いて行きます。

CGLIB で実行時にクラスにメソッドを追加する

CGLIB を使って実行時に新たにメソッドを追加する方法を調べた時のメモです。


Code Generation Library - Code Generation Library
http://cglib.sourceforge.net/


使用した Jar は以下のバージョンのものです。

  • cglib-nodep-2.1_3.jar


バイトコード拡張なので、出来るだろうとは思っていたのですが、意外と情報が少なくて調べるのに時間がかかってしまいました。


以下のサンプルでは、Date 型の setCreatedAt しか持たない JavaBean に対して、文字列を引数として同名のメソッドを呼び出せるようにしています。

ポイントは InterfaceMaker を使ってインターフェースの定義をして、Enhancer で作成したインターフェースを元のクラスに追加してやるところになります。

package example;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;

import net.sf.cglib.asm.Type;
import net.sf.cglib.core.Signature;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.InterfaceMaker;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class CGLibExample {

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        // 新規インターフェースを定義する
        InterfaceMaker im = new InterfaceMaker();
        // 文字列を引数とした setCreatedAt を定義
        im.add(new Signature("setCreatedAt", Type.VOID_TYPE, new Type[] { Type
                .getType(String.class) }), null);

        // インターフェースを生成
        Class myInterface = im.create();

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(ExampleBean.class);
        // 生成したインターフェースを追加する
        enhancer.setInterfaces(new Class[] { myInterface });
        enhancer.setCallback(new MethodInterceptor() {
            public Object intercept(Object obj, Method method, Object[] args,
                    MethodProxy proxy) throws Throwable {
                
                ExampleBean bean = (ExampleBean) obj;
                
                // 文字列を引数とした setCreatedAt が呼ばれた場合に Date 型に変換し本来の Setter を呼び出す。
                if (method.getName().startsWith("setCreatedAt")
                        && args[0] != null && args[0] instanceof String) {

                    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
                    Date date = null;
                    try {
                        date = sdf.parse((String) args[0]);
                    } catch (final Exception e) { /* nop */ }
                    bean.setCreatedAt(date);
                    return null;

                }
                return proxy.invokeSuper(obj, args);
            }
        });

        // Bean を生成
        ExampleBean bean = (ExampleBean) enhancer.create();
        bean.setId(999);

        // 実行時に型を追加しているため、呼び出しはリフレクション経由
        try {
            // 追加したメソッドはあくまで CGLIB によって作成された型にしか存在しないため、
            // ExampleBean.class ではなく、bean.getClass() のようにして指定する必要がある。
            Method method = bean.getClass().getMethod("setCreatedAt", new Class[] {String.class});
            method.invoke(bean, new Object[]{"20100531"});
        } catch (final Exception e) {
            e.printStackTrace();
        }
        
        System.out.printf("id : [%d] createdAt : [%s]\n", bean.getId(), bean.getCreatedAt());
    }
}

/**
 * サンプル用の JavaBeans
 */
class ExampleBean implements Serializable {
    private static final long serialVersionUID = -8121418052209958014L;
    
    private int id;
    private Date createdAt;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }
}


実行すると以下のように日付が設定されていることがわかります。

id : [999] createdAt : [Mon May 31 00:00:00 JST 2010]


実行時に追加しているため、通常のメソッドのように呼び出す事は出来ず、常にリフレクション経由になってしまうのですが、フレームワーク内部などでは元々 BeanUtils などを使ってたりする事も多いと思うので、いろいろと使い道はありそうです。


また、Spring Framework などを使っていると依存関係で cglib が最初から含まれているのも気軽に試せていいですね。

参考

RE: Using cglib to expand classes/interfaces with n ew methods - msg#00002 - java.cglib.devel
http://osdir.com/ml/java.cglib.devel/2005-01/msg00002.html