SpringBoot对象拷贝
概述
众所周知,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坐标为:
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>1.0.2</version>
</dependency>
造数据方法很简单,一个批量一个单个,可以通过count来控制造的数据量。
/**
* 批量造数据
*
* @return
*/
private List<Car> findCar() {
Faker faker = new Faker(new Locale("zh-CN"));
List<Car> 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
<!-- https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
下面看看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
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.0</version>
</dependency>
下面看看 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
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.0.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.0.Final</version>
</dependency>
下面看看MapStruct用法举例
使用前需要先定义一个接口,编写转换逻辑,而后通过调用接口方法获取转换后的结果。
CarInfoMapper
import com.ramble.demo.dto.CarInfo;
import com.ramble.demo.model.Car;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public 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种方式的处理速度
*/
@GetMapping("/test1")
public void test1() {
Faker faker = new Faker(new Locale("zh-CN"));
List<Car> carList = findCar();
StopWatch sw = new StopWatch("对象拷贝速度测试");
sw.start("方式1:Spring BeanUtils.copyProperties");
List<CarInfo> 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<CarInfo> 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<CarInfo> 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<CarInfo> 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.copyProperties
2322316400 043% 方式2:apache BeanUtils.copyProperties
442981700 008% 方式3:BeanCopier
280836100 005% 方式4:MapStruct
总耗时:5.3822332
100W 数据量
StopWatch '对象拷贝速度测试': running time = 58689078300 ns
---------------------------------------------
ns % Task name
---------------------------------------------
25199532100 043% 方式1:Spring BeanUtils.copyProperties
27179965900 046% 方式2:apache BeanUtils.copyProperties
3629756500 006% 方式3:BeanCopier
2679823800 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随之改变,说明是浅拷贝。