文章目录
1. 对象属性拷贝的常见方式及其性能
在日常编码中,经常会遇到DO、DTO对象之间的转换,如果对象本身的属性比较少的时候,那么我们采用硬编码手工setter也还ok,但如果对象的属性比较多的情况下,手工setter就显得又low又效率又低。这个时候我们就考虑采用一些工具类来进行对象属性的拷贝了。
我们常用的对象属性拷贝的方式有:
Hard Code
net.sf.cglib.beans.BeanCopier#copy
org.springframework.beans.BeanUtils.copyProperties
org.apache.commons.beanutils.PropertyUtils.copyProperties
org.apache.commons.beanutils.BeanUtils.copyProperties
针对以上的拷贝方式,我做了一个简单的性能测试,结果如下:
拷贝方式 | 对象数量: 1 | 对象数量: 1000 | 对象数量: 100000 | 对象数量: 1000000 |
---|---|---|---|---|
Hard Code |
0 ms | 1 ms | 18 ms | 43 ms |
cglib.BeanCopier |
111 ms | 117 ms | 107 ms | 110 ms |
spring.BeanUtils |
116 ms | 137 ms | 246 ms | 895 ms |
apache.PropertyUtils |
167 ms | 212 ms | 601 ms | 7869 ms |
apache.BeanUtils |
167 ms | 275 ms | 1732 ms | 12380 ms |
测试环境:OS=
macOS 10.14
, CPU=2.5 GHz,Intel Core I7
, Memory=16 GB, 2133MHz LPDDR3
测试方法:通过copy指定数量的复杂对象,分别执行每个Case
10
次,取其平均值
版本:commons-beanutils:commons-beanutils:1.9.3
,org.springframework:spring-beans:4.3.5.RELEASE
,cglib:cglib:2.2.2
结论:从测试结果中很明显可以看出采用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时产生的差异如此巨大呢? 这正是本文我们想要去探究的内容。
我们首先解读
org.apache.commons.beanutils.BeanUtils
的源码,其次解读org.springframework.beans.BeanUtils
源码,最后通过它们各自实现方式来进行论证性能差异
apache.BeanUtils
与spring.BeanUtils
均采用反射技术实现,也都调用了Java关于反射的高级API——Introspector
(内省),因此我们首先要了解Introspector
是什么.
2. Introspector
Introspector(内省)
是jdk提供的用于描述Java bean
支持的属性、方法以及事件的工具;利用此类可得到BeanInfo
接口的实现对象,BeanInfo
接口中有两个重要的方法:
-
BeanDescriptor getBeanDescriptor();
,BeanDescriptor
提供了java bean的一些全局的信息,如class类型、类名称等 -
PropertyDescriptor[] getPropertyDescriptors()
**
PropertyDescriptor
** 描述了java bean中一个属性并导出了他们的getter & setter
方法的SoftReference
Jdk的内省接口极大的简化了反射类信息的方式,通过这组api我们可以很方便进行java bean的反射调用。本组api采用软引用、虚引用来充分利用了空闲的内存;在某些地方(如declaredMethodCache
)采用缓存来加速api的执行效率,并且此组api是线程安全的。
使用方式:
BeanInfo beanInfo= Introspector.getBeanInfo(icontext.getTargetClass());
PropertyDescriptor[] descriptors= beanInfo.getPropertyDescriptors();for(PropertyDescriptor descriptor: descriptors){
Method readMethod= descriptor.getReadMethod();
Method writeMethod= descriptot.getWriteMethod();// readMethod.invoke(...);}
以上就是关于Introspector
的简单了解,接下来我们先来看apache.BeanUtils
的源码.
3. 源码:apache.BeanUtils
apache.BeanUtils
是一个包含了很多静态方法的工具类,而几乎所有的静态方法均是BeanUtilsBean
的单例对象提供的实现。BeanUtilsBean
是进行JavaBean属性操作的入口方法,它以单实例对外提供功能。但这里有一个不同于普通单例的地方:不同的类加载器拥有不同的实例,每一个类加载器只有一个实例
,所以这里的单例其实是一个伪单例pseudo-singletion
。
// ContextClassLoaderLocal对象管理了BeanUtilsBean的所有实例privatestaticfinal ContextClassLoaderLocal<BeanUtilsBean>
BEANS_BY_CLASSLOADER=newContextClassLoaderLocal<BeanUtilsBean>(){@Overrideprotected BeanUtilsBeaninitialValue(){returnnewBeanUtilsBean();}};publicstatic BeanUtilsBeangetInstance(){return BEANS_BY_CLASSLOADER.get();}// {@link ContextClassLoaderLocal#get}publicsynchronized Tget(){
valueByClassLoader.isEmpty();try{final ClassLoader contextClassLoader= Thread.currentThread().getContextClassLoader();// 获取当前线程的类加载器if(contextClassLoader!= null){
T value= valueByClassLoader.get(contextClassLoader);if((value== null)&&!valueByClassLoader.containsKey(contextClassLoader)){
value=initialValue();// 初始化BeanUtilsBean,即 new BeanUtilsBean();
valueByClassLoader.put(contextClassLoader, value);}return value;}}catch(final SecurityException e){/* SWALLOW - should we log this? */}if(!globalValueInitialized){
globalValue=initialValue();
globalValueInitialized=true;}return globalValue;}
当获取到了BeanUtilsBean
的实例之后,接下来就是我们进行对象属性拷贝的时候了.
// omit exceptionpublicstaticvoidcopyProperties(final Object dest,final Object orig){
BeanUtilsBean.getInstance().copyProperties(dest, orig);}
在copyProperties
方法中,针对原始对象的类型分别采用了不同的逻辑:
Map
: 通过Map的Key与dest中的属性进行匹配,然后赋值;DynaBean
:DynaBean
顾名思义,它是一种可以形成动态java bean的对象,也就是说它内部会存储属性名称、类型以及对应的值,在copy属性时也是将其内部的属性名称与dest对象的属性名称对应后赋值;标准Java Bean
:这个是我们主要进行分析的类型,它是标准的JavaBean对象;与前两者的差异只是在于对原始bean的取值的处理上.
3.1 针对标准JavaBean进行属性copy时的步骤
publicvoidcopyProperties(final Object dest,final Object orig){// omit some code (省略一部分代码) ...final PropertyDescriptor[] origDescriptors=getPropertyUtils().getPropertyDescriptors(orig);for(PropertyDescriptor origDescriptor: origDescriptors){final String name= origDescriptor.getName();if("class".equals(name)){continue;// No point in trying to set an object's class}if(getPropertyUtils().isReadable(orig, name)&&getPropertyUtils().isWriteable(dest, name)){try{final Object value=getPropertyUtils().getSimpleProperty(orig, name);copyProperty(dest, name, value);}catch(final NoSuchMethodException e){// Should not happen}}}}
- 根据原始bean的类型解析、缓存其
PropertyDescriptor
- 轮询原始bean的每一个
PropertyDescriptor
,判断PropertyDescriptor
在原始bean中是否可读、在目标bean中是否可写,只有这两个条件都成立时才具备copy的资格 - 根据
PropertyDescriptor
从原始bean中获取对应的值,将值copy至目标bean的对应属性上
3.2 获取Bean的PropertyDescriptor
final PropertyDescriptor[] origDescriptors=getPropertyUtils().getPropertyDescriptors(orig);
获取PropertyDescriptor
委托给PropertyUtilsBean
对象来实现:
publicBeanUtilsBean(){this(newConvertUtilsBean(),newPropertyUtilsBean());}
PropertyUtilsBean
是用于使用java 反射API来操作Java Bean上getter和setter方法的,此类中的代码原先是位于BeanUtilsBean
中的,但是考虑到代码量的原因进行了分离(Much of this code was originally included in BeanUtils, but has been separated because of the volume of code involved
)。
在PropertyUtilsBean
中,每个Bean的PropertyDescriptor
会存储于BeanIntrospectionData
对象中,当每次需要获取PropertyDescriptor
时,会先从cahche中获取BeanIntrospectionData
;如果不存在,则通过内省API获取BeanIntrospectionData
并将其置于缓存中:
private BeanIntrospectionDatagetIntrospectionData(final Class<?> beanClass){// omit some check code ...
BeanIntrospectionData data= descriptorsCache.get(beanClass);if(data== null){
data=fetchIntrospectionData(beanClass);
descriptorsCache.put(beanClass, data);}return data;}private BeanIntrospectionDatafetchIntrospectionData(final Class<?> beanClass){final DefaultIntrospectionContext ictx=newDefaultIntrospectionContext(beanClass);for(final BeanIntrospector bi: introspectors){try{
bi.introspect(ictx);}catch(final IntrospectionException iex){
log.error("Exception during introspection", iex);}}returnnewBeanIntrospectionData(ictx.getPropertyDescriptors());}
在fetchIntrospectionData()
方法中,通过内置的内省器DefaultBeanIntrospector
使用java的内省API将获取的信息传递给DefaultIntrospectionContext
, 在通过DefaultIntrospectionContext
构造BeanIntrospectionData
。DefaultBeanIntrospector
具体的代码:
publicvoidintrospect(final IntrospectionContext icontext){
BeanInfo beanInfo= null;try{// JAVA 的 Instrospector
beanInfo= Introspector.getBeanInfo(icontext.getTargetClass());}catch(final IntrospectionException e){return;}
PropertyDescriptor[] descriptors= beanInfo.getPropertyDescriptors();if(descriptors== null){
descriptors=newPropertyDescriptor[0];}// 解决IndexedPropertyDescriptor在不同版本的JDK下的差异handleIndexedPropertyDescriptors(icontext.getTargetClass(), descriptors);
icontext.addPropertyDescriptors(descriptors);}
3.3 判断属性是否可读/可写
要进行属性copy,那么首先得确保原始对象的属性可读、目标对象属性可写。在PropertyUtilsBean
中通过isWriteable(); isReadable()
方法,这两个方法看上去比较长,我们把关于exception的处理省略掉拿出来看下:
publicbooleanisReadable(Object bean, String name){// Omit Validate method parameters// Resolve nested references, 解析内嵌的属性,形如 student.namewhile(resolver.hasNested(name)){final String next= resolver.next(name);
Object nestedBean= nestedBean=getProperty(bean, next);if(nestedBean== null){thrownewNestedNullException("Null property valuefor);}
bean= nestedBean;
name= resolver.remove(name);}// Remove any subscript from the final name value, 在最终的方法名中移除所有的下标
name= resolver.getProperty(name);if(beaninstanceofWrapDynaBean){
bean=((WrapDynaBean)bean).getInstance();}if(beaninstanceofDynaBean){// All DynaBean properties are readable,所有DynaBean的属性均是可读的return(((DynaBean) bean).getDynaClass().getDynaProperty(name)!= null);}else{final PropertyDescriptor desc=getPropertyDescriptor(bean, name);if(desc!= null){
Method readMethod=getReadMethod(bean.getClass(), desc);if(readMethod== null){if(descinstanceofIndexedPropertyDescriptor){
readMethod=((IndexedPropertyDescriptor) desc).getIndexedReadMethod();}elseif(descinstanceofMappedPropertyDescriptor){
readMethod=((MappedPropertyDescriptor) desc).getMappedReadMethod();}
readMethod= MethodUtils.getAccessibleMethod(bean.getClass(), readMethod);}return(readMethod!= null);}else{return(false);}}}
从以上代码我们可以得知,每个属性的可读、可写在每次使用时都需要获取Method,然后进行判断,并且还需要处理DynaBean、Nested的逻辑;当我们进行批量的属性copy时,依然需要执行以上步骤,并未将method的判断结果进行缓存,这也是其相比于其他的jar低效的原因.
3.4 读取原始Bean的属性值、设置目标Bean的属性值
我们还是省略掉其中的有效性判断和异常的代码:
public ObjectgetSimpleProperty(final Object bean,final String name)throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException{// omit check null code ...// 校验属性if(resolver.hasNested(name)){thrownewIllegalArgumentException("Nested property names are not allowed: Property '"+
name+"' on bean class '"+ bean.getClass()+"'");}elseif(resolver.isIndexed(name)){thrownewIllegalArgumentException("Indexed property names are not allowed: Property '"+
name+"' on bean class '"+ bean.getClass()+"'");}elseif(resolver.isMapped(name)){thrownewIllegalArgumentException("Mapped property names are not allowed: Property '"+
name+"' on bean class '"+ bean.getClass()+"'");}// DynaBean的特殊逻辑if(beaninstanceofDynaBean){final DynaProperty descriptor=((DynaBean) bean).getDynaClass().getDynaProperty(name);if(descriptor== null){thrownewNoSuchMethodException("Unknown property '"+
name+"' on dynaclass '"+((DynaBean) bean).getDynaClass()+"'");}return(((DynaBean) bean).get(name));}final PropertyDescriptor descriptor=getPropertyDescriptor(bean, name);if(descriptor== null){thrownewNoSuchMethodException("Unknown property '"+
name+"' on class '"+ bean.getClass()+"'");}// 获取getter方法final Method readMethod=getReadMethod(bean.getClass(), descriptor);if(readMethod== null){thrownewNoSuchMethodException("Property '"+ name+"' has no getter method in class '"+ bean.getClass()+"'");}// 调用getter方法读取值final Object value=invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY);return(value);}
以上是读取属性值的方法。 读取到属性值之后,就是设置值到目标bean上了。 在BeanUtilsBean
的实现中,又重复的处理了属性的内嵌逻辑与DynaBean逻辑,最终获取到其setter方法将值赋予目标Bean.
4. 源码: spring.BeanUtils
BeanUtils
位于spring-beans
模块中,暴露出静态方法copyProperties
用以进行属性copy,每个copyProperties
最终均调用一个私有静态方法实现属性copy:
privatestaticvoidcopyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties){
Assert.notNull(source,"Source must not be null");
Assert.notNull(target,"Target must not be null");
Class<?> actualEditable= target.getClass();if(editable!= null){if(!editable.isInstance(target)){thrownewIllegalArgumentException("Target class ["+ target.getClass().getName()+"] not assignable to Editable class ["+ editable.getName()+"]");}
actualEditable= editable;}// 第一步 调用Java 内省API 获取PropertyDescriptor
PropertyDescriptor[] targetPds=getPropertyDescriptors(actualEditable);
List<String> ignoreList=(ignoreProperties!= null? Arrays.asList(ignoreProperties): null);// 第二步 轮询目标bean的PropertyDescriptorfor(PropertyDescriptor targetPd: targetPds){
Method writeMethod= targetPd.getWriteMethod();// 判断是否存在setter方法以及属性是否在需要忽略的属性列表中if(writeMethod!= null&&(ignoreList== null||!ignoreList.contains(targetPd.getName()))){// 获取源bean的PropertyDescriptor
PropertyDescriptor sourcePd=getPropertyDescriptor(source.getClass(), targetPd.getName());if(sourcePd!= null){// 获取getter方法
Method readMethod= sourcePd.getReadMethod();if(readMethod!= null&&
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())){try{// 如果getter方法不是public,则需要设置其accessibleif(!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())){
readMethod.setAccessible(true);}// 反射获取属性值
Object value= readMethod.invoke(source);// 如果setter方法不是public则需要设置其accessibleif(!Modifier.isPublic(writeMethod