开发者

Create simple POJO classes (bytecode) at runtime (dynamically)

I've the following scenario..

I am writing some tool that run user-entered query against the database and return the result..

The simplest way is to return the result as: List<String[]> but I need to take this a step further.

I need to create (at runtime) some POJO (or DTO) with some name and create fields and setters and getters for it and populate it with the data returned and then return it to the user among with the .class file generated...

So the idea here is How to Create simple class(bytecode) at runtime (dynamically) I do a basic search and found many li开发者_StackOverflow中文版b including Apache BCEL But I think I need something more simpler...

What do you think of that?

Thanks.


Creating a simple POJO with getters and setters is easy if you use CGLib:

public static Class<?> createBeanClass(
    /* fully qualified class name */
    final String className,
    /* bean properties, name -> type */
    final Map<String, Class<?>> properties){

    final BeanGenerator beanGenerator = new BeanGenerator();

    /* use our own hard coded class name instead of a real naming policy */
    beanGenerator.setNamingPolicy(new NamingPolicy(){
        @Override public String getClassName(final String prefix,
            final String source, final Object key, final Predicate names){
            return className;
        }});
    BeanGenerator.addProperties(beanGenerator, properties);
    return (Class<?>) beanGenerator.createClass();
}

Test code:

public static void main(final String[] args) throws Exception{
    final Map<String, Class<?>> properties =
        new HashMap<String, Class<?>>();
    properties.put("foo", Integer.class);
    properties.put("bar", String.class);
    properties.put("baz", int[].class);

    final Class<?> beanClass =
        createBeanClass("some.ClassName", properties);
    System.out.println(beanClass);
    for(final Method method : beanClass.getDeclaredMethods()){
        System.out.println(method);
    }

}

Output:

class some.ClassName
public int[] some.ClassName.getBaz()
public void some.ClassName.setBaz(int[])
public java.lang.Integer some.ClassName.getFoo()
public void some.ClassName.setFoo(java.lang.Integer)
public java.lang.String some.ClassName.getBar()
public void some.ClassName.setBar(java.lang.String)

But the problem is: you have no way of coding against these methods, as they don't exist at compile time, so I don't know what good this will do you.


Well This may also given a try. But I need to understand this if anyone can explain.

UPDATE :

Imagine your application have to create Java POJO instances dynamically at runtime from some external configuration. This task can be easily done using one of the bytecode manipualtion library. This post demonstrates how this can be done using Javassist library.

Assume we have following configuration for the properties our dynamically created POJO should contain:

Map<String, Class<?>> props = new HashMap<String, Class<?>>();
props.put("foo", Integer.class);
props.put("bar", String.class);

Let’s write a PojoGenerator, that dynamically generates a Class object for the given class name and a map, containing required properties:

import java.io.Serializable;
import java.util.Map;
import java.util.Map.Entry;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.NotFoundException;

public class PojoGenerator {

public static Class generate(String className, Map<String, Class<?>>  properties) throws NotFoundException,
        CannotCompileException {

    ClassPool pool = ClassPool.getDefault();
    CtClass cc = pool.makeClass(className);

    // add this to define a super class to extend
    // cc.setSuperclass(resolveCtClass(MySuperClass.class));

    // add this to define an interface to implement
    cc.addInterface(resolveCtClass(Serializable.class));

    for (Entry<String, Class<?>> entry : properties.entrySet()) {

        cc.addField(new CtField(resolveCtClass(entry.getValue()), entry.getKey(), cc));

        // add getter
        cc.addMethod(generateGetter(cc, entry.getKey(), entry.getValue()));

        // add setter
        cc.addMethod(generateSetter(cc, entry.getKey(), entry.getValue()));
    }

    return cc.toClass();
}

private static CtMethod generateGetter(CtClass declaringClass, String fieldName, Class fieldClass)
        throws CannotCompileException {

    String getterName = "get" + fieldName.substring(0, 1).toUpperCase()
            + fieldName.substring(1);

    StringBuffer sb = new StringBuffer();
    sb.append("public ").append(fieldClass.getName()).append(" ")
            .append(getterName).append("(){").append("return this.")
            .append(fieldName).append(";").append("}");
    return CtMethod.make(sb.toString(), declaringClass);
}

private static CtMethod generateSetter(CtClass declaringClass, String fieldName, Class fieldClass)
        throws CannotCompileException {

    String setterName = "set" + fieldName.substring(0, 1).toUpperCase()
            + fieldName.substring(1);

    StringBuffer sb = new StringBuffer();
    sb.append("public void ").append(setterName).append("(")
            .append(fieldClass.getName()).append(" ").append(fieldName)
            .append(")").append("{").append("this.").append(fieldName)
            .append("=").append(fieldName).append(";").append("}");
    return CtMethod.make(sb.toString(), declaringClass);
}

private static CtClass resolveCtClass(Class clazz) throws NotFoundException {
    ClassPool pool = ClassPool.getDefault();
    return pool.get(clazz.getName());
}
}

That’s it!

Using PojoGenerator is quite simple. Let’s generate some POJO, output via reflection all its methods, set and then get some property:

public static void main(String[] args) throws Exception {

Map<String, Class<?>> props = new HashMap<String, Class<?>>();
props.put("foo", Integer.class);
props.put("bar", String.class);

Class<?> clazz = PojoGenerator.generate(
        "net.javaforge.blog.javassist.Pojo$Generated", props);

Object obj = clazz.newInstance();

System.out.println("Clazz: " + clazz);
System.out.println("Object: " + obj);
System.out.println("Serializable? " + (obj instanceof Serializable));

for (final Method method : clazz.getDeclaredMethods()) {
    System.out.println(method);
}

// set property "bar"
clazz.getMethod("setBar", String.class).invoke(obj, "Hello World!");

// get property "bar"
String result = (String) clazz.getMethod("getBar").invoke(obj);
System.out.println("Value for bar: " + result);

}

Executing above will result in the following console output:

Clazz: class net.javaforge.blog.javassist.Pojo$Generated
Object: net.javaforge.blog.javassist.Pojo$Generated@55571e
Serializable? true
public void     net.javaforge.blog.javassist.Pojo$Generated.setBar(java.lang.String)
public java.lang.String     net.javaforge.blog.javassist.Pojo$Generated.getBar()
public java.lang.Integer     net.javaforge.blog.javassist.Pojo$Generated.getFoo()
public void     net.javaforge.blog.javassist.Pojo$Generated.setFoo(java.lang.Integer)
Value for bar: Hello World!


I have used ASM for this in the past. What I like is the ASMifier which can create code to generate a class. e.g. I create a generic POJO in java code with one field of every type in Java and use ASMifier to create the Java code to create this from byte code and used it as a template to generate an arbitary POJO.

As @Michael suggests, you may want to add a non-reflective way to get arbitary fields. e.g.

public Set<String> fieldNames();
public Object getField(String name);
public void setField(String name, Object name);

Why do you want to do this? There are ways you can make use a Map<String, Object> style objects more efficient than using a regular map.

Another approach is to generate the Java source using Velocity and compile the code using the Compiler API. Its a pain to use so I wrote a wrapped for it here Essence JCF The only read advantage to using this approach is that you can easily debug your generated code. (The library has the option of saving the java code to somewhere the debugger can find it so when you step into the generated code)


What would the caller do with a class that is generated on the fly and which his code therefore cannot know? The only way to access it would be via reflection. Returning a List<String[]> or Map<String, String> would actually be a much cleaner and more usable design.


I also hate writing getters and setters. I would prefer to use POJOs, even POJOs declared as nested classes.

There is another way to do this, even with old servers and technology and without introducing Springs (we use JBoss 4.2 and JBoss's incomplete EJB 3.0). Extending the org.apache.commons.beanutils.BeanMap, you can wrap the POJO in a bean map, and when you get or put you can manipulate the fields using reflection. If the getter or setter does not exist we just use field manipulation to get it. Obviously it's NOT a real bean, so that's perfectly OK.

package com.aaa.ejb.common;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Set;

import org.apache.commons.beanutils.BeanMap;
import org.apache.commons.collections.set.UnmodifiableSet;
import org.apache.log4j.Logger;

/**
 * I want the bean map to be able to handle a POJO. 
 * @author gbishop
 */
public final class NoGetterBeanMap extends BeanMap {

    private static final Logger LOG = Logger.getLogger(NoGetterBeanMap.class);

    /**
     * Gets a bean map that can handle writing to a pojo with no getters or setters.
     * @param bean
     */
    public NoGetterBeanMap(Object bean) {
        super(bean);
    }

    /* (non-Javadoc)
     * @see org.apache.commons.beanutils.BeanMap#get(java.lang.Object)
     */
    public Object get(Object name) {
        Object bean = getBean();
        if ( bean != null ) {
            Method method = getReadMethod( name );
            if ( method != null ) {
                try {
                    return method.invoke( bean, NULL_ARGUMENTS );
                }
                catch (  IllegalAccessException e ) {
                    logWarn( e );
                }
                catch ( IllegalArgumentException e ) {
                    logWarn(  e );
                }
                catch ( InvocationTargetException e ) {
                    logWarn(  e );
                }
                catch ( NullPointerException e ) {
                    logWarn(  e );
                }
            } else {
                if(name instanceof String) {
                    Class<?> c = bean.getClass();
                    try {
                        Field datafield = c.getDeclaredField( (String)name );
                        datafield.setAccessible(true);
                        return datafield.get(bean);
                    } catch (SecurityException e) {
                        throw new IllegalArgumentException( e.getMessage() );
                    } catch (NoSuchFieldException e) {
                        throw new IllegalArgumentException( e.getMessage() );
                    } catch (IllegalAccessException e) {
                        throw new IllegalArgumentException( e.getMessage() );
                    }
                }
            }
        }
        return null;
    }

    /* (non-Javadoc)
     * @see org.apache.commons.beanutils.BeanMap#put(java.lang.Object, java.lang.Object)
     */
    public Object put(Object name, Object value) throws IllegalArgumentException, ClassCastException {
        Object bean = getBean();
        if ( bean != null ) {
            Object oldValue = get( name );
            Method method = getWriteMethod( name );
            Object newValue = null;
            if ( method == null ) {
                if(name instanceof String) {//I'm going to try setting the property directly on the bean.
                    Class<?> c = bean.getClass();
                    try {
                        Field datafield = c.getDeclaredField( (String)name );
                        datafield.setAccessible(true);
                        datafield.set(bean, value);
                        newValue = datafield.get(bean);
                    } catch (SecurityException e) {
                        throw new IllegalArgumentException( e.getMessage() );
                    } catch (NoSuchFieldException e) {
                        throw new IllegalArgumentException( e.getMessage() );
                    } catch (IllegalAccessException e) {
                        throw new IllegalArgumentException( e.getMessage() );
                    }
                } else {
                    throw new IllegalArgumentException( "The bean of type: "+ 
                            bean.getClass().getName() + " has no property called: " + name );
                }
            } else {
                try {
                    Object[] arguments = createWriteMethodArguments( method, value );
                    method.invoke( bean, arguments );
                    newValue = get( name );
                } catch ( InvocationTargetException e ) {
                    logInfo( e );
                    throw new IllegalArgumentException( e.getMessage() );
                } catch ( IllegalAccessException e ) {
                    logInfo( e );
                    throw new IllegalArgumentException( e.getMessage() );
                }
                firePropertyChange( name, oldValue, newValue );
            }
            return oldValue;
        }
        return null;
    }

    /* (non-Javadoc)
     * @see org.apache.commons.beanutils.BeanMap#keySet()
     */
    public Set keySet() {
        Class<?> c = getBean().getClass();
        Field[] fields = c.getDeclaredFields();
        Set<String> keySet = new HashSet<String>(super.keySet());
        for(Field f: fields){
            if( Modifier.isPublic(f.getModifiers()) && !keySet.contains(f.getName())){
                keySet.add(f.getName());
            }
        }
        keySet.remove("class");
        return UnmodifiableSet.decorate(keySet);
    }
}

The tricky part is factoring up the POJOs to return back, but reflection can help you there:

/**
* Returns a new instance of the specified object.  If the object is a bean, 
* (serializable, with a default zero argument constructor), the default 
* constructor is called.  If the object is a Cloneable, it is cloned, if the
* object is a POJO or a nested POJO, it is cloned using the default 
* zero argument constructor through reflection.  Such objects should only be
* used as transfer objects since their constructors and initialization code
* (if any) have not have been called. 
* @param obj
* @return A new copy of the object, it's fields are blank.
*/
public static Object constructBeanOrPOJO(final Object obj) {

    Constructor<?> ctor = null;
    Object retval = null;
    //Try to invoke where it's Serializable and has a public zero argument constructor.
    if(obj instanceof Serializable){
        try {
            ctor = obj.getClass().getConstructor((Class<?>)null);
            if(ctor.isAccessible()){
                retval = ctor.newInstance();
                //LOG.info("Serializable class called with a public constructor.");
                return retval;
            }
        } catch (Exception ignoredTryConeable) {
        }
    }
    //Maybe it's Clonable.
    if(obj instanceof Cloneable){
        try {
            Method clone = obj.getClass().getMethod("clone");
            clone.setAccessible(true);
            retval = clone.invoke(obj);
            //LOG.info("Cloneable class called.");
            return retval;
        } catch (Exception ignoredTryUnNestedClass) {
        }
    }
    try {
        //Maybe it's not a nested class.
        ctor = obj.getClass().getDeclaredConstructor((Class<?>)null);
        ctor.setAccessible(true);
        retval = ctor.newInstance();
        //LOG.info("Class called with no public constructor.");
        return retval; 
    } catch (Exception ignoredTryNestedClass) {
    }
    try {
       Constructor[] cs = obj.getClass().getDeclaredConstructors();
       for(Constructor<?> c: cs){
          if(c.getTypeParameters().length==0){
             ctor = c;
             ctor.setAccessible(true);
             retval = ctor.newInstance();
             return retval;
          }
       }
       //Try a nested class class.
       Field parent = obj.getClass().getDeclaredField("this$0");
       parent.setAccessible(true);
       Object outer = (Object) parent.get(obj);
       //ctor = (Constructor<? extends Object>) obj.getClass().getConstructors()[0];//NO, getDECLAREDConstructors!!!
       ctor = (Constructor<? extends Object>)         obj.getClass().getDeclaredConstructor(parent.get(obj).getClass());
       ctor.setAccessible(true);
       retval = ctor.newInstance(outer);
       //LOG.info("Nested class called with no public constructor.");
       return retval;
    } catch (Exception failure) {
        throw new IllegalArgumentException(failure);
    }
}

Example code to get a generic bean from a bean, a cloneable, or a POJO:

public List<Object> getGenericEJBData(String tableName, Object desiredFields, Object beanCriteria){
    NoGetterBeanMap desiredFieldMap = new NoGetterBeanMap(desiredFields);
    NoGetterBeanMap criteriaMap = new NoGetterBeanMap(beanCriteria);
    List<Object> data = new ArrayList<Object>();
    List<Map<String, Object>> mapData = getGenericEJBData(tableName, desiredFieldMap, criteriaMap);
    for (Map<String,Object> row: mapData) {
        Object bean = NoGetterBeanMap.constructBeanOrPOJO(desiredFields);//Cool eh?
        new NoGetterBeanMap(bean).putAll(row);//Put the data back in too!
        data.add(bean);
    }
    return data;
}

Example usage with EJB:

IGenericBean genericRemote = BeanLocator.lookup(IGenericBean.class);

//This is the minimum required typing.
class DesiredDataPOJO {
    public String makename="";//Name matches column and return type. 
}
class CriteriaPOJO {
    //Names match column and contains criteria values.
    public String modelname=model,yearid=year; 
}

List<DesiredDataPOJO> data = 
    genericRemote.getGenericEJBData(ACES_VEHICLE_TABLE, new DesiredDataPOJO(), new CriteriaPOJO() );
for (DesiredDataPOJO o: data) {
    makes.add(o.makename);
}

The EJB has an interface like this:

package com.aaa.ejb.common.interfaces;

import java.util.List;
import java.util.Map;

import javax.ejb.Local;
import javax.ejb.Remote;

/**
 * @see
 * http://trycatchfinally.blogspot.com/2006/03/remote-or-local-interface.html
 * 
 * Note that the local and remote interfaces extend a common business interface.
 * Also note that the local and remote interfaces are nested within the business
 * interface. I like this model because it reduces the clutter, keeps related
 * interfaces together, and eases understanding.
 * 
 * When using dependency injection, you can specify explicitly whether you want 
 * the remote or local interface.  For example:
 * @EJB(beanInterface=services.DistrictService.IRemote.class)
 * public final void setDistrictService(DistrictService districtService) {
 *    this.districtService = districtService;
 * }
 */
public interface IGenericBean {

    @Remote 
    public interface IRemote extends IGenericBean {
    }

    @Local 
    public interface ILocal extends IGenericBean {
    }

    /**
     * Gets a list of beans containing data.
     * Requires a table name and pair of beans containing the fields
     * to return and the criteria to use.
     * 
     * You can even use anonymous inner classes for the criteria.
     * EX: new Object() { public String modelname=model,yearid=year; }
     * 
     * @param tableName
     * @param fields
     * @param criteria
     * @return
     */
    public <DesiredFields> List<DesiredFields> getGenericEJBData(String tableName, DesiredFields desiredFields, Object beanCriteria);

}

You can imagine what the ejb implementation looks like, right now we are building prepared statements and calling them but we could use criteria, or something cooler like hibernate or whatever if we wanted.

Here is a rough example (some are NOT going to like this part). In the example we have a single table with data in 3rd normal form. For this to work the bean fields have to match the table column names. The toLowerCase() calls are in case this is a REAL bean being used, which will screw up the name matching (MyField vs. getMyfield). This part could probably be polished quite a bit better. In particular the order by and distinct ought to be flags or something. There are probably other edge conditions that can also happen. Of course I only have to write this ONCE, and for performance, nothing prevents you from having a much more precise receiver of the data for performance.

@Stateless
public class GenericBean implements ILocal, IRemote {

...

/* (non-Javadoc)
 * @see com.aaa.ejb.acesvehicle.beans.interfaces.IAcesVehicleBean#getGenericEJBData(java.lang.String, java.util.Map, java.util.Map)
 */
@Override
public List<Map<String, Object>> getGenericEJBData(String tableName,Map<String, Object> desiredFields, Map<String, Object> criteria){
    try {
        List<Map<String,Object>> dataFieldKeyValuePairs = new ArrayList<Map<String,Object>>();

        StringBuilder sql = new StringBuilder("SELECT DISTINCT ");
        int selectDistinctLength = sql.length();
        for(Object key : desiredFields.keySet()){
            if(desiredFields.get(key)!=null) {
                sql.append(key).append(", ");
            }
        }
        sql.setLength(sql.length()-2);//Remove last COMMA.
        int fieldsLength = sql.length();
        sql.append(" FROM ").append(tableName).append(" WHERE ");

        String sep = "";//I like this, I like it a lot.
        for(Object key : criteria.keySet()){
            sql.append(sep);
            sql.append(key).append(" = COALESCE(?,").append(key).append(") ");
            sep = "AND ";
        }
        sql.append(" ORDER BY ").append(sql.substring(selectDistinctLength, fieldsLength));

        PreparedStatement ps = connection.prepareStatement(sql.toString(), ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
        int criteriaCounter=1;
        for(Object key : criteria.keySet()){
            ps.setObject(criteriaCounter++, criteria.get(key));
        }
        ResultSet rs = ps.executeQuery();
        while (rs.next()) {
            Map<String,Object> data = new HashMap<String,Object>();
            int columnIndex = rs.getMetaData().getColumnCount();
            for(int x=0;x<columnIndex;x++){
                String columnName = rs.getMetaData().getColumnName(x+1);
                if(desiredFields.keySet().contains(columnName.toLowerCase())){
                    //Handle bean getters and setters with different case than metadata case.
                    data.put(columnName.toLowerCase(), rs.getObject(x+1));
                } else {
                    data.put(columnName, rs.getObject(x+1));
                }
            }
            dataFieldKeyValuePairs.add(data);
        }
        rs.close();
        ps.close();
        return dataFieldKeyValuePairs;
    } catch (SQLException sqle) {
        LOG.debug("National database access failed.", sqle);
        throw new EJBException(new DataSourceException("Database access failed. \n"
                + "getGenericEJBData()", sqle.getMessage()));
    }

}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜