Background:
As it is known, Hibernate uses accessors (getters/setters) to access object properties. This is noted as a RIGHT THING in documentation, since it raises incapsulation. That's right, but I have a few real-world examples that show that direct field access can be (highly) desirable.
Problem scenarios:
1. We're working on a relatively large web application. Hibernate works fine, but there is a problem:
There are some legacy modules in the system which sometimes provide not completely correct data to the system database. They have direct access to the database, and it cannot be changed right now.
At the same time, we in our application use business objects with setters like
public void setEmail(String email) { Assert.validEmail(email); this.email = email.trim(); }
What happens when legacy stores invalid email (for example) into the system? We unable not only to load the broken object via Hibernate, but also to load a list of objects via HibernateSession.find(), since it breaks too as soon as it attempts to load the object.
2. Another application has a kind of public forum in it, with search ability. It has also built-in filter that prohibits to post the message if the body contains any of restricted words. The filter itself shall take each single word from the body and compare it with set of restricted words. It takes (quite small) time, but the operation repeats every time we load a message, and it becomes real performance hit when search engine loads a bunch of messages to index them.
3. Third application has a lot of POD (plain old data) objects. They has no any logic associated with them (there are special services instead), and the structure of these objects is stable for a very long time. It looks unconvinient to write getters and setters to satisfy a library.
We might have various workarounds in first two cases (like having another set of setters for direct access, or moving fields in superclass), but all of them are ugly and add a portion of mess into code instead of making it cleaner.
In short: field access can be beneficial in terms of stability, peformance and code simplicity.
Patch:
I decided that both access strategies are applicable in various situation, and that there shall be opportunity to choose. At the same time, I understand that there is a lot of written code that relays on setter/getters for Hibernate usage, and having global switch "now we use reflect.Field" is not enough: we need a co-existance, so even existing projects (like ours) may switch new objects to field access while leaving old ones with setters.
A best decision would be to make the option class-wide (via *.xml configuration), but I started with a more simple approach: I created new configuration variable
hibernate.access.order fields-then-accessors
When it absent or has value "accessors-then-fields" the things are very like current state: when accessing property "foo" Hibernate will search for method getFoo (isFoo/setFoo) in the object and it's superclasses (so, all existing project will work as usual).
If, thought, it cannot find the method, it will search the field "foo" in the object and it's superclasses.
When the variable set to "fields-then-accessors", the order is opposite: the fields are scanned first, and methods second.
All this functionality is hidden inside ReflectHelper (Getter/Setter), and completely transparent for the rest of Hibernate.
Main changes are in ReflectHelper, with minor changes in AbstractPersister and another class (I've forgot the name ;) ).
I also added tests for the functionality.
I'm attached the changed source, but it's just for reference (to let to see my coding level etc etc etc). I worked over CVS copy got via anonymous CVS. I can commit my changes back if someone will grant commit right to user tut-framework. I could create a new branch for that.
Comments?
dozen
P.S. There is still unresolved problem with proxy classes that accepts Method instead of Getter; it might be resolved quite easy, as I believe, but it changes the interface ClassPersister.
//$Id: ReflectHelper.java,v 1.13 2003/08/03 01:15:28 oneovthafew Exp $ package net.sf.hibernate.util;
import java.beans.Introspector; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import net.sf.cglib.MetaClass; import net.sf.hibernate.AssertionFailure; import net.sf.hibernate.HibernateException; import net.sf.hibernate.MappingException; import net.sf.hibernate.PropertyAccessException; import net.sf.hibernate.PropertyNotFoundException; import net.sf.hibernate.cfg.Environment; import net.sf.hibernate.type.Type; import net.sf.hibernate.type.TypeFactory; public final class ReflectHelper { static final Log log = LogFactory.getLog(ReflectHelper.class); private static final Class[] NO_CLASSES = new Class[0]; private static final Class[] OBJECT = new Class[] { Object.class }; private static final Method OBJECT_EQUALS; static Boolean accessFieldsFirst_ = null; public static boolean tryFieldsFirst() { if( accessFieldsFirst_ == null ) { String val = (String)Environment.getProperties().get(Environment.PROPERTIES_ACCESS_ORDER); log.info(Environment.PROPERTIES_ACCESS_ORDER+" property is "+val); if( val == null || val.trim().equalsIgnoreCase("accessors-then-fields") ) { accessFieldsFirst_ = new Boolean(false); } else { accessFieldsFirst_ = new Boolean(true); } log.info("fields-then-methods="+accessFieldsFirst_); } return accessFieldsFirst_.booleanValue(); } private static class Accessor { protected Class clazz; protected final Method method; protected final Field field; protected final String propertyName; protected Accessor(Class c,Method m,Field f,String name) { clazz = c; method = m; field = f; propertyName = name; if( method != null ) method.setAccessible(true); if( field != null ) field.setAccessible(true); } /** * @deprecated */ public Method getMethod() { return method; } /** * @deprecated */ public String getName() { return method != null? method.getName() : field.getName(); } } public static final class Setter extends Accessor { Setter(Class clazz, Method method, String propertyName) { super(clazz, method, null, propertyName); } Setter(Class clazz, Field field, String propertyName) { super(clazz, null, field, propertyName); } public void set(Object target, Object value) throws HibernateException { try { if( method != null ) { method.invoke( target, new Object[] { value } ); } else { field.set(target,value); } } catch (NullPointerException npe) { if ( value==null && method.getParameterTypes()[0].isPrimitive() ) { throw new PropertyAccessException(npe, "Null value was assigned to a property of primitive type", true, clazz, propertyName); } else { throw new PropertyAccessException(npe, "NullPointerException occurred while calling", true, clazz, propertyName); } } catch (InvocationTargetException ite) { throw new PropertyAccessException(ite, "Exception occurred inside", true, clazz, propertyName); } catch (IllegalAccessException iae) { throw new PropertyAccessException(iae, "IllegalAccessException occurred while calling", true, clazz, propertyName); //cannot occur } catch (IllegalArgumentException iae) { if ( value==null && method.getParameterTypes()[0].isPrimitive() ) { throw new PropertyAccessException(iae, "Null value was assigned to a property of primitive type", true, clazz, propertyName); } else { log.error( "IllegalArgumentException in class: " + clazz.getName() + ", setter method of property: " + propertyName ); log.error( "expected type: " + method.getParameterTypes()[0].getName() + ", actual value: " + ( value==null ? null : value.getClass().getName() ) ); throw new PropertyAccessException(iae, "IllegalArgumentException occurred while calling", true, clazz, propertyName); } } } } public static final class Getter extends Accessor { Getter(Class clazz, Method method, String propertyName) { super(clazz,method,null,propertyName); } Getter(Class clazz, Field field, String propertyName) { super(clazz,null,field,propertyName); } public Object get(Object target) throws HibernateException { try { if( method != null ) { return method.invoke(target, null); } else { return field.get(target); } } catch (InvocationTargetException ite) { throw new PropertyAccessException(ite, "Exception occurred inside", false, clazz, propertyName); } catch (IllegalAccessException iae) { throw new PropertyAccessException(iae, "IllegalAccessException occurred while calling", false, clazz, propertyName); //cannot occur } catch (IllegalArgumentException iae) { log.error( "IllegalArgumentException in class: " + clazz.getName() + ", getter method of property: " + propertyName ); throw new PropertyAccessException(iae, "IllegalArgumentException occurred calling", false, clazz, propertyName); } } public Class getReturnType() { return method != null? method.getReturnType():field.getType(); } } public static Setter getSetter(Class theClass, String propertyName) throws PropertyNotFoundException { Setter result = getSetterOrNull(theClass, propertyName); if (result==null) throw new PropertyNotFoundException( "Could not find a setter for property " + propertyName + " in class " + theClass.getName() ); return result; } public static Getter getGetter(Class theClass, String propertyName) throws PropertyNotFoundException { Getter result = getGetterOrNull(theClass, propertyName); if (result==null) throw new PropertyNotFoundException( "Could not find a getter for " + propertyName + " in class " + theClass.getName() ); return result; } private static Setter getSetterOrNull(Class theClass, String propertyName) { if (theClass==Object.class || theClass==null) return null; if( tryFieldsFirst() ) { // fields-then-accessors try { Field field = theClass.getDeclaredField(propertyName); return new Setter(theClass,field,propertyName); } catch( Exception ex ) { } Method method = setterMethod(theClass, propertyName); if(method!=null) { return new Setter(theClass, method, propertyName); } } else { // accessors-then-fields Method method = setterMethod(theClass, propertyName); if(method!=null) { return new Setter(theClass, method, propertyName); } try { Field field = theClass.getDeclaredField(propertyName); return new Setter(theClass,field,propertyName); } catch( Exception ex ) { } } // scan superclasses and interfaces Setter setter = getSetterOrNull( theClass.getSuperclass(), propertyName ); if (setter==null) { Class[] interfaces = theClass.getInterfaces(); for ( int i=0; setter==null && i<interfaces.length; i++ ) { setter=getSetterOrNull( interfaces[i], propertyName ); } } return setter; } private static Getter getGetterOrNull(Class theClass, String propertyName) { if (theClass==Object.class || theClass==null) return null; if( tryFieldsFirst() ) { // fields-then-accessors try { Field field = theClass.getDeclaredField(propertyName); return new Getter(theClass,field,propertyName); } catch( Exception ex ) { } Method method = getterMethod(theClass, propertyName); if(method!=null) { return new Getter(theClass, method, propertyName); } } else { // accessors-then-fields Method method = getterMethod(theClass, propertyName); if(method!=null) { return new Getter(theClass, method, propertyName); } try { Field field = theClass.getDeclaredField(propertyName); return new Getter(theClass,field,propertyName); } catch( Exception ex ) { } } Getter getter = getGetterOrNull( theClass.getSuperclass(), propertyName ); if (getter==null) { Class[] interfaces = theClass.getInterfaces(); for ( int i=0; getter==null && i<interfaces.length; i++ ) { getter=getGetterOrNull( interfaces[i], propertyName ); } } return getter; } private static Method setterMethod(Class theClass, String propertyName) { Getter getter = getGetterOrNull(theClass, propertyName); Class returnType = (getter==null) ? null : getter.getReturnType(); Method[] methods = theClass.getDeclaredMethods(); Method potentialSetter = null; for (int i=0; i<methods.length; i++) { if( ( methods[i].getName().length() > 3 ) && ( methods[i].getName().startsWith("set") ) ) { String testStdMethod = Introspector.decapitalize( methods[i].getName().substring(3) ); String testOldMethod = methods[i].getName().substring(3); if ( ( testStdMethod.equals(propertyName) || testOldMethod.equals(propertyName) ) && ( methods[i].getParameterTypes().length==1 ) ) { potentialSetter = methods[i]; if ( returnType==null || methods[i].getParameterTypes()[0].equals(returnType) ) return potentialSetter; } } } return potentialSetter; } private static Method getterMethod(Class theClass, String propertyName) { Method[] methods = theClass.getDeclaredMethods(); for (int i=0; i<methods.length; i++) { // only carry on if the method has no parameters if(methods[i].getParameterTypes().length==0) { // try "get" if( (methods[i].getName().length() > 3) && methods[i].getName().startsWith("get") ) { String testStdMethod = Introspector.decapitalize( methods[i].getName().substring(3) ); String testOldMethod = methods[i].getName().substring(3); if( testStdMethod.equals(propertyName) || testOldMethod.equals(propertyName) ) return methods[i]; } // if not "get" then try "is" if( (methods[i].getName().length() > 2) && methods[i].getName().startsWith("is") ) { String testStdMethod = Introspector.decapitalize( methods[i].getName().substring(2) ); String testOldMethod = methods[i].getName().substring(2); if( testStdMethod.equals(propertyName) || testOldMethod.equals(propertyName) ) return methods[i]; } } } return null; } public static Type reflectedPropertyType(Class theClass, String name) throws MappingException { return TypeFactory.hueristicType( getGetter(theClass, name).getReturnType().getName() ); } public static Class classForName(String name) throws ClassNotFoundException { try { return Thread.currentThread().getContextClassLoader().loadClass(name); } catch (Exception e) { return Class.forName(name); } } public static boolean isPublic(Class clazz, Member member) { return Modifier.isPublic( member.getModifiers() ) && Modifier.isPublic( clazz.getModifiers() ); } public static Object getConstantValue(String name) { Class clazz; try { clazz = classForName( StringHelper.qualifier(name) ); } catch(ClassNotFoundException cnfe) { return null; } try { return clazz.getField( StringHelper.unqualify(name) ).get(null); } catch (Exception e) { return null; } } public static Constructor getDefaultConstructor(Class clazz) throws PropertyNotFoundException { if (isAbstractClass(clazz)) return null; try { Constructor constructor = clazz.getDeclaredConstructor(NO_CLASSES); if (!isPublic(clazz, constructor)) { constructor.setAccessible(true); } return constructor; } catch (NoSuchMethodException nme) { throw new PropertyNotFoundException( "Object class " + clazz.getName() + " must declare a default (no-argument) constructor" ); } } public static boolean isAbstractClass(Class clazz) { int modifier = clazz.getModifiers(); return (Modifier.isAbstract(modifier) || Modifier.isInterface(modifier)); } public static MetaClass getMetaClass(Class clazz, String[] getterNames, String[] setterNames, Class[] types) { try { MetaClass optimizer = MetaClass.getInstance( clazz.getClassLoader(), clazz, getterNames, setterNames, types ); if ( !clazz.isInterface() ) { //test out the optimizer: optimizer.setPropertyValues( optimizer.newInstance(), optimizer.getPropertyValues( optimizer.newInstance() ) ); } //if working: return optimizer; } catch (Throwable t) { log.info( "reflection optimizer disabled for: " + clazz.getName() + ", " + StringHelper.unqualify( t.getClass().getName() ) + ": " + t.getMessage() ); return null; } } private ReflectHelper() {} static { Method eq; try { eq = Object.class.getMethod("equals", OBJECT); } catch (Exception e) { throw new AssertionFailure("Could not find Object.equals()", e); } OBJECT_EQUALS = eq; } public static boolean overridesEquals(Class clazz) { Method equals; try { equals = clazz.getMethod("equals", OBJECT); } catch (NoSuchMethodException nsme) { return false; //its an interface so we can't really tell anything... } return !OBJECT_EQUALS.equals(equals); } }