Создавайте простые классы POJO (байт-код) во время выполнения (динамически)
У меня есть следующий сценарий.
Я пишу какой-то инструмент, который запускает введенный пользователем запрос к базе данных и возвращает результат.
Самый простой способ - вернуть результат как: List<String[]>
, но мне нужно сделать еще один шаг.
Мне нужно создать (в время выполнения) некоторое POJO (или DTO) с некоторым именем и создать для него поля и сеттеры и геттеры и заполнить их возвращенными данными, а затем вернуть их пользователю среди файлов .class
, сгенерированных...
Итак, идея здесь заключается в том, как создать простой класс (байт-код) во время выполнения (динамически)
Я делаю базовый поиск и нашел много lib включая Apache BCEL Но я думаю, что мне нужно что-то более упрощенное...
Что вы думаете об этом?
Спасибо.
Ответы
Ответ 1
Создание простого POJO с помощью getters и seters легко, если вы используете 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();
}
Тестовый код:
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);
}
}
Вывод:
класс 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)
Но проблема в том, что у вас нет способа кодирования этих методов, поскольку они не существуют во время компиляции, поэтому я не знаю, что хорошо вам это поможет.
Ответ 2
Я использовал ASM для этого в прошлом. Мне нравится ASMifier, который может создавать код для создания класса. например Я создаю общий POJO в Java-коде с одним полем каждого типа в Java и использую ASMifier для создания кода Java для создания этого из байтового кода и использовал его в качестве шаблона для создания произвольного POJO.
Как предлагает @Michael, вы можете захотеть добавить неотражающий способ получения суточных полей. например
public Set<String> fieldNames();
public Object getField(String name);
public void setField(String name, Object name);
Почему вы хотите это сделать? Можно использовать объекты стиля Map<String, Object>
более эффективно, чем использование обычной карты.
Другой подход заключается в создании источника Java с использованием Velocity и компиляции кода с использованием API компилятора. Его боль в использовании, поэтому я написал для нее завернутый Essence JCF Единственное преимущество чтения в использовании этого подхода заключается в том, что вы можете легко отлаживать свои сгенерированные код. (В библиотеке есть возможность сохранить код java, где отладчик может найти его, когда вы входите в сгенерированный код)
Ответ 3
Что бы вызывающий вызывал с классом, который генерируется "на лету" и который его код поэтому не может знать? Единственный способ получить доступ к нему - через отражение. Возврат List<String[]>
или Map<String, String>
будет на самом деле намного более чистым и более удобным для использования дизайном.
Ответ 4
Я также ненавижу писать геттеры и сеттеры. Я бы предпочел использовать POJO, даже POJO, объявленные как вложенные классы.
Есть еще один способ сделать это, даже со старыми серверами и технологиями, и не вводя Springs (мы используем JBoss 4.2 и JBoss неполный EJB 3.0). Расширяя org.apache.commons.beanutils.BeanMap, вы можете обернуть POJO на карту bean, а когда вы получите или поместите, вы можете манипулировать полями с помощью отражения. Если геттер или сеттер не существует, мы просто используем манипуляции с полями для его получения. Очевидно, это НЕ настоящий bean, так что отлично ОК.
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);
}
}
Сложная часть - факторизация POJO для возврата, но отражение может помочь вам:
/**
* 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 fields are blank.
*/
public static Object constructBeanOrPOJO(final Object obj) {
Constructor<?> ctor = null;
Object retval = null;
//Try to invoke where it 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 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 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);
}
}
Пример кода для получения общего bean из bean, клонируемого или 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;
}
Пример использования с 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);
}
EJB имеет такой интерфейс:
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);
}
Вы можете себе представить, как выглядит реализация ejb, прямо сейчас мы создаем подготовленные заявления и вызываем их, но мы можем использовать критерии или что-то более холодное, как спящий режим или что-то еще, если мы хотим.
Вот приблизительный пример (некоторым НЕ понравится эта часть). В примере у нас есть одна таблица с данными в 3-й нормальной форме. Для этого поля bean должны соответствовать именам столбцов таблицы. Вызов toLowerCase() в случае, если используется REAL bean, что приведет к совпадению имени (MyField vs. getMyfield). Возможно, эту часть можно было бы отполировать немного лучше. В частности, порядок и четкость должны быть флагами или чем-то еще. Возможно, есть и другие краевые условия. Конечно, мне нужно писать только это, и для производительности ничего не мешает вам получать гораздо более точный приемник данных для производительности.
@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()));
}
}
Ответ 5
Хорошо Это может также дать попробовать. Но мне нужно понять это, если кто-нибудь может объяснить.
ОБНОВЛЕНИЕ:
Представьте, что ваше приложение должно динамически создавать экземпляры Java POJO во время выполнения из некоторой внешней конфигурации. Эта задача может быть легко выполнена с использованием одной из библиотек манипуляции байт-кода. Это сообщение демонстрирует, как это можно сделать с помощью библиотеки Javassist.
Предположим, что мы имеем следующую конфигурацию для свойств, которые наш динамически созданный POJO должен содержать:
Map<String, Class<?>> props = new HashMap<String, Class<?>>();
props.put("foo", Integer.class);
props.put("bar", String.class);
Давайте напишем PojoGenerator, который динамически генерирует объект класса для данного имени класса и карты, содержащей требуемые свойства:
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());
}
}
Вот оно!
Использование PojoGenerator довольно просто. Позволяет генерировать POJO, выводить через отражение все его методы, устанавливать и затем получать некоторое свойство:
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);
}
Выполнение выше приведет к следующему выводу консоли:
Clazz: class net.javaforge.blog.javassist.Pojo$Generated
Object: [email protected]
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!
Ответ 6
Решение обеспечивается 1-м ответом следующей ссылки, которая является 1 из запросов Stack).
Динамически создавать классы таблиц и java во время выполнения