文章目录
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的所有实例
private static final ContextClassLoaderLocal<BeanUtilsBean>
BEANS_BY_CLASSLOADER = new ContextClassLoaderLocal<BeanUtilsBean>() {
@Override
protected BeanUtilsBean initialValue() {
return new BeanUtilsBean();
}
};
public static BeanUtilsBean getInstance() {
return BEANS_BY_CLASSLOADER.get();
}
// {@link ContextClassLoaderLocal#get}
public synchronized T get() {
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 exception
public static void copyProperties(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时的步骤
public void copyProperties(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
对象来实现:
public BeanUtilsBean() {
this(new ConvertUtilsBean(), new PropertyUtilsBean());