JavaBeansクラスのBeans Binding対応自動化

Beans Binding(JSR 295)でJavaBeansを監視できる条件は、クラスがPropertyChangeEventをサポートしていることです。つまり

public class MyModel {
    private String name;
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

みたいな単純なPOJOは監視対象にすることはできず、

public class MyModel {
    private String name;
    private PropertyChangeSupport pcs = new PropertyChangeSupport(this);

    public void addPropertyChangeListener(PropertyChangeListener l) {
        pcs.addPropertyChangeListener(l);
    }

    public void removePropertyChangeListener(PropertyChangeListener l) {
        pcs.removePropertyChangeListener(l);
    }

    public String getName() { return name; }

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

と書き換える必要があります。こんなこといちいちやってられないので、自動化しました。

  • addPropertyChangeListener()とremovePropertyChangeListener()、firePropertyChange()の各メソッドを追加するInterType
  • 各プロパティのsetterにfirePropertyChange()の呼び出しを挿入するInterceptor

の2つを作って、これでエンハンスしたBeansをS2Containerから取得するようにしました。以下、そのソースの本体部分です。

  • PropertyChangeInterType.java
public class PropertyChangeInterType extends AbstractInterType {
    @Override
    protected void introduce() throws CannotCompileException, NotFoundException {
        introducePropertyChangeSupport();
        introduceAddPropertyChangeListener();
        introduceRemovePropertyChangeListener();
        introduceFirePropertyChange();
    }

    private void introducePropertyChangeSupport() {
        addField(Modifier.PUBLIC, PropertyChangeSupport.class, "pcs",
                CtField.Initializer.byExpr("new " + 
                        PropertyChangeSupport.class.getName() + "(this)"));
    }

    private void introduceAddPropertyChangeListener() {
        addMethod("addPropertyChangeListener", 
                new Class[] { PropertyChangeListener.class },
                "{ pcs.addPropertyChangeListener($1); }");
    }

    private void introduceRemovePropertyChangeListener() {
        addMethod("removePropertyChangeListener", 
                new Class[] { PropertyChangeListener.class },
                "{ pcs.removePropertyChangeListener($1); }");
    }

    private void introduceFirePropertyChange() {
        addMethod("firePropertyChange", 
                new Class[] { String.class, Object.class, Object.class },
                "{ pcs.firePropertyChange($1, $2, $3); }");
    }
}
  • PropertyChangeInterceptor.java
public class PropertyChangeInterceptor implements MethodInterceptor {
    public Object invoke(MethodInvocation invocation) throws Throwable {
        String methodName = invocation.getMethod().getName();
        if(!methodName.startsWith("set")) {
            return invocation.proceed();
        }
        if(invocation.getArguments().length != 1) {
            return invocation.proceed();
        }

        String propertyName = StringUtil.decapitalize(methodName.substring(3));
        BeanDesc beanDesc = BeanDescFactory.getBeanDesc(invocation.getThis().getClass());
        PropertyDesc propDesc = beanDesc.getPropertyDesc(propertyName);
        if(!propDesc.isReadable()) {
            return invocation.proceed();
        }

        Object oldValue = propDesc.getValue(invocation.getThis());
        Object result = invocation.proceed();
        Object newValue = propDesc.getValue(invocation.getThis());

        Method firePropChangeMethod = beanDesc.getMethod("firePropertyChange",
                new Class[] { String.class, Object.class, Object.class });
        MethodUtil.invoke(firePropChangeMethod, invocation.getThis(), 
                new Object[] { propertyName, oldValue, newValue });
        
        return result;
    }
}