ehlo.

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);
    }
}








Reply via email to