目录

  • 概述
  • 定义实体类
    • Car
    • size
    • carInfo
    • 造测试数据
  • Spring BeanUtils
  • Apache BeanUtils
  • Cglib BeanCopier
  • MapStruct
  • 性能测试
  • 深拷贝or浅拷贝

概述

众所周知,java世界是由构成的,各种各样的类,提供各种各样的作用,共同创造了一个个的java应用。对象是类的实例,在SpringBoot框架中,对象经常需要拷贝,例如数据库实体拷贝成业务实体,导入实体转换为业务实体,各种数据传输对象之间的拷贝等等。日常开发工作中用到的地方和频率是相当的高。本文就围绕对象拷贝来聊聊常用的姿势(方式)和工具

定义实体类

为了演示对象拷贝将创建几个实体类和几个生成测试数据的方法。

Car

car描述了车辆这个业务对象, 其中包含一些常见的基本数据类型的属性和一个 size类型 的属性,即包含基本数据类型和引用类型,包含子实体。

import lombok.Data;import lombok.experimental.Accessors;import java.io.Serializable;import java.math.BigDecimal;import java.time.LocalDate;@Data@Accessors(chain = true)public class Car implements Serializable {  private Integer id;  private String name;  private String brand;  private String address;  private Size size;  private Double cc;  /**  * 扭矩  */  private Double torque;  /**  * 厂商  */  private String manufacturer;  /**  * 上市时间  */  private LocalDate marketDate;  /**  * 售价  */  private BigDecimal price;}

size

size描述了车辆大小,具体来说就是长宽高。

import lombok.Data;import lombok.experimental.Accessors;import java.io.Serializable;@Data@Accessors(chain = true)public class Size implements Serializable {  private Double length;  private Double width;  private Double height;}

carInfo

car对象需要拷贝为carInfo对象,他们两个大部分属性都一样,carInfo比car多了 color ,type

package com.ramble.demo.dto;import com.ramble.demo.model.Size;import lombok.Data;import lombok.experimental.Accessors;import java.io.Serializable;import java.math.BigDecimal;import java.time.LocalDate;@Data@Accessors(chain = true)public class CarInfo implements Serializable {  private Integer id;  private String name;  private String brand;  private String address;  private Size size;  private Double cc;  /**  * 扭矩  */  private Double torque;  /**  * 厂商  */  private String manufacturer;  /**  * 上市时间  */  private LocalDate marketDate;  /**  * 售价  */  private BigDecimal price;  private String color;  private Integer type;}

造测试数据

造测试数据用到了一个工具 javaFaker,通过实例化一个Faker 对象可以轻易的生成测试数据:人名、地址、网址、手机号。。。。。。

gav坐标为:

  com.github.javafaker  javafaker  1.0.2

造数据方法很简单,一个批量一个单个,可以通过count来控制造的数据量。

  /**  * 批量造数据  *  * @return  */  private List findCar() {    Faker faker = new Faker(new Locale("zh-CN"));    List list = new ArrayList();    int count = 100000;    for (int i = 0; i < count; i++) {      list.add(getCar(faker));    }    return list;  }      /**  * 造数据 - 单个  *  * @param faker  * @return  */  private Car getCar(Faker faker) {    Car car = new Car();    car.setId(faker.number().numberBetween(1, 999999999));    car.setName(faker.name().name());    car.setBrand(faker.cat().breed());    car.setAddress(faker.address().fullAddress());    Size size = new Size();    size.setLength(faker.number().randomDouble(7, 4000, 7000));    size.setWidth(faker.number().randomDouble(7, 4000, 7000));    size.setHeight(faker.number().randomDouble(7, 4000, 7000));    car.setSize(size);    car.setCc(faker.number().randomDouble(7, 1000, 12000));    car.setTorque(faker.number().randomDouble(1, 100, 70000));    car.setManufacturer(faker.name().name());    Date date = faker.date().birthday();    Instant instant = date.toInstant();    ZoneId zone = ZoneId.systemDefault();    LocalDate localDate = instant.atZone(zone).toLocalDate();    car.setMarketDate(localDate);    car.setPrice(BigDecimal.valueOf(faker.number().randomDigit()));    return car;  }

Spring BeanUtils

这么常用的功能官方肯定已经集成了,对对对,就是BeanUtils.copyProperties了。顺便说一句,遇到找工具类的时候不要盲目baidu,不妨先看看springframework.util这个包下面的东西。

下面看看Spring的BeanUtils.copyProperties用法举例

Faker faker = new Faker(new Locale("zh-CN"));Car car = getCar(faker);CarInfo carInfo = new CarInfo();BeanUtils.copyProperties(car, carInfo);
  • 使用反射实现两个对象的拷贝
  • 不会对类型进行转换,如source对象有一个integer类型的id属性,target对象有一个string类型的id属性,拷贝之后,target对象为null;同理integer 无法拷贝到long
  • 官方出品,无需引入其他依赖,推荐指数8颗星

Apache BeanUtils

apache-common系列的东西,java开发多多少少会用到一些,它本身也积累了很多经验提供一些有用并常用的工具和组件。

针对BeanUtils 使用前需要引入pom

  commons-beanutils  commons-beanutils  1.9.4

下面看看Apache的BeanUtils.copyProperties 用法举例

Faker faker = new Faker(new Locale("zh-CN"));Car car = getCar(faker);CarInfo carInfo = new CarInfo();try { org.apache.commons.beanutils.BeanUtils.copyProperties(carInfo, car);} catch (IllegalAccessException e) {  throw new RuntimeException(e);} catch (InvocationTargetException e) {  throw new RuntimeException(e);}
  • 强制要处理异常
  • source对象和target对象相对Spring来说是颠倒的
  • 通过反射实现
  • 不会对类型进行转换,如source对象有一个integer类型的id属性,target对象有一个string类型的id属性,拷贝之后,target对象为null;同理integer 无法拷贝到long
  • 性能稍微逊色与Spring,推荐指数7颗星

Cglib BeanCopier

BeanCopier使用起来稍微有点复杂,需要先实例化一个BeanCopier对象,然后再调用其copy方法完成对象拷贝

使用前需要引入如下pom

  cglib  cglib  3.2.0

下面看看 Cglib 的 BeanCopier 用法举例

Faker faker = new Faker(new Locale("zh-CN"));Car car = getCar(faker);CarInfo item = new CarInfo();inal BeanCopier copier = BeanCopier.create(Car.class, CarInfo.class, false);copier.copy(car, item, null);
  • BeanCopier虽然使用复杂,且要引入依赖,但是性能出众
  • 对于数据量小的情况不没有必要使用这个,推荐指数5颗星
  • 对于数据量大,比如5W个对象拷贝为另外一个对象,这种场景虽然少但是一旦发生了Spring BeanUtils处理起来就非常耗时,这种情况下推荐指数9颗星

MapStruct

MapStruct 是一个代码生成器,它基于约定生成的映射代码使用的是普通的方法调用,因此快速、类型安全且易于理解。MapStruct 本质没有太多科技与狠活,就是帮助开发人员编写getA,setB这样的样板代码,并提供扩展点,比如将carType映射为type。

使用前需要引入如下pom

  org.mapstruct  mapstruct  1.5.0.Final  org.mapstruct  mapstruct-processor  1.5.0.Final

下面看看MapStruct用法举例

使用前需要先定义一个接口,编写转换逻辑,而后通过调用接口方法获取转换后的结果。

CarInfoMapper

import com.ramble.demo.dto.CarInfo;import com.ramble.demo.model.Car;import org.mapstruct.Mapper;import org.mapstruct.factory.Mappers;@Mapperpublic interface CarInfoMapper {  CarInfoMapper INSTANCT = Mappers.getMapper(CarInfoMapper.class);  CarInfo carToCarInfo(Car car);}
  • Mapper:将此接口标注为用来转换bean的接口,并在编译过程中生成相应的实现类
  • carToCarInfo,声明一个转换方法,并将source作为方法入参,target作为方法出参
  • INSTANCT,按照约定,接口声明一个INSTANCT成员,提供给访问者消费
  • 一个接口可以有多个转换方法
  • 可通过在方法上添加@Mapping注解,将不同属性名子的属性进行转换
  • 可以将枚举类型转换为字符串
  • 性能出众,术业有专攻,在处理复杂转换和大数据量的时候很有必要
  • 因为要引入依赖并且需要单独编写接口,所以对于简单的转换还是推荐Spring,这种情况下推荐指数5颗星
  • 如果在大数据量的情况,并且转换相对复杂,推荐指数10颗星

调用 CarInfoMapper 进行对象拷贝

Faker faker = new Faker(new Locale("zh-CN"));Car car = getCar(faker);CarInfo carInfo = CarInfoMapper.INSTANCT.carToCarInfo(car);

性能测试

原则上, 性能测试是多维度的,速度、CPU消耗、内存消耗等等,本文仅考虑速度,只为说明问题,不为得到测试数据

测试代码逻辑,使用for结合faker生成一个大的List,然后用上述4种方式做属性拷贝。使用StopWatch记录程序运行时间。

测试代码如下:

/*** 测试4种方式的处理速度*/@GetMapping("/test1")public void test1() {  Faker faker = new Faker(new Locale("zh-CN"));  List carList = findCar();  StopWatch sw = new StopWatch("对象拷贝速度测试");  sw.start("方式1:Spring BeanUtils.copyProperties");  List list1 = new ArrayList();  carList.forEach(x -> {    CarInfo item = new CarInfo();    BeanUtils.copyProperties(x, item);    item.setColor(faker.color().name());    item.setType(faker.number().randomDigit());    list1.add(item);  });  sw.stop();  sw.start("方式2:apache BeanUtils.copyProperties");  List list2 = new ArrayList();  carList.forEach(x -> {    CarInfo item = new CarInfo();    try {      org.apache.commons.beanutils.BeanUtils.copyProperties(item, x);    } catch (IllegalAccessException e) {      throw new RuntimeException(e);    } catch (InvocationTargetException e) {      throw new RuntimeException(e);    }    item.setColor(faker.color().name());    item.setType(faker.number().randomDigit());    list2.add(item);  });  sw.stop();  sw.start("方式3:BeanCopier");  List list3 = new ArrayList();  carList.forEach(x -> {    CarInfo item = new CarInfo();    final BeanCopier copier = BeanCopier.create(Car.class, CarInfo.class, false);    copier.copy(x, item, null);    item.setColor(faker.color().name());    item.setType(faker.number().randomDigit());    list3.add(item);  });  sw.stop();  sw.start("方式4:MapStruct");  List list4 = new ArrayList();  carList.forEach(x -> {    CarInfo item = CarInfoMapper.INSTANCT.carToCarInfo(x);    item.setColor(faker.color().name());    item.setType(faker.number().randomDigit());    list4.add(item);  });  sw.stop();  String s = sw.prettyPrint();  System.out.println(s);  double totalTimeSeconds = sw.getTotalTimeSeconds();  System.out.println("总耗时:" + totalTimeSeconds);}

测试结果如下

10W 数据量

StopWatch '对象拷贝速度测试': running time = 5382233200 ns---------------------------------------------ns         %     Task name---------------------------------------------2336099000  043%  方式1:Spring BeanUtils.copyProperties2322316400  043%  方式2:apache BeanUtils.copyProperties442981700  008%  方式3:BeanCopier280836100  005%  方式4:MapStruct总耗时:5.3822332

100W 数据量

StopWatch '对象拷贝速度测试': running time = 58689078300 ns---------------------------------------------ns         %     Task name---------------------------------------------25199532100  043%  方式1:Spring BeanUtils.copyProperties27179965900  046%  方式2:apache BeanUtils.copyProperties3629756500  006%  方式3:BeanCopier2679823800  005%  方式4:MapStruct总耗时:58.6890783

深拷贝or浅拷贝

上述四种方式均为浅拷贝。可通过如下测试代码验证

/*** 测试4种方式是浅拷贝还是深拷贝*/@GetMapping("/test2")public void test2() {  Faker faker = new Faker(new Locale("zh-CN"));  Car car = getCar(faker);  log.info("car={}", JSON.toJSONString(car));  CarInfo carInfo = new CarInfo();  //方式1//    BeanUtils.copyProperties(car, carInfo);//    Size sizeNew = car.getSize();//    sizeNew.setLength(0d);//    sizeNew.setWidth(0d);//    sizeNew.setHeight(0d);////    Size s = new Size();////    s.setHeight(10d);////    car.setSize(s);//    car.setName("新名字");//    log.info("carInfo={}", JSON.toJSONString(carInfo));    //方式2//    try {//      org.apache.commons.beanutils.BeanUtils.copyProperties(carInfo, car);//    } catch (IllegalAccessException e) {//      throw new RuntimeException(e);//    } catch (InvocationTargetException e) {//      throw new RuntimeException(e);//    }//    Size sizeNew = car.getSize();//    sizeNew.setLength(0d);//    sizeNew.setWidth(0d);//    sizeNew.setHeight(0d);////    car.setName("新名字");//    log.info("carInfo={}", JSON.toJSONString(carInfo));    //方式3//    final BeanCopier copier = BeanCopier.create(Car.class, CarInfo.class, false);//    copier.copy(car, carInfo, null);//    Size sizeNew = car.getSize();//    sizeNew.setLength(0d);//    sizeNew.setWidth(0d);//    sizeNew.setHeight(0d);////    car.setName("新名字");//    log.info("carInfo={}", JSON.toJSONString(carInfo));    //方式4    carInfo = CarInfoMapper.INSTANCT.carToCarInfo(car);    Size sizeNew = car.getSize();    sizeNew.setLength(0d);    sizeNew.setWidth(0d);    sizeNew.setHeight(0d);    car.setName("新名字");    log.info("carInfo={}", JSON.toJSONString(carInfo));}

验证逻辑为,在完成copy后修改car.size 的值,发现carInfo.size随之改变,说明是浅拷贝。

邮箱:cnaylor@163.com 技术交流QQ群:1158377441 欢迎关注我的微信公众号【TechnologyRamble】,后续博文将在公众号首发: