文章目录
背景
在实际项目中,考虑到不同的数据使用者,我们经常要处理 VO、DTO、Entity、DO 等对象的转换,或是 Map 与 Bean 之间互相转换,如果通过 Hard Code 编写 setter/getter 方法一个个赋值,将非常繁琐且难维护。通常情况下,这类转换都是同名属性的转换(类型可能不同),我们更多地会使用 bean copy 工具,例如 Spring BeanUtils、Apache Commons BeanUtils、Cglib BeanCopier 、BeanMap、Mapstruct、或者是利用 JSON 转换工具曲线救国等等。本文总结各种工具的性能、基本原理及区别。
分类
上述方案按原理可以分为:
- 通过反射调用 getter/setter;
- 通过动态代理(生成字节码)技术在运行时生成包含 setter/getter 的代码的代理类;
- 编译时通过注解处理器基于 setter/getter 生成对应的赋值代码;
- 通过转为 JSON 字符串再转回对象。
通常我们追求的是更优的性能,并且有时希望能满足额外的需求(如支持类型不同的转换)。
另外对象转换又可以分为深拷贝和浅拷贝(深拷贝就是对基本数据类型进行值复制,对引用数据类型,创建新对象并复制其内容;浅拷贝对基本数据类型进行值复制,引用对象还是指向原对象):
- 深拷贝
- JSON 转换
- Serializable 序列化
- 浅拷贝:
- Spring BeanUtils
- Apache Commons BeanUtils
- Cglib BeanCopier
- Mapstruct
原理****剖析****
Apache Commons BeanUtils
Apache BeanUtils 的实现原理跟 Spring 的 BeanUtils 基本一样,也是主要通过Java的Introspector
机制获取到类的属性来进行赋值操作,对 BeanInfo 和 PropertyDescriptor 同样有缓存,但是 Apache BeanUtils 加了一些特性(对于对象拷贝加了很多的检验,包括类型的转换,甚至还会检验对象所属的类的可访问性,可谓相当复杂,这也造就了它的差劲的性能)在里面,使得性能相对较低。
public void copyProperties(final Object dest, final Object orig)
throws IllegalAccessException, InvocationTargetException {
// Validate existence of the specified beans
if (dest == null) {
throw new IllegalArgumentException
("No destination bean specified");
}
if (orig == null) {
throw new IllegalArgumentException("No origin bean specified");
}
if (log.isDebugEnabled()) {
log.debug("BeanUtils.copyProperties(" + dest + ", " +
orig + ")");
}
// Copy the properties, converting as necessary
if (orig instanceof DynaBean) {
final DynaProperty[] origDescriptors =
((DynaBean) orig).getDynaClass().getDynaProperties();
for (DynaProperty origDescriptor : origDescriptors) {
final String name = origDescriptor.getName();
// Need to check isReadable() for WrapDynaBean
// (see Jira issue# BEANUTILS-61)
if (getPropertyUtils().isReadable(orig, name) &&
getPropertyUtils().isWriteable(dest, name)) {
final Object value = ((DynaBean) orig).get(name);
copyProperty(dest, name, value);
}
}
} else if (orig instanceof Map) {
@SuppressWarnings("unchecked")
final
// Map properties are always of type <String, Object>
Map<String, Object> propMap = (Map<String, Object>) orig;
for (final Map.Entry<String, Object> entry : propMap.entrySet()) {
final String name = entry.getKey();
if (getPropertyUtils().isWriteable(dest, name)) {
copyProperty(dest, name, entry.getValue());
}
}
} else /* if (orig is a standard JavaBean) */ {
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
}
}
}
}
}
Spring BeanUtils
通过Java的Introspector
获取到两个类的PropertyDescriptor
,对比两个属性具有相同的名字和兼容的类型,如果是,则进行赋值(通过 ReadMethod 获取值,通过 WriteMethod 赋值),否则忽略,代码比较简洁。
private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties)
throws BeansException {
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)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
"] not assignable to Editable class [" + editable.getName() + "]");
}
actualEditable = editable;
}
// 获取target类的属性(优先缓存)
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
// 获取source类的属性(优先缓存)
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
// 判断 target 的 setter 方法入参类型和 source 的 getter 方法出参类型是否相同或是 getter 出参的父类/父接口
if (readMethod != null &&
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
// 反射调用 setter 赋值
Object value = readMethod.invoke(source);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
}
catch (Throwable ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
}
Cglib BeanCopier
BeanCopier 的实现原理跟 BeanUtils 完全不同,它不是利用反射对属性进行赋值,而是直接使用cglib 来生成带有的 get/set 方法的class类,然后执行。由于是直接生成字节码执行,所以BeanCopier 的性能接近手写。
BeanCopier支持两种方式,一种是不使用Converter的方式,仅对两个bean间属性名和类型完全相同的变量进行拷贝。另一种则引入Converter,可以对某些特定属性值进行特殊操作。
copier.copy(source, target, new Converter() {
/**
* @param sourceValue source 对象属性值
* @param targetClass target 对象对应类
* @param methodName targetClass 里属性对应set方法名,eg.setId
* @return
*/
public Object convert(Object sourceValue, Class targetClass, Object methodName) {
if (targetClass.equals(Integer.TYPE)) {
return new Integer(((Number)sourceValue).intValue() + 1);
}
return sourceValue;
}
});
不使用 Converter 生成的字节码类似下面:
public class ObjectBeanCopierByCGLIBd18c8 extends BeanCopier {
public ObjectBeanCopierByCGLIBd1d970c8() {
}
public void copy(Object var1, Object var2, Converter var3) {
TargetVO var10000 = (TargetVO)var2;
SourceVO var10001 = (SourceVO)var1;
var10000.setDate1(((SourceVO)var1).getDate1());
var10000.setIn(var10001.getIn());
var10000.setListData(var10001.getListData());
var10000.setMapData(var10001.getMapData());
var10000.setP1(var10001.getP1());
var10000.setP2(var10001.getP2());
var10000.setP3(var10001.getP3());
var10000.setPattr1(var10001.getPattr1());
}
}
Mapstruct
他与其他工具最大的不同之处在于,其并不是在程序运行过程中通过反射进行字段复制的,而是在编译期生成用于字段复制的代码(类似于 Lombok 生成 getter 和 setter 方法),这种特性使得该框架在运行时相比于工具有很大的性能提升。生成的类类似下面:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "xxx",
comments = "version: 1.4.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.3.jar, environment: Java 1.8.0_231 (Oracle Corporation)"
)
public class DemoMapperImpl implements DemoMapper {
@Override
public DemoDto toDemoDto(Demo demo) {
if ( demo == null ) {
return null;
}
DemoDto demoDto = new DemoDto();
demoDto.setId( demo.getId() );
demoDto.setName( demo.getName() );
return demoDto;
}
}
性能对比
总结
性能上最优的是 Hard Code 方式,但由于维护困难一般不考虑;其次是 Cglib BeanCopier 和 Mapstruct 性能较稳定;再次则是基于反射的 beanUtils,而 JSON 方式本身定位不是对象拷贝,一般不做考虑,不过其性能一般优于基于反射的工具。
手动拷贝 > Mapstruct = cglib beanCopier > JSON > spring beanUtils > apache commons beanUtils
除了性能以外,在项目中选择何种方式进行对象转换则需要根据业务情况、项目原先使用的依赖库等进行衡量,权衡性能和使用的方便性、安全性(避免出错)等。