• Java对象拷贝原理剖析及最佳实践


    作者:宁海翔

    1 前言

    对象拷贝,是我们在开发过程中,绕不开的过程,既存在于Po、Dto、Do、Vo各个表现层数据的转换,也存在于系统交互如序列化、反序列化。

    Java对象拷贝分为深拷贝和浅拷贝,目前常用的属性拷贝工具,包括Apache的BeanUtils、Spring的BeanUtils、Cglib的BeanCopier、mapstruct都是浅拷贝。

    1.1 深拷贝

    深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容称为深拷贝。

    深拷贝常见有以下四种实现方式:

    • 构造函数
    • Serializable序列化
    • 实现Cloneable接口
    • JSON序列化

    1.2 浅拷贝

    浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝称为浅拷贝。通过实现Cloneabe接口并重写Object类中的clone()方法可以实现浅克隆。

    2 常用对象拷贝工具原理剖析及性能对比

    目前常用的属性拷贝工具,包括Apache的BeanUtils、Spring的BeanUtils、Cglib的BeanCopier、mapstruct。

    • Apache BeanUtils:BeanUtils是Apache commons组件里面的成员,由Apache提供的一套开源 api,用于简化对javaBean的操作,能够对基本类型自动转换。
    • Spring BeanUtils:BeanUtils是spring框架下自带的工具,在org.springframework.beans包下, spring项目可以直接使用。
    • Cglib BeanCopier:cglib(Code Generation Library)是一个强大的、高性能、高质量的代码生成类库,BeanCopier依托于cglib的字节码增强能力,动态生成实现类,完成对象的拷贝。
    • mapstruct:mapstruct 是一个 Java注释处理器,用于生成类型安全的 bean 映射类,在构建时,根据注解生成实现类,完成对象拷贝。

    2.1 原理分析

    2.1.1 Apache BeanUtils

    使用方式:BeanUtils.copyProperties(target, source);
    BeanUtils.copyProperties 对象拷贝的核心代码如下:

    
    // 1.获取源对象的属性描述
    PropertyDescriptor[] origDescriptors = this.getPropertyUtils().getPropertyDescriptors(orig);
    PropertyDescriptor[] temp = origDescriptors;
    int length = origDescriptors.length;
    String name;
    Object value;
    
    // 2.循环获取源对象每个属性,设置目标对象属性值
    for(int i = 0; i < length; ++i) {
    PropertyDescriptor origDescriptor = temp[i];
    name = origDescriptor.getName();
    // 3.校验源对象字段可读切目标对象该字段可写
    if (!"class".equals(name) && this.getPropertyUtils().isReadable(orig, name) && this.getPropertyUtils().isWriteable(dest, name)) {
    try {
    // 4.获取源对象字段值
    value = this.getPropertyUtils().getSimpleProperty(orig, name);
    // 5.拷贝属性
    this.copyProperty(dest, name, value);
    } catch (NoSuchMethodException var10) {
    }
    }
    }
    

    循环遍历源对象的每个属性,对于每个属性,拷贝流程为:

    • 校验来源类的字段是否可读isReadable
    • 校验目标类的字段是否可写isWriteable
    • 获取来源类的字段属性值getSimpleProperty
    • 获取目标类字段的类型type,并进行类型转换
    • 设置目标类字段的值

    由于单字段拷贝时每个阶段都会调用PropertyUtilsBean.getPropertyDescriptor获取属性配置,而该方法通过for循环获取类的字段属性,严重影响拷贝效率。
    获取字段属性配置的核心代码如下:

    PropertyDescriptor[] descriptors = this.getPropertyDescriptors(bean);
    if (descriptors != null) {
    for (int i = 0; i < descriptors.length; ++i) {
    if (name.equals(descriptors[i].getName())) {
    return descriptors[i];
    }
    }
    }
    

    2.1.2 Spring BeanUtils

    使用方式: BeanUtils.copyProperties(source, target);
    BeanUtils.copyProperties核心代码如下:

    PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
    List ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
    PropertyDescriptor[] arr$ = targetPds;
    int len$ = targetPds.length;
    for(int i$ = 0; i$ < len$; ++i$) {
    PropertyDescriptor targetPd = arr$[i$];
    Method writeMethod = targetPd.getWriteMethod();
    if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
    PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
    if (sourcePd != null) {
    Method readMethod = sourcePd.getReadMethod();
    if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
    try {
    if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
    readMethod.setAccessible(true);
    }
    Object value = readMethod.invoke(source);
    if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
    writeMethod.setAccessible(true);
    }
    writeMethod.invoke(target, value);
    } catch (Throwable var15) {
    throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var15);
    }
    }
    }
    }
    }
    

    拷贝流程简要描述如下:

    • 获取目标类的所有属性描述
    • 循环目标类的属性值做以下操作
      • 获取目标类的写方法
      • 获取来源类的该属性的属性描述(缓存获取)
      • 获取来源类的读方法
      • 读来源属性值
      • 写目标属性值

    与Apache BeanUtils的属性拷贝相比,Spring通过Map缓存,避免了类的属性描述重复获取加载,通过懒加载,初次拷贝时加载所有属性描述。

    2.1.3 Cglib BeanCopier

    使用方式:

    BeanCopier beanCopier = BeanCopier.create(AirDepartTask.class, AirDepartTaskDto.class, false);
    beanCopier.copy(airDepartTask, airDepartTaskDto, null);
    

    create调用链如下:

    BeanCopier.create
    -> BeanCopier.Generator.create
    -> AbstractClassGenerator.create
    ->DefaultGeneratorStrategy.generate
    -> BeanCopier.Generator.generateClass

    BeanCopier 通过cglib动态代理操作字节码,生成一个复制类,触发点为BeanCopier.create

    2.1.4 mapstruct

    使用方式:

    • 引入pom依赖
    • 声明转换接口

    mapstruct基于注解,构建时自动生成实现类,调用链如下:
    MappingProcessor.process -> MappingProcessor.processMapperElements
    MapperCreationProcessor.process:生成实现类Mapper
    MapperRenderingProcessor:将实现类mapper,写入文件,生成impl文件
    使用时需要声明转换接口,例如:

    @Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    public interface AirDepartTaskConvert {
    AirDepartTaskConvert INSTANCE = getMapper(AirDepartTaskConvert.class);
    AirDepartTaskDto convertToDto(AirDepartTask airDepartTask);
    }
    

    生成的实现类如下:

    public class AirDepartTaskConvertImpl implements AirDepartTaskConvert {
    
    @Override
    public AirDepartTaskDto convertToDto(AirDepartTask airDepartTask) {
    if ( airDepartTask == null ) {
    return null;
    }
    
    AirDepartTaskDto airDepartTaskDto = new AirDepartTaskDto();
    
    airDepartTaskDto.setId( airDepartTask.getId() );
    airDepartTaskDto.setTaskId( airDepartTask.getTaskId() );
    airDepartTaskDto.setPreTaskId( airDepartTask.getPreTaskId() );
    List<String> list = airDepartTask.getTaskBeginNodeCodes();
    if ( list != null ) {
    airDepartTaskDto.setTaskBeginNodeCodes( new ArrayList<String>( list ) );
    }
    // 其他属性拷贝
    airDepartTaskDto.setYn( airDepartTask.getYn() );
    
    return airDepartTaskDto;
    }
    }
    

    2.2 性能对比

    以航空业务系统中发货任务po到dto转换为例,随着拷贝数据量的增大,研究拷贝数据耗时情况

    2.3 拷贝选型

    经过以上分析,随着数据量的增大,耗时整体呈上升趋势

    • 整体情况下,Apache BeanUtils的性能最差,日常使用过程中不建议使用
    • 在数据规模不大的情况下,spring、cglib、mapstruct差异不大,spring框架下建议使用spring的beanUtils,不需要额外引入依赖包
    • 数据量大的情况下,建议使用cglib和mapstruct
    • 涉及大量数据转换,属性映射,格式转换的,建议使用mapstruct

    3 最佳实践

    3.1 BeanCopier

    使用时可以使用map缓存,减少同一类对象转换时,create次数

    /**
    * BeanCopier的缓存,避免频繁创建,高效复用
    */
    private static final ConcurrentHashMap<String, BeanCopier> BEAN_COPIER_MAP_CACHE = new ConcurrentHashMap<String, BeanCopier>();
    
    /**
    * BeanCopier的copyBean,高性能推荐使用,增加缓存
    *
    * @param source 源文件的
    * @param target 目标文件
    */
    public static void copyBean(Object source, Object target) {
    String key = genKey(source.getClass(), target.getClass());
    BeanCopier beanCopier;
    if (BEAN_COPIER_MAP_CACHE.containsKey(key)) {
    beanCopier = BEAN_COPIER_MAP_CACHE.get(key);
    } else {
    beanCopier = BeanCopier.create(source.getClass(), target.getClass(), false);
    BEAN_COPIER_MAP_CACHE.put(key, beanCopier);
    }
    beanCopier.copy(source, target, null);
    }
    
    /**
    * 不同类型对象数据copylist
    *
    * @param sourceList
    * @param targetClass
    * @param 
    * @return
    */
    public static  List copyListProperties(List sourceList, Class targetClass) throws Exception {
    if (CollectionUtils.isNotEmpty(sourceList)) {
    List list = new ArrayList(sourceList.size());
    for (Object source : sourceList) {
    T target = copyProperties(source, targetClass);
    list.add(target);
    }
    return list;
    }
    return Lists.newArrayList();
    }
    
    /**
    * 返回不同类型对象数据copy,使用此方法需注意不能覆盖默认的无参构造方法
    *
    * @param source
    * @param targetClass
    * @param 
    * @return
    */
    public static  T copyProperties(Object source, Class targetClass) throws Exception {
    T target = targetClass.newInstance();
    copyBean(source, target);
    return target;
    }
    
    /**
    * @param srcClazz 源class
    * @param tgtClazz 目标class
    * @return string
    */
    private static String genKey(Class srcClazz, Class tgtClazz) {
    return srcClazz.getName() + tgtClazz.getName();
    }
    

    3.2 mapstruct

    mapstruct支持多种形式对象的映射,主要有下面几种

    • 基本映射
    • 映射表达式
    • 多个对象映射到一个对象
    • 映射集合
    • 映射map
    • 映射枚举
    • 嵌套映射
    @Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    public interface AirDepartTaskConvert {
    AirDepartTaskConvert INSTANCE = getMapper(AirDepartTaskConvert.class);
    
    // a.基本映射
    @Mapping(target = "createTime", source = "updateTime")
    // b.映射表达式
    @Mapping(target = "updateTimeStr", expression = "java(new SimpleDateFormat( \"yyyy-MM-dd\" ).format(airDepartTask.getCreateTime()))")
    AirDepartTaskDto convertToDto(AirDepartTask airDepartTask);
    }
    
    @Mapper
    public interface AddressMapper {
    AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);
    
    // c.多个对象映射到一个对象
    @Mapping(source = "person.description", target = "description")
    @Mapping(source = "address.houseNo", target = "houseNumber")
    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
    }
    
    @Mapper
    public interface CarMapper {
    // d.映射集合
    Set integerSetToStringSet(Set integers);
    
    List carsToCarDtos(List cars);
    
    CarDto carToCarDto(Car car);
    // e.映射map
    @MapMapping(valueDateFormat = "dd.MM.yyyy")
    Map longDateMapToStringStringMap(Map<Long, Date> source);
    
    // f.映射枚举
    @ValueMappings({
    @ValueMapping(source = "EXTRA", target = "SPECIAL"),
    @ValueMapping(source = "STANDARD", target = "DEFAULT"),
    @ValueMapping(source = "NORMAL", target = "DEFAULT")
    })
    ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
    // g.嵌套映射
    @Mapping(target = "fish.kind", source = "fish.type")
    @Mapping(target = "fish.name", ignore = true)
    @Mapping(target = "ornament", source = "interior.ornament")
    @Mapping(target = "material.materialType", source = "material")
    @Mapping(target = "quality.report.organisation.name", source = "quality.report.organisationName")
    FishTankDto map( FishTank source );
    }
    

    4 总结

    以上就是我在使用对象拷贝过程中的一点浅谈。在日常系统开发过程中,要深究底层逻辑,哪怕发现一小点的改变能够使我们的系统更加稳定、顺畅,都是值得我们去改进的。

    最后,希望随着我们的加入,系统会更加稳定、顺畅,我们会变得越来越优秀。

  • 相关阅读:
    0905(047天 大数02 hadoop环境搭建)
    docker容器
    解决 UDP 接收不到数据问题
    《游戏编程模式》学习笔记(十四)事件队列 Event Queue
    Spring Boot学习笔记
    Spring cloud alibaba实战
    【计网】链路层
    Google开源依赖注入框架-Guice指南
    【最优化理论】03-无约束优化
    CVE-2022-25237 Bonitasoft Platform RCE漏洞复现
  • 原文地址:https://www.cnblogs.com/Jcloud/p/16921336.html