一次失败的动态转换bean的尝试与思考

前因

公司规范确定不允许使用反射类的进行属性bean的拷贝了,只允许手动的get/set,可以猜到这样定义的原因是制定规范的同事认为反射性能低,虽然写get/set慢点好在性能高。平时开发的时候也是迫不得已才用反射。不过禁用的话就感觉有点钻牛角尖了。

所谓反射性能低是指在使用JDK自带反射工具类反射与非反射性能相比会差7倍(个人测试,未必科学),听起来的确很悬殊的,不过从另一个角度来看貌似反射也没那么恐怖,比如一个非反射对象的转换需要10ns,使用反射就是70ns,这对系统性能提高貌似没有太显著的效果,而且用反射基本都会把能缓存的都存下来,性能也差不哪去。如果一个接口的性能很低一定不是对象拷贝上搞的鬼,基本都和代码逻辑实现、IO、RPC等这些因素相关,这时要做的是优化代码流程、异步线程等方式,优化对象拷贝实在标、本都不治。算上研发人员写get/set的时间与痛苦,感觉得不偿失。对大对象类型的拷贝,的确会耗些性能比如到1ms,目前为止我还没见过超过5ms的对象拷贝,很明显这不是瓶颈,我见过最高要求的接口超时要求是5ms,试想这样的高标准,服务器是不是也很牛逼了,CPU是不是就更那啥了。。。

峰回路转,规范还是要照样遵守:}。不过本人的确很懒,要写那么多的机械代码感觉很low。于是就想能否通过什么办法避过反射完成属性的拷贝。

解决方式

首先,我希望类型转换能像这样优雅的调用:

ConvertUtil.convert(srcBean, TargetBean.class);

于是,我想到两种方式:

第一种 对象克隆,需要科普的可以找找我写的原型设计模式。这个实现起来最简单,但是容错能力很明显不好。因此不考虑。

第二种 动态生成转换方法。根据source object和target class反射生成字符串,动态编译到一个class文件,实例化并放入内存中。预加载一次后再也不用动了。我觉得这个挺好,一劳永逸,“巧妙”的绕过了规范。于是就有了下面的一坨代码:
转换逻辑:

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager.Location;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;

public final class ConvertUtil {

    private ConvertUtil(){}

    public final static String PREFIX = "X$";
    private final static String PKG = "com.array7.util.dynamic_proxy";
    private final static Map<String, IConvert<?, ?>> CLASS_MAPPING = new ConcurrentHashMap<String, IConvert<?, ?>>();
    
    public static <S, T> StringBuffer genConvertUtilStr(Class<S> srcClazz, Class<T> targetClazz, String pkg) {
        StringBuffer sb = new StringBuffer();
        sb.append("package ").append(pkg).append(";

");
        sb.append("public class ").append(getJavaFileName(srcClazz, targetClazz)).append(" implements ")
                .append(IConvert.class.getName()).append("<").append(srcClazz.getName()).append(",")
                .append(targetClazz.getName()).append(">").append(" {
");
        sb.append("	@Override
");
        sb.append("	public ").append(targetClazz.getName()).append(" convert(").append(srcClazz.getName())
                .append(" s) throws IllegalAccessException, InstantiationException  {
");
        sb.append(targetClazz.getName()).append(" t = new ").append(targetClazz.getName()).append("(); 
");
        
        genSetFiledsStr(srcClazz, targetClazz, sb);
        
        sb.append("		return t;
");
        sb.append("	}
");
        sb.append("}
");
        return sb;
    }
    
    /**
     * create convert method string
     * @param srcClazz .
     * @param targetClazz .
     * @param sb .
     */
    private static <S, T> void genSetFiledsStr(Class<S> srcClazz, Class<T> targetClazz, final StringBuffer sb) {
        // class field map
    	Map<String, Class<?>> filedMap = getFieldMap(srcClazz, targetClazz);

    	for (String name : filedMap.keySet()) {
            sb.append("		t.").append(getSetMethodName(name)).append("(s.").append(getGetMethodName(name)).append("()); 
");
        }
    }

    /**
     *
     * @param srcClazz .
     * @param targetClazz .
     * @return
     */
    private static Map<String, Class<?>> getFieldMap (Class<?> srcClazz, Class<?> targetClazz) {
    	return getFieldMap(srcClazz, targetClazz, null);
    }

    /**
     * getFieldMap
     * @param srcClazz
     * @param targetClazz
     * @param map
     * @return
     */
    private static Map<String, Class<?>> getFieldMap (Class<?> srcClazz, Class<?> targetClazz, Map<String, Class<?>> map) {
    	if (map == null) {
    		map = new HashMap<String, Class<?>>();
    	}
    	Field[] fields = srcClazz.getDeclaredFields();
    	for (Field field : fields) {
            String name = field.getName();
            try {
                srcClazz.getMethod(getSetMethodName(name), field.getType());
                targetClazz.getMethod(getGetMethodName(name));

                map.put(field.getName(), field.getType());
            } catch (NoSuchMethodException e) {
                System.err.println(getSetMethodName(name));
                System.err.println(getGetMethodName(name));
                continue;
            }

    	}
    	Class<?> srcSuperClazz = srcClazz.getSuperclass();
        Class<?> targetSuperClazz = targetClazz.getSuperclass();
    	if (!Object.class.getName().equals(srcSuperClazz.getName())
                && !Object.class.getName().equals(srcSuperClazz.getName())) {
    		return getFieldMap(srcSuperClazz, targetSuperClazz, map);
    	}
		return map;
    }

    private static String getSetMethodName(String fieldName) {
        return "set" + upper1stChar(fieldName);
    }

    private static String getGetMethodName(String fieldName) {
        return "get" + upper1stChar(fieldName);
    }

    private static String upper1stChar(String name) {
        byte[] bytes = name.getBytes();
        bytes[0] = (byte) ((char) bytes[0] - 'a' + 'A');
        return new String(bytes);
    }

    private static <S, T, Z> Class<Z> genClazz(Class<S> sClazz, Class<T> tClazz, String pkg) {
        StringBuffer code = ConvertUtil.genConvertUtilStr(sClazz, tClazz, pkg);
        // debug
        System.out.println(code);

        String output_path = ConvertUtil.class.getResource("/").getPath();

        JavaCompiler jc = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileManager = jc.getStandardFileManager(null, null, null);
        Location location = StandardLocation.CLASS_OUTPUT;

        File[] outputs = new File[] { new File(output_path) };
        try {
            fileManager.setLocation(location, Arrays.asList(outputs));
        } catch (IOException e) {
            e.printStackTrace();
        }

        JavaFileObject jfo = new LoadSourceFromString(pkg + "." + getJavaFileName(sClazz, tClazz),
                code.toString());
        JavaFileObject[] jfos = new JavaFileObject[] { jfo };

        Iterable<? extends JavaFileObject> compilationUnits = Arrays.asList(jfos);
        boolean success = jc.getTask(null, fileManager, null, null, null, compilationUnits).call();
        if (success) {
            try {
                return (Class<Z>) Class.forName(pkg + "." + getJavaFileName(sClazz, tClazz));
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
    
    private static <S, T, Z> Z instanceClazz(Class<S> sClazz, Class<T> tClazz, String pkg) throws InstantiationException,
            IllegalAccessException {
        Class<Z> clazzZ = genClazz(sClazz, tClazz, pkg);
        if (clazzZ == null) {
            throw new NullPointerException();
        }
        return clazzZ.newInstance();
    }
    
	public static <S, T> T convert(S s, Class<T> clazz) {
        try {
            if (s == null || clazz == null) {
                throw new NullPointerException("Source value or target class is null.");
            }
            IConvert<?, ?> convert = CLASS_MAPPING.get(getJavaFileName(s.getClass(), clazz));
            if (convert == null) {
                convert = instanceClazz(s.getClass(), clazz, PKG);
                if (convert == null) {
                    throw new InstantiationException();
                }
                CLASS_MAPPING.put(getJavaFileName(s.getClass(), clazz), convert);
            }
            return ((IConvert<S, T>) convert).convert(s);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    private static String getJavaFileName(Class<?> srcClazz, Class<?> targetClazz) {
        String src = srcClazz.getSimpleName();
        String target = targetClazz.getSimpleName();
        return PREFIX + src + "2" + target;
    }

}

通用接口:

public interface IConvert<S, T> {
    public T convert(S s) throws InstantiationException, IllegalAccessException;
}

动态编译:

import java.net.URI;

import javax.tools.SimpleJavaFileObject;

public class LoadSourceFromString extends SimpleJavaFileObject {
    final String code;
    LoadSourceFromString(String name, String code) {
        super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
        this.code = code;
    }
    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
        return code;
    }
}

自定义映射关系,以后打算缓存配置文件接口来着,用不上了:

public class MetaFiledData {
	private String srcFieldName;
	private String srcFieldType;
	private String targetFieldName;
	private String targetFieldType;
	...
	getter/setter
}

以上代码完成了基本类型的转换,同时也支持了父类的属性的递归调用。
但是再往下继续写的时候发现一个很尴尬的问题。如果两个对象的类型不一致的情况就很难继续下去了。
举个栗子:
source bean有一个属性是List a,而target bean对应的属性是ArrayList a。直接看我们知道他是可以转的,但是如果在代码里实现,要if else的逻辑会让人崩溃,加之集合内部元素XyzBean很明显也有一些属性,如此递归下去,可能性太多了,甚至有极端情况属性bean里如果有循环依赖,那就。。。
再举个栗子:
如果有人写了List接口的自定义实现,我甚至根本不知道这是个玩意。通过反射可以实现找到List的接口,其中的工作量~~

结果

写到后面没有动力了,可以预见后面有很多的关于集合、自定义对象、不同数据类型转换的逻辑判断,根据其中的巨大工作量,还可能有很多的坑留下,反射异常的不可控制等因素。发现对自己和对调用者而言,很明显弊大于利。
我也终于明白为什么目前没有使用生成内部类的方式写对象转换实现的开源包了,目光短浅了。
性能的确没有问题了,这是废话。。简单类型转换还是太过鸡肋。
最后竟然是相当于只写了个简易的代码生成器。。

收获

  1. 验证了意图的不可实现;
  2. 自动生成转换方法的函数;
  3. 走出一步既有收获,不过要看的远点。。
原文地址:https://www.cnblogs.com/liushijie/p/4714473.html