常见对象转换方案对比

背景

在实际项目中,考虑到不同的数据使用者,我们经常要处理 VO、DTO、Entity、DO 等对象的转换,或是 Map 与 Bean 之间互相转换,如果通过 Hard Code 编写 setter/getter 方法一个个赋值,将非常繁琐且难维护。通常情况下,这类转换都是同名属性的转换(类型可能不同),我们更多地会使用 bean copy 工具,例如 Spring BeanUtils、Apache Commons BeanUtils、Cglib BeanCopier 、BeanMap、Mapstruct或者是利用 JSON 转换工具曲线救国等等。本文总结各种工具的性能、基本原理及区别。

分类

上述方案按原理可以分为:

  1. 通过反射调用 getter/setter;
  2. 通过动态代理(生成字节码)技术在运行时生成包含 setter/getter 的代码的代理类;
  3. 编译时通过注解处理器基于 setter/getter 生成对应的赋值代码;
  4. 通过转为 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;
    }
}

性能对比

Untitled

总结

性能上最优的是 Hard Code 方式,但由于维护困难一般不考虑;其次是 Cglib BeanCopier 和 Mapstruct 性能较稳定;再次则是基于反射的 beanUtils,而 JSON 方式本身定位不是对象拷贝,一般不做考虑,不过其性能一般优于基于反射的工具。

手动拷贝 > Mapstruct = cglib beanCopier > JSON > spring beanUtils > apache commons beanUtils

除了性能以外,在项目中选择何种方式进行对象转换则需要根据业务情况、项目原先使用的依赖库等进行衡量,权衡性能和使用的方便性、安全性(避免出错)等。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

Scroll to Top