エンハンサによるPropertyChangeSupportの自動化

今まで何話かバイトコードエンハンサに関してのネタを書いて来たが、その総括として、Swingアプリケーションに限らずよく例として挙げられる、JavaBeansのPropertyChangeSupportによるプロパティ値の変更通知を自動化してみよう。

1. PropertyChangeSupportクラスの利用
元々JavaBeansにはプロパティ値の変更を通知する処理を委譲するために、java.beans.PropertyChangeSupportクラスが提供されているので、これを利用する。

具体的には同クラスのインスタンスを扱うためのインタフェース、又は抽象クラスがあれば良いのだが、今回はインタフェースに頼らずとも自動化が出来る、というのが肝心なため、敢えて抽象クラスにする。

public abstract class AbstractBean {
    protected PropertyChangeSupport pChangeSupport;
    protected AbstractBean() {
        this.pChangeSupport = new PropertyChangeSupport(this);
    }
    public void addPropertyChangeListener(PropertyChangeListener listener) {
        this.pChangeSupport.addPropertyChangeListener(listener);
    }
    public void removePropertyChangeListener(PropertyChangeListener listener) {
        this.pChangeSupport.removePropertyChangeListener(listener);
    }
    public void firePropertyChange(String propertyName, Object oldValue,
            Object newValue) {
        this.pChangeSupport.firePropertyChange(propertyName, oldValue, newValue);
    }
}

いつもの事だが、サンプルなので必要の無い処理はばっさりと削っている。実際にはプロパティ名で分類された通知リスナの登録と解除や、登録済みのリスナをまとめて取得するメソッドがあると便利だろうか。

2.AbstractBeanの具象クラスを用意する

これはどんなのでも良いのだが、例として一つだけプロパティを持つHogeBeanクラスを用意しよう。

public class HogeBean extends AbstractBean {
    String name;
    public HogeBean () {
        super();
    }
    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

通常であれば、ここでNameプロパティのセッターであるsetNameメソッドにプロパティ通知のための処理を必ず書く必要がある。これは一般的には以下のようなコードになるだろう。

    public void setName(String name) {
        String old = this.name;
        this.name = name;
        this.firePropertyChange("Name", old, this.name);
    }

この黄色部分のコーディングをプロパティが追加される度に行う必要がある訳だが、今回はこの部分をエンハンサによる拡張された型によって自動化してしまおう、というのが目的だ。

3. JavassistのProxyFactoryによる型の拡張
今まで何度か採りあげて来たように、HogeBeanの型を拡張する。ここで重要なのは、

  • プロパティのセッターだけを捕捉の対象にする
  • 捕捉するセッターの実行前にゲッターを実行して、プロパティの変更前の値を取得する

ことである。
以下、これらを実現するコードになる。

public class PropertyChangeEnhancer {
    private static final MethodFilter filter =  new MethodFilter(){
        @Override
        public boolean isHandled(Method method) {
            return method.getName().matches("^set.*");
        }};
    private static final MethodHandler handler = new MethodHandler(){
        @Override
        public Object invoke(Object self, Method method,
                Method proceed, Object[] args) throws Throwable {
            String propertyName = method.getName().replace("set", "");
            Method getter = self.getClass().getMethod("get"+propertyName
                    , new Class[]{});
            Object preValue = null;
            if ( getter != null ) {
                //転記前の値を退避
                preValue = getter.invoke(self, new Object[]{});
            }
            //ここで捕捉したセッターを実行
            Object result = proceed.invoke(self, args);
            //変更後の値を退避
            Object postValue = getter.invoke(self, new Object[]{});
            
            AbstractBean bean = (AbstractBean)self;
            if (preValue == null) {
                if ( postValue != null ) {
                    //変更を通知
                    bean.firePropertyChange(propertyName, preValue, postValue);
                }
            } else {
                if (!preValue.equals(postValue)) {
                    //変更を通知
                    bean.firePropertyChange(propertyName, preValue, postValue);
                }
            }
            return result;
        }};
    public Class getEnhancedClass() {
        ProxyFactory factory = new ProxyFactory();
        factory.setSuperclass(HogeBean.class);
        factory.setHandler(handler);
        factory.setFilter(filter);
        return factory.createClass();
    }
}

前回、ProxyFactoryによるキャッシュの戦略を理解する必要があると書いたが、今回はそれを意識して、メソッドを捕捉するハンドラとフィルタをスタティックなフィールドで前もって定数として定義している。これにより、getEnhancedClassメソッドを何度実行しても生成される拡張されたクラスは唯一つとなる。

さて、では実行してみよう。

public static void main(String[] args) throws Exception {
    PropertyChangeEnhancer enhancer = new PropertyChangeEnhancer();
    Class enhancedClass = enhancer.getEnhancedClass();
    HogeBean hoge = (HogeBean)enhancedClass.newInstance();
    
    hoge.addPropertyChangeListener(new PropertyChangeListener(){
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            System.out.println(evt.getSource().getClass().getName() 
                    + " " + evt.getPropertyName() + " property changed : " 
                    + evt.getOldValue() + " => " + evt.getNewValue() );
        }});
    hoge.setName("Kazz");
    hoge.setName("Kazzz");
    hoge.setName("Kazzzzz");
    hoge.setName("Kazzzzz"); //値が同じなので通知されない
    hoge.setName("Kazzzzzzzz");
    hoge.setName(null);
}
:
:
実行結果
HogeBean_$$_javassist_0 Name property changed : null => Kazz
HogeBean_$$_javassist_0 Name property changed : Kazz => Kazzz
HogeBean_$$_javassist_0 Name property changed : Kazzz => Kazzzzz
HogeBean_$$_javassist_0 Name property changed : Kazzzzz => Kazzzzzzzz
HogeBean_$$_javassist_0 Name property changed : Kazzzzzzzz => null

これでインタフェースと追加コーディングに頼らずにプロパティ値の通知を自動化することができた。

Javaは元々java.reflect.Proxyクラスによって、インタフェースのメソッドを捕捉、内容を書き換えることで擬似的な型の拡張を実現できたが、実際に型自身を書き換えるためには、Jakarta BCELのような低レベルライブラリィを使ってバイトコードを直接書く必要があった。今回のようにJavassistやcglib等の高次のバイトコードエンハンサを使うことにより、Javaバイトコードの詳細な仕様を知らなくともオンザフライで型の拡張したり、インタフェースに依らないメソッド捕捉と処理挿入ができるようになったのである。(これこそが、SpringやSeasar等で実現されているAOP機能のベースになっている)

なお、実行結果を見ても解るが、Javassistにより拡張された型の名前は"_$$_javassist_+世代番号"というサフィクスが付加される。つまり、それまでクラス名を基準に何かを判定するようなコードは拡張された型を使う場合は役に立たないので、導入には注意が必要である。