1.对象属性拷贝的常见方式及其性能 在日常编码中,经常会遇到DO、DTO对象之间的转换,如果对象本身的属性比较少的时候,那么采用Hard Code工作量也不大,但如果对象的属性比较多的情况下,Hard Code效率就比较低。这时候就要使用其它工具类来进行对象属性的拷贝。 常用的对象属性拷贝的方式和性能测试如下:
拷贝方式
copy次数1000
copy次数100000
copy次数1000000
Hard Code
1 ms
18 ms
43 ms
net.sf.cglib.beans.BeanCopier#copy
117 ms
107 ms
110 ms
org.springframework.beans.BeanUtils#copyProperties
137 ms
246 ms
895 ms
org.apache.commons.beanutils.PropertyUtils#copyProperties
212 ms
601 ms
7869 ms
org.apache.commons.beanutils.BeanUtils#copyProperties
275 ms
1732 ms
12380 ms
结论:采用Hard Code方式进行对象属性Copy性能最佳;采用net.sf.cglib.beans.BeanCopier#copy方式进行对象属性copy性能最稳定;而org.apache.commons.beanutils.BeanUtils.copyProperties 方式在数据量大时性能下降最厉害。所以在日常编程中遇到具有较多属性的对象进行属性复制时优先考虑采用net.sf.cglib.beans.BeanCopier#copy。
以上的数据之所以产生巨大差距的原因在于其实现原理与方式的不同而导致的,Hard Code直接调用getter & setter方法值,cglib采用的是字节码技术,而后三种均采用反射的方式。前两者性能优异众所周知,但为何同样采用反射的方式进行属性Copy时产生的差异如此巨大呢?
2.Introspector org.apache.commons.beanutils.BeanUtils与org.springframework.beans.BeanUtils均采用反射技术实现,也都调用了Java关于反射的高级API——Introspector(内省)。Introspector(内省)是jdk提供的用于描述Java bean支持的属性、方法以及事件的工具;利用此类可得到BeanInfo接口的实现对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 BeanInfo beanInfo = Introspector.getBeanInfo(Client.class); BeanDescriptor beanDescriptor = beanInfo.getBeanDescriptor(); System.out.println(beanDescriptor.getBeanClass()); PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors(); for(PropertyDescriptor descriptor: descriptors) { Method readMethod = descriptor.getReadMethod(); Method writeMethod = descriptor.getWriteMethod(); System.out.println(descriptor.getName()); // address System.out.println(readMethod); // public java.lang.String xxx.Client.getAddress() System.out.println(writeMethod); // public void xxx.Client.setAddress(java.lang.String) }
getBeanDescriptor()返回的BeanDescriptor提供了java bean的一些全局的信息,如class类型、类名称等。getPropertyDescriptors()返回PropertyDescriptor[],PropertyDescriptor描述了java bean中一个属性和它们的getter & setter方法的SoftReference。
3.BeanCopier#copy的使用 1 2 3 4 5 6 Client client = new Client(); client.setAddress("adc"); client.setEmail("@163.com"); BeanCopier copier = BeanCopier.create(Client.class, ClientDO.class, false); ClientDO clientDO = new ClientDO(); copier.copy(client, clientDO, null);
BeanCopier#copy的使用示例有很多,注意BeanCopier只拷贝名称和类型都相同的属性,使用Converter可以解决类型不同的字段拷贝。 那么,如果字段名称不一样,但是需要BeanCopier#copy来拷贝怎么弄呢?这里给一个解法,可能不是最优解。我们知道BeanCopier#copy是使用了asm来生成了一个转换类来做属性的拷贝,只要修改生成的类里面的逻辑即可,来看代码:
1 2 3 4 5 6 7 public static BeanCopier create(Class source, Class target, boolean useConverter) { Generator gen = new Generator(); gen.setSource(source); gen.setTarget(target); gen.setUseConverter(useConverter); return gen.create(); }
默认情况下,使用了net.sf.cglib.beans.BeanCopier.Generator,注意它是ClassGenerator的实现类,那么在生成类的使用就会调用它的generateClass方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 public void generateClass(ClassVisitor v) { Type sourceType = Type.getType(source); Type targetType = Type.getType(target); ClassEmitter ce = new ClassEmitter(v); ce.begin_class(Constants.V1_2, Constants.ACC_PUBLIC, getClassName(), BEAN_COPIER, null, Constants.SOURCE_FILE); EmitUtils.null_constructor(ce); CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, COPY, null); PropertyDescriptor[] getters = ReflectUtils.getBeanGetters(source); PropertyDescriptor[] setters = ReflectUtils.getBeanSetters(target); Map names = new HashMap(); for (int i = 0; i < getters.length; i++) { names.put(getters[i].getName(), getters[i]); } Local targetLocal = e.make_local(); Local sourceLocal = e.make_local(); if (useConverter) { e.load_arg(1); e.checkcast(targetType); e.store_local(targetLocal); e.load_arg(0); e.checkcast(sourceType); e.store_local(sourceLocal); } else { e.load_arg(1); e.checkcast(targetType); e.load_arg(0); e.checkcast(sourceType); } for (int i = 0; i < setters.length; i++) { PropertyDescriptor setter = setters[i]; PropertyDescriptor getter = (PropertyDescriptor)names.get(setter.getName()); if (getter != null) { MethodInfo read = ReflectUtils.getMethodInfo(getter.getReadMethod()); MethodInfo write = ReflectUtils.getMethodInfo(setter.getWriteMethod()); if (useConverter) { Type setterType = write.getSignature().getArgumentTypes()[0]; e.load_local(targetLocal); e.load_arg(2); e.load_local(sourceLocal); e.invoke(read); e.box(read.getSignature().getReturnType()); EmitUtils.load_class(e, setterType); e.push(write.getSignature().getName()); e.invoke_interface(CONVERTER, CONVERT); e.unbox_or_zero(setterType); e.invoke(write); } else if (compatible(getter, setter)) { e.dup2(); e.invoke(read); e.invoke(write); } } } e.return_value(); e.end_method(); ce.end_class(); }
asm不熟悉也不要紧,关键在于
1 2 3 4 5 PropertyDescriptor setter = setters[i]; PropertyDescriptor getter = (PropertyDescriptor)names.get(setter.getName()); if (getter != null) { MethodInfo read = ReflectUtils.getMethodInfo(getter.getReadMethod()); MethodInfo write = ReflectUtils.getMethodInfo(setter.getWriteMethod());
根据target中的setter的name,即字段名,去source中取getter,取不到就拉倒。那比如target有个字段是gender,但是实际对应source字段应该是sex,实际上可以在gender这个字段上加一个注解,值是sex,用注解值去source中取getter,然后做后面流程:
1 2 3 4 5 6 7 8 9 10 11 12 PropertyDescriptor getter = (PropertyDescriptor) names.get(setter.getName()); if (getter == null) { try { Field field = target.getDeclaredField(setter.getName()); Alias alias = field.getAnnotation(Alias.class); if (alias != null) { getter = (PropertyDescriptor) names.get(alias.value()); } } catch (NoSuchFieldException err) { err.printStackTrace(); } }
为了避免对源码的修改,可以自定义一个ConsumerGenerator extends BeanCopier.Generator,在创建BeanCopier时使用ConsumerGenerator生成。
参考:https://blog.csdn.net/u010209217/article/details/84837821#1__2