12

SpringBoot对象拷贝 - Naylor

 8 months ago
source link: https://www.cnblogs.com/Naylor/p/17926055.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

定义实体类

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

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描述了车辆大小,具体来说就是长宽高。


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种方式做属性拷贝。使用StopWatch记录程序运行时间。

测试代码如下:


/**
 * 测试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随之改变,说明是浅拷贝。

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK