• 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.javafakergroupId>
        <artifactId>javafakerartifactId>
        <version>1.0.2version>
    dependency>
    
    

    造数据方法很简单,一个批量一个单个,可以通过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

    
    
    <dependency>
        <groupId>commons-beanutilsgroupId>
        <artifactId>commons-beanutilsartifactId>
        <version>1.9.4version>
    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>cglibgroupId>
        <artifactId>cglibartifactId>
        <version>3.2.0version>
    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.mapstructgroupId>
        <artifactId>mapstructartifactId>
        <version>1.5.0.Finalversion>
    dependency>
    <dependency>
        <groupId>org.mapstructgroupId>
        <artifactId>mapstruct-processorartifactId>
        <version>1.5.0.Finalversion>
    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 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.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随之改变,说明是浅拷贝。

  • 相关阅读:
    shardingsphere分库分表示例(逻辑表,真实表,绑定表,广播表,单表)
    java计算机毕业设计小区停车场信息系统源码+系统+数据库+lw文档+mybatis+运行部署
    ctfhub-文件上传-双写后缀
    【免杀前置课——Windows编程】三、第一个窗口程序——WINDOWS程序框架-消息、WINDOWS中应用程序如何接受消息
    Sentinel流控规则详解
    在线课堂分销商城小程序源码系统 带完整搭建教程
    深入解读[面向对象五大设计原则]
    qt之movetothread理解
    计算机网络——HTTPS协议
    Pytorch之ConvNeXt图像分类
  • 原文地址:https://www.cnblogs.com/Naylor/p/17926055.html