Online Books:
java.net on MarkMail:
Search |
||
BeansBinding: not only for the GUI?Posted by fabriziogiudici on January 13, 2008 at 8:02 AM PST
"Beans binding" is, in general, any Java technology that allows to automatically bind some properties in a plain JavaBean class to a GUI form. There have been libraries around for beans binding since a few years, but the thing is getting more and more interest because of JSR-295, an official specificaion from Sun together with its reference implementation ("Beans Binding in capitals) that is also supported by NetBeans 6.
Don't worry: this is not Yet Another Tutorial on NetBeans and BeansBindings since there are already around (even though it seems there's nothing about the use of JSR-295 with NetBeans RCP projects). My point is about using Beans Binding for other purposes than merely bind a form - you know that I like to use stuff also for different things than the ones it has been designed for. Just be warned that this is only work in progress at the time. The context: blueMarine Metadata Let me start with the context, which is a new sub-project of blueMarine which is called blueMarine Metadata and, as it appears obvious from its name, it has the responsibility for managing metadata of photos. In a few words, the requirements are:
![]() I'm particularly sensible about reusability as this metadata stuff will evolve in some interesting stuff in the next months, but I'm not going to tell you more about it now (yes, this is a little teaser ;-). At the moment I would just like to point out that the NetBeans integrated support for Beans Binding is great for writing GUI forms quickly. ![]() Common design patterns Since this module is being developed mostly by TDD and I have a very good test coverage, I can afford to do some experimenting, especially in the area of persistence. Let's first look at this diagram: ![]() The upper part of the diagram shows a very common pattern that people use, probably influenced by some J2EE blueprints:
Beans Binding for Persistence And now my point. Beans Binding is basically used as an alternate approach to implement MVC for the GUI and the Business objects without requiring you to write specific listeners; instead, Beans Binding takes advantage of the standard java.beans.PropertyChangeListener implemented in the Java runtime, posing as an additional requirement that all the involved objects are compliant with the JavaBeans specification (remember that a "real" JavaBean is not the same as a POJO, as it must have proper event notification upon every property change). This is an annoyance that I'll address at the end of this post, but I anticipate that it's just a boilerplate code issue, not a design problem.But if the relationship between Business and Persistence can be expressed as a kind-of-MVC, why don't we use Beans Binding to take care of the relationship between Business and Persistence too? It's basically what I've done so far in the first sketch of the blueMarine Metadata subproject, as explained by the upper part of this diagram: ![]() Now Form, Domain Object and Persistence are pretty much decoupled each from the other - it's up to two special services, the UI Binder and the Persistence Binder, to use JSR 295 for keeping them synchronized. Any change in any object will properly reflect into the others: this happens both if the user types something in the form (and the update is propagated to the Business and Persistence objects) and if something changes in Persistence (and the update is propagated to the Business and the Form); of course changes could also originate in the Business Object (e.g. a timer triggers) and the other two are updated. Keep in mind that the Business Object mustn't be a passive thing such as a Value Object, but it can contain business logic too (i.e. a property change might originate some computing); on the other hand, the "Binders" are Services that need to be invoked only once, to set up bindings, and then they don't have a further role in the evolution of the object states. I like this approach since it seems to overcome the classic scheme of "active-Services" versus "passive-Objects", instead here each object is active and its design is only driven by the domain model. Also, in the example the Persistence object seems to be modeled on the Domain Bean (same properties), but this is not a constraint. For instance, in blueMarine I have implemented a persistent module where every property from every JavaBean is storead as the tuple ("metadata group", "metadata property name", "value") in a single table. Beans Binding gives you a lot of flexibility. Two problems There are two problems in this design, both related to the specific technology available now:
package it.tidalwave.metadata.persistence.jpa; A similar approach could be used for the AWT Thread Decorator. If you're thinking of AOP, yes, we are basically implementing an aspect here. But I prefer to do it manually with CGLIB rather than resorting to AspectJ for simplicity, as I don't want to add another complex library (and a new language) to my project. What about boilerplate code? It's known that properly implementing JavaBeans with all the listener stuff is a PITA. While for the UI stuff you could just extend the AbstractBean class from SwingLabs, and 90% of the work is done, this is not a viable solution for the Business and the Persistence objects for two reasons:
The current state of this stuff is that tests pass, but the thing doesn't work in production yet, since CGLIB has some conflicts with the NetBeans RCP classloader. This is something that I hope to fix soon. There are other specific desing issues that I'm dealing with, most notably another module of blueMarine where I'm trying to apply this scheme but it falls short of a specific case. But this is just preliminary work, expect updates from me in future. In the meantime, I'd like to have some feedback from you. PS If this works, I wonder wheter the JSR-295 package name org.jdesktop.beansbinding is somewhat inappropriate, since the scope of the technology goes beyond the desktop. Perhaps org.beans.beansbinding would be better?Technorati Tags: beansbinding, JSR-295, NetBeans »
Comments
Comments are listed in date ascending order (oldest first)
Submitted by fabriziogiudici on Tue, 2008-01-22 14:03.
Thank you for pointing to Spin! I'm doing a similar stuff in these days and maybe it can save me time. I'm going to try it.
Submitted by guette on Tue, 2008-01-22 07:03.
Hi interesting post, personnaly I used aspectJ to intercept setter and insturment add/removeProperty change listener. It allows to have configurable point cut matching easily configurable with Spring.
For Event Dispatch Thread issues I use Spin
Submitted by pupmonster on Mon, 2008-01-14 07:48.
Fabrizio,
Excellent article. We did the a similar thing for PropertyChangeSupport as follows:
public class BeanProxyFactory {
public static T createPropertyChangeProxy(T bean) {
return (T) Enhancer.create(bean.getClass(),
new Class[] {PropertyChangeListenerSupport.class},
new PropertyChangeMethodInterceptor(bean));
}
}
public interface PropertyChangeListenerSupport {
void addPropertyChangeListener(String propertyName, PropertyChangeListener listener);
void addPropertyChangeListener(PropertyChangeListener listener);
void removePropertyChangeListener(String propertyName, PropertyChangeListener listener);
void removePropertyChangeListener(PropertyChangeListener listener);
}
public class PropertyChangeMethodInterceptor implements MethodInterceptor, PropertyChangeListenerSupport {
private static final String SET_PREFIX = "set";
private static final String GET_PREFIX = "get";
private static final String IS_PREFIX = "is";
private PropertyChangeSupport propertyChangeSupport;
private Object source;
public PropertyChangeMethodInterceptor(Object source) {
this.source = source;
propertyChangeSupport = new PropertyChangeSupport(source);
}
public Object intercept(Object object, Method method, Object[] methodParameters, MethodProxy proxy) throws Throwable {
// check for property change methods
if (Arrays.asList(PropertyChangeListenerSupport.class.getMethods()).contains(method)) {
return method.invoke(this, methodParameters);
}
// if the call is to a setter, we need to fire a property change
if (method.getName().startsWith(SET_PREFIX)) {
// determine the name of the property
StringBuilder propertyName = new StringBuilder(method.getName().substring(SET_PREFIX.length()));
Method getMethod = null;
// try to get a method with a "get" prefix. if that doesn't work, try an "is" prefix
try {
getMethod = object.getClass().getMethod(new StringBuilder().append(GET_PREFIX).append(propertyName.toString()).toString());
}
catch (NoSuchMethodException e) {
// might be a boolean get method with an "is" prefix
getMethod = object.getClass().getMethod(new StringBuilder().append(IS_PREFIX).append(propertyName.toString()).toString());
}
// get the old value
Object old = getMethod.invoke(object, (Object[]) null);
// change first letter to lower case
propertyName.replace(0, 1, propertyName.substring(0, 1).toLowerCase());
// fire a property change
propertyChangeSupport.firePropertyChange(propertyName.toString(), old, methodParameters[0]);
}
return method.invoke(source, methodParameters);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
propertyChangeSupport.addPropertyChangeListener(listener);
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
propertyChangeSupport.removePropertyChangeListener(listener);
}
public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
propertyChangeSupport.removePropertyChangeListener(propertyName, listener);
}
}
The idea is to pass a class to the factory and get back a class with PropertyChangeSupport built in. A one-liner.
It is clearly a matter of preference and philosophy, but this is for our purposes an attractive alternative to AOP. The main thing is, this took a lot of the pain out of the beansBinding framework. As you mentioned, it does require using CGLIB. Steve
Submitted by fabriziogiudici on Mon, 2008-01-14 23:55.
Thank you for the contribution, it will save me some time :-) BTW, I've fixed my NetBeans problems with CGLIB and clarified some parts of my idea, as soon as I can find the time for blogging I'll post a follow-up.
Submitted by jarppe2 on Tue, 2008-01-15 02:47.
Nice utility Steve, thanks.
I took the liberty to enhance it a bit. First, I added support for "write-only" properties, then I changed the order so that the setter is called on the bean before the property change event is fired, and finally, I added support for "setter chaining", so you can have beans like this: class Bean { public Bean setOne(int newValue) { one = newValue; return this; } public Bean setTwo(int newValue) { two = newValue; return this; } private int one; private int two; } Bean b = new Bean(); b.setOne(1).setTwo(2); Here's the PropertyChangeMethodInterceptor with my changes: import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; final class PropertyChangeMethodInterceptor implements MethodInterceptor, PropertyChangeListenerSupport { // // Life-cycle: // PropertyChangeMethodInterceptor(Object bean) { this.bean = bean; propertyChangeSupport = new java.beans.PropertyChangeSupport(bean); } // // MethodIntercepror API: // public Object intercept(Object object, Method method, Object[] methodParameters, MethodProxy proxy) throws Throwable { // If property change listener method is called, pass it to 'this': if (propertyChangeListenerSupportMethods.contains(method)) { return method.invoke(this, methodParameters); } // If method other than setter is called, pass it to bean: if (!method.getName().startsWith(SET_PREFIX)) { // No, pass the call to bean. return method.invoke(bean, methodParameters); } // // Setter called. // // Try to get the old value, call the setter, and finally fire a property change event: // // Get the property name: StringBuilder methodSuffix = new StringBuilder(method.getName().substring(SET_PREFIX.length())); // Get the old value, or use null if bean does'nt have a getter. Method getMethod = getGetter(object.getClass(), methodSuffix); Object oldValue = (getMethod != null) ? getMethod.invoke(object) : null; // Call the setter: Object returnValue = method.invoke(bean, methodParameters); // If the setter returns the bean itself, return the wrapped object instead. This // provides support for "setter chaining". if (returnValue == bean) returnValue = object; // Change the first letter to lower case and fire property change: methodSuffix.setCharAt(0, Character.toLowerCase(methodSuffix.charAt(0))); propertyChangeSupport.firePropertyChange(methodSuffix.toString(), oldValue, methodParameters[0]); // Return what ever the setter returned: return returnValue; } // // java.beans.PropertyChangeListenerSupport API: // public void addPropertyChangeListener(java.beans.PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(listener); } public void addPropertyChangeListener(String propertyName, java.beans.PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(propertyName, listener); } public void removePropertyChangeListener(java.beans.PropertyChangeListener listener) { propertyChangeSupport.removePropertyChangeListener(listener); } public void removePropertyChangeListener(String propertyName, java.beans.PropertyChangeListener listener) { propertyChangeSupport.removePropertyChangeListener(propertyName, listener); } // // Private stuff: // /** * Get the getter for given property, or <code>null</code> if bean does not support * getter for given property. * * @param beanClass <code>Class</code> of the bean * @param propertyName Property name * @return Getter method, or <code>null</code> if getter is not supported. */ private Method getGetter(Class<?> beanClass, StringBuilder propertyName) { // Try the 'get' getter first: try { return beanClass.getMethod(new StringBuilder().append(GET_PREFIX).append(propertyName).toString()); } catch (NoSuchMethodException e) { // No 'get' getter, proceed... } // Try 'is' getter: try { return beanClass.getMethod(new StringBuilder().append(IS_PREFIX).append(propertyName).toString()); } catch (NoSuchMethodException e1) { // No 'is' getter either, proceed... } // Return null indicating that this bean does not have a getter for this property. return null; } private final java.beans.PropertyChangeSupport propertyChangeSupport; private final Object bean; private static final List<Method> propertyChangeListenerSupportMethods = Arrays.asList(PropertyChangeListenerSupport.class.getMethods()); private static final String SET_PREFIX = "set"; private static final String GET_PREFIX = "get"; private static final String IS_PREFIX = "is"; } -jarppe |
||
|
|