Chinaunix首页 | 论坛 | 博客
  • 博客访问: 122728
  • 博文数量: 165
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 1655
  • 用 户 组: 普通用户
  • 注册时间: 2022-09-26 14:37
文章分类

全部博文(165)

文章存档

2024年(2)

2023年(95)

2022年(68)

我的朋友

分类: Java

2022-11-24 15:00:59

作者:宁海翔

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. // 1.获取源对象的属性描述
  2. PropertyDescriptor[] origDescriptors = this.getPropertyUtils().getPropertyDescriptors(orig);
  3. PropertyDescriptor[] temp = origDescriptors;
  4. int length = origDescriptors.length;
  5. String name;
  6. Object value;

  7. // 2.循环获取源对象每个属性,设置目标对象属性值
  8. for(int i = 0; i < length; ++i) {
  9. PropertyDescriptor origDescriptor = temp[i];
  10. name = origDescriptor.getName();
  11. // 3.校验源对象字段可读切目标对象该字段可写
  12. if (!"class".equals(name) && this.getPropertyUtils().isReadable(orig, name) && this.getPropertyUtils().isWriteable(dest, name)) {
  13.       try {
  14. // 4.获取源对象字段值
  15.           value = this.getPropertyUtils().getSimpleProperty(orig, name);
  16. // 5.拷贝属性
  17.           this.copyProperty(dest, name, value);
  18.       } catch (NoSuchMethodException var10) {
  19.       }
  20.    }
  21. }


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

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

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

点击(此处)折叠或打开

  1. PropertyDescriptor[] descriptors = this.getPropertyDescriptors(bean);
  2. if (descriptors != null) {
  3. for (int i = 0; i < descriptors.length; ++i) {
  4. if (name.equals(descriptors[i].getName())) {
  5. return descriptors[i];
  6. }
  7. }
  8. }

2.1.2 Spring BeanUtils

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

点击(此处)折叠或打开

  1. PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
  2. List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
  3. PropertyDescriptor[] arr$ = targetPds;
  4. int len$ = targetPds.length;
  5. for(int i$ = 0; i$ < len$; ++i$) {
  6.     PropertyDescriptor targetPd = arr$[i$];
  7.     Method writeMethod = targetPd.getWriteMethod();
  8.     if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
  9.         PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
  10.         if (sourcePd != null) {
  11.             Method readMethod = sourcePd.getReadMethod();
  12.             if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
  13.                 try {
  14.                     if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
  15.                         readMethod.setAccessible(true);
  16.                     }
  17.                     Object value = readMethod.invoke(source);
  18.                     if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
  19.                         writeMethod.setAccessible(true);
  20.                     }
  21.                     writeMethod.invoke(target, value);
  22.                 } catch (Throwable var15) {
  23.                     throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var15);
  24.                 }
  25.             }
  26.         }
  27.     }
  28. }

拷贝流程简要描述如下:

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

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


2.1.3 Cglib BeanCopier

使用方式:

点击(此处)折叠或打开

  1. BeanCopier beanCopier = BeanCopier.create(AirDepartTask.class, AirDepartTaskDto.class, false);
  2. 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文件
使用时需要声明转换接口,例如:

点击(此处)折叠或打开

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

生成的实现类如下:

点击(此处)折叠或打开

  1. public class AirDepartTaskConvertImpl implements AirDepartTaskConvert {

  2.     @Override
  3.     public AirDepartTaskDto convertToDto(AirDepartTask airDepartTask) {
  4.         if ( airDepartTask == null ) {
  5.             return null;
  6.         }

  7.         AirDepartTaskDto airDepartTaskDto = new AirDepartTaskDto();

  8.         airDepartTaskDto.setId( airDepartTask.getId() );
  9.         airDepartTaskDto.setTaskId( airDepartTask.getTaskId() );
  10.         airDepartTaskDto.setPreTaskId( airDepartTask.getPreTaskId() );
  11.         List<String> list = airDepartTask.getTaskBeginNodeCodes();
  12.         if ( list != null ) {
  13.             airDepartTaskDto.setTaskBeginNodeCodes( new ArrayList<String>( list ) );
  14.         }
  15.         // 其他属性拷贝
  16.         airDepartTaskDto.setYn( airDepartTask.getYn() );

  17.         return airDepartTaskDto;
  18.     }
  19. }

2.2 性能对比

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


2.3 拷贝选型

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

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

3 {BANNED}最佳佳实践

3.1 BeanCopier

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

点击(此处)折叠或打开

  1. /**
  2.      * BeanCopier的缓存,避免频繁创建,高效复用
  3.      */
  4.     private static final ConcurrentHashMap<String, BeanCopier> BEAN_COPIER_MAP_CACHE = new ConcurrentHashMap<String, BeanCopier>();

  5.     /**
  6.      * BeanCopier的copyBean,高性能推荐使用,增加缓存
  7.      *
  8.      * @param source 源文件的
  9.      * @param target 目标文件
  10.      */
  11.     public static void copyBean(Object source, Object target) {
  12.         String key = genKey(source.getClass(), target.getClass());
  13.         BeanCopier beanCopier;
  14.         if (BEAN_COPIER_MAP_CACHE.containsKey(key)) {
  15.             beanCopier = BEAN_COPIER_MAP_CACHE.get(key);
  16.         } else {
  17.             beanCopier = BeanCopier.create(source.getClass(), target.getClass(), false);
  18.             BEAN_COPIER_MAP_CACHE.put(key, beanCopier);
  19.         }
  20.         beanCopier.copy(source, target, null);
  21.     }

  22.     /**
  23.      * 不同类型对象数据copylist
  24.      *
  25.      * @param sourceList
  26.      * @param targetClass
  27.      * @param <T>
  28.      * @return
  29.      */
  30.     public static <T> List<T> copyListProperties(List<?> sourceList, Class<T> targetClass) throws Exception {
  31.         if (CollectionUtils.isNotEmpty(sourceList)) {
  32.             List<T> list = new ArrayList<T>(sourceList.size());
  33.             for (Object source : sourceList) {
  34.                 T target = copyProperties(source, targetClass);
  35.                 list.add(target);
  36.             }
  37.             return list;
  38.         }
  39.         return Lists.newArrayList();
  40.     }

  41.     /**
  42.      * 返回不同类型对象数据copy,使用此方法需注意不能覆盖默认的无参构造方法
  43.      *
  44.      * @param source
  45.      * @param targetClass
  46.      * @param <T>
  47.      * @return
  48.      */
  49.     public static <T> T copyProperties(Object source, Class<T> targetClass) throws Exception {
  50.         T target = targetClass.newInstance();
  51.         copyBean(source, target);
  52.         return target;
  53.     }

  54.     /**
  55.      * @param srcClazz 源class
  56.      * @param tgtClazz 目标class
  57.      * @return string
  58.      */
  59.     private static String genKey(Class<?> srcClazz, Class<?> tgtClazz) {
  60.         return srcClazz.getName() + tgtClazz.getName();
  61.     }


3.2 mapstruct

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

  • 基本映射
  • 映射表达式
  • 多个对象映射到一个对象
  • 映射集合
  • 映射map
  • 映射枚举
  • 嵌套映射

    点击(此处)折叠或打开

    1. @Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    2. public interface AirDepartTaskConvert {
    3.     AirDepartTaskConvert INSTANCE = getMapper(AirDepartTaskConvert.class);

    4.     // a.基本映射
    5.     @Mapping(target = "createTime", source = "updateTime")
    6.     // b.映射表达式
    7.     @Mapping(target = "updateTimeStr", expression = "java(new SimpleDateFormat( \"yyyy-MM-dd\" ).format(airDepartTask.getCreateTime()))")
    8.     AirDepartTaskDto convertToDto(AirDepartTask airDepartTask);
    9. }

    10. @Mapper
    11. public interface AddressMapper {
    12.     AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);

    13.  // c.多个对象映射到一个对象
    14.     @Mapping(source = "person.description", target = "description")
    15.     @Mapping(source = "address.houseNo", target = "houseNumber")
    16.     DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
    17. }

    18. @Mapper
    19. public interface CarMapper {
    20.   // d.映射集合
    21.     Set<String> integerSetToStringSet(Set<Integer> integers);

    22.     List<CarDto> carsToCarDtos(List<Car> cars);

    23.     CarDto carToCarDto(Car car);
    24.   // e.映射map
    25.   @MapMapping(valueDateFormat = "dd.MM.yyyy")
    26.   Map<String,String> longDateMapToStringStringMap(Map<Long, Date> source);

    27.  // f.映射枚举
    28.     @ValueMappings({
    29.         @ValueMapping(source = "EXTRA", target = "SPECIAL"),
    30.         @ValueMapping(source = "STANDARD", target = "DEFAULT"),
    31.         @ValueMapping(source = "NORMAL", target = "DEFAULT")
    32.     })
    33.     ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
    34.     // g.嵌套映射
    35.     @Mapping(target = "fish.kind", source = "fish.type")
    36.     @Mapping(target = "fish.name", ignore = true)
    37.     @Mapping(target = "ornament", source = "interior.ornament")
    38.     @Mapping(target = "material.materialType", source = "material")
    39.     @Mapping(target = "quality.report.organisation.name", source = "quality.report.organisationName")
    40.     FishTankDto map( FishTank source );
    41. }

    4 总结

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

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

阅读(2570) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~