2008年6月23日月曜日

GuiceでAOPする

「Modelのsetter全部に対してPropertyChangeEventを発火するコードを書きたくない」という目的で、これをGuiceのAOPの機能を使って実装してみる。

Guice的なAOP

まずはソースを見る方がわかりやすい。SetterInterceptorというclassがInterceptor。
    Injector injector = Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
            // 対象は以下。
            // ClassMather: AbstractModel classのSubClass
            // MethodMatche: "set"で始まるMethod
            bindInterceptor(Matchers.subclassesOf(AbstractModel.class), new AbstractMatcher<Method>() {
                public boolean matches(Method t) {
                    return t.getName().startsWith("set");
                }
            }, new SetterInterceptor());
        }
    });

ポイントはまず以下のメソッド。

  • bindInterceptor(
    Matcher<? super Class<?>> classMatcher, Matcher<? super Method> methodMatcher, MethodInterceptor... interceptors)
対象のClassの絞り込み条件と、対象Methodの絞り込み条件、Interceptor、を渡す。もうひとつのポイントは以下のクラス。
  • Matcher
これはMatchersというclassに便利なstatic Methodが用意されていて、それを使用する事もできるし、自分で記述する事もできる。ここでは、Classの指定には「AbstractModelのSubClass」と指定するために「Matchers.subclassOf()」を使い、Methodの指定には「setで始まるMethod名を持つMethod」を指定するために自分でMatcherClassを作成した(AbstractMatcher<T>を継承した匿名Class)。

今回は自分で作ったMatcherも使用しているが、Matchersには以下のようなものも定義されている。

  • 特定のPackage内のものを指定するinPackage()
  • 特定のannotationで修飾されたものを指定するannotatedWith()
  • 引数に指定したインスタンスとequals()が成立したインスタンスのみ反応するonly()。
  • 上記のonly()は「equals()」での判断だが、「==」で判断するのがidenticalTo()。
  • 返り値の型を指定するreturns()

この程度でAOPできるが、試したソースをこの下に書いておく。

PropertyChangeEventを発火するInterceptorの実装

Guiceでは、AOP Allianceに準拠したInterceptorが使える。Guice専用のInterceptorもある(com.google.inject.cglib.proxy.MethodInterceptor)。ここではAOP Allianceに準拠した実装を行ってみる。
import java.beans.PropertyChangeEvent;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class SetterInterceptor implements MethodInterceptor {

    public Object invoke(MethodInvocation invocation) throws Throwable {
        Method method = invocation.getMethod();
        String methodName = invocation.getMethod().getName();
        // setter method名からfield名を作成する。
        String fieldName = methodName.replaceFirst("set", "");
        fieldName = fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1);
        Class<?> declaringClass = method.getDeclaringClass();
        // java.lang.Fieldを取得する。
        Field field = declaringClass.getDeclaredField(fieldName);
        if (field.getModifiers() != Field.PUBLIC) {
            field.setAccessible(true);
        }
        // setterの実行と、その前後の値を取得する。
        Object oldValue = field.get(invocation.getThis());
        Object result = invocation.proceed();
        Object newValue = field.get(invocation.getThis());

        try {
            // "firePropertyChange"固定のmethod名でEventの発火を実行する。
            Method fireMethod = declaringClass.getMethod("firePropertyChange", PropertyChangeEvent.class);
            fireMethod.invoke(invocation.getThis(), new PropertyChangeEvent(invocation.getThis(), field.getName(),
                    oldValue, newValue));
        } catch (NoSuchMethodException ex) {
            // do nothing.
        }
        return result;
    }

}

PropertyChangeEventを発するModelClass

もちろん、setter内ではPropertyEventのfireはしない。
  • package model;
    
    import java.beans.PropertyChangeEvent;
    import java.beans.PropertyChangeListener;
    import java.util.ArrayList;
    import java.util.List;
    
    public abstract class AbstractModel {
        private String name;
    
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
    
        List<PropertyChangeListener> listeners = new ArrayList<PropertyChangeListener>();
    
        public void addListener(PropertyChangeListener l) {
            listeners.add(l);
        }
        
        public void removeListener(PropertyChangeListener l) {
            listeners.remove(l);
        }
        
        public void firePropertyChange(PropertyChangeEvent event) {
            for (PropertyChangeListener l : listeners) {
                l.propertyChange(event);
            }
        }
    }
  • package model;
    
    public class Column extends AbstractModel {
        public String type;
    
        public Column() { }
    
        public String getType() { return type; }
        public void setType(String type) { this.type = type; }
    }
  • package model;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class Table extends AbstractModel {
        private List<Column> columns = new ArrayList<Column>();
        private String propertyString;
    
        public Table() { }
    
        public List<Column> getColumns() { return columns; }
        public void setColumns(List<Column> columns) { this.columns = columns; }
    
        public String getPropertyString() { return propertyString; }
        public void setPropertyString(String propertyString) { this.propertyString = propertyString; }
    }
コメントを投稿