SpringBoot

Spring学习笔记(三十)——SpringBoot对象拷贝总结&Mapstruct

Nick · 9月8日 · 2021年 · 本文8557字 · 阅读22分钟47

深拷贝浅拷贝

概念

深拷贝
深拷贝相当于创建了一个新的对象,只是这个对象的所有内容,都和被拷贝的对象一模一样而已,即两者的修改是隔离的,相互之间没有影响。
浅拷贝
浅拷贝也是创建了一个对象,但是这个对象的某些内容(比如A)依然是被拷贝对象的,即通过这两个对象中任意一个修改A,两个对象的A都会受到影响。
那么问题来了:
* 浅拷贝中,是所有的内容公用呢?还是某些内容公用?
* 从隔离来讲,都不希望出现浅拷贝这种方式了,太容易出错了,那么两种拷贝方式的应用场景是怎样的?

浅拷贝

浅拷贝方式需要实现Cloneable接口,下面结合一个实例,来看下浅拷贝中哪些是独立的,哪些是公用的?
1. 代码如下:

import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
 * Created by tao.
 * Date: 2021/9/7 16:44
 * 描述:
 */
@Data
public class ShallowClone implements Cloneable {
    private String name;

    private int age;

    private List<String> books;

    /*浅拷贝克隆的方法*/
    public ShallowClone clone() {
        ShallowClone clone = null;
        try {
            clone = (ShallowClone) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return clone;
    }

    public static void main(String[] args) {
        ShallowClone shallowClone = new ShallowClone();
        shallowClone.setName("SourceName");
        shallowClone.setAge(28);
        List<String> list = new ArrayList<>();
        list.add("java");
        list.add("c++");
        shallowClone.setBooks(list);

        //浅拷贝一个对象
        ShallowClone cloneObj = shallowClone.clone();

        // 判断两个对象是否为同一个对象(即是否是新创建了一个实例)
        System.out.println(shallowClone == cloneObj);
        // 修改一个对象的内容是否会影响另一个对象
        shallowClone.setName("newName");
        shallowClone.getBooks().add("javascript");
        System.out.println("source: " + shallowClone.toString() + "\nclone:" + cloneObj.toString());
        List<String> books = shallowClone.getBooks();
        books.remove("c++");
        shallowClone.setBooks(books);
        System.out.println("source: " + shallowClone.toString() + "\nclone:" + cloneObj.toString());
    }
}
  1. 输出结果:
    Spring学习笔记(三十)——SpringBoot对象拷贝总结&Mapstruct-左眼会陪右眼哭の博客

  2. 通过上面两次打印的结果都能看出:

* 拷贝后获取的是一个独立的对象,和原对象拥有不同的内存地址
* 基本元素类型,两者是隔离的(虽然上面只给出了int,String)
基本元素类型包括:int, Integer, long, Long, char, Charset, byte,Byte, boolean, Boolean, float,Float, double, Double, String
* 非基本数据类型(如基本容器,其他对象等),只是拷贝了一份引用出去了,实际指向的依然是同一份

  1. 总结特点:
    基本数据类型是值赋值;非基本的就是引用赋值

深拷贝

深拷贝,就是要创建一个全新的对象,新的对象内部所有的成员也都是全新的,只是初始化的值已经由被拷贝的对象确定了而已。这个是我们在代码中用的最多的,比如对象拷贝,从Enity转Dto或者Vo,可能大部分使用的对象转换,数据拷贝都使用的是深拷贝。

深拷贝代码就不演示了,直接总结一下特点:
* 深拷贝独立的对象
* 拷贝后对象的内容,与原对象的内容完全没关系,都是独立的
* 深拷贝是需要自己来实现的,对于基本类型可以直接赋值,而对于对象、容器、数组来讲,需要创建一个新的出来,然后重新赋值

应用场景区分

深拷贝的用途我们很容易可以想见,某个复杂对象创建比较消耗资源的时候,就可以缓存一个蓝本,后续的操作都是针对深clone后的对象,这样就不会出现混乱的情况了。
那么浅拷贝呢?感觉留着是一个坑,一个人修改了这个对象的值,结果发现对另一个人造成了影响,感觉像是坑爹。所以实际中也用的不多。

假设下面一个场景:
我们现在随机挑选了一千个人,同时发送通知消息,所以需要创建一千个上面的对象,这些对象中呢,除了notifyUser不同,其他的都一样
在发送之前,突然发现要临时新增一条通知信息,如果是浅拷贝的话,只用在任意一个通知对象的notifyRules中添加一调消息,那么这一千个对象的通知消息都会变成最新的了;而如果你是用深拷贝,那么苦逼的得遍历这一千个对象,每个都加一条消息了。

对象拷贝工具

对象拷贝工具真的是一个在开发过程中可以极大提高开发效率的工具,在java工程中,肯定需要用到实体间的转换,比如po转vo,domain转dto,通常我们会写一些convert 写一堆set get来处理这个转换,目前我接触到了一个效率特别高的对象映射拷贝工具:Mapstruct。为此,也总结对比了一下之前用过的 Apache的BeanUtils、Spring的BeanUtils中的对象拷贝工具。

整合Mapstruct工具

Mapstruct简介

MapStruct是用于生成类型安全的bean映射类的Java注解处理器。
你所要做的就是定义一个映射器接口,声明任何需要映射的方法。在编译过程中,MapStruct将自动生成该接口的实现。此实现使用纯Java的方法调用源对象和目标对象之间进行映射,并非Java反射机制。
MapStruct是基于JSR 269的Java注解处理器,因此可以在命令行构建中使用(javac、Ant、Maven等等),也可以在IDE内使用。

Mapstruct的使用

  1. 创建SpringBoot项目后添加依赖
<!--mapStruct依赖-->
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-jdk8</artifactId>
            <version>${mapstruct.version}</version>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <version>${mapstruct.version}</version>
        </dependency>
  1. 创建实体类

User.java

public class User implements Serializable {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    private Dept dept;
    private Date createtime;
    private Date updatetime;

    public User(String name, Integer age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
}

UserDto.java

@Getter
@Setter
@ToString
public class UserDto {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    private Dept dept;
}
  1. 创建一个BaseMapper接口
import java.util.List;
/**
 * Created by tao
 * Date: 2021/9/2 9:32
 * 描述:
 */
public interface BaseMapper<D, E> {
    /**
     * DTO转Entity
     * @param dto /
     * @return /
     */
    E toEntity(D dto);

    /**
     * Entity转DTO
     * @param entity /
     * @return /
     */
    D toDto(E entity);

    /**
     * DTO集合转Entity集合
     * @param dtoList /
     * @return /
     */
    List <E> toEntity(List<D> dtoList);

    /**
     * Entity集合转DTO集合
     * @param entityList /
     * @return /
     */
    List <D> toDto(List<E> entityList);
}
  1. 创建一个UserMapper接口去继承BaseMapper接口
import cn.kt.mapstructdemo.base.BaseMapper;
import cn.kt.mapstructdemo.domin.User;
import cn.kt.mapstructdemo.service.dto.UserDto;
import org.mapstruct.Mapper;
import org.mapstruct.ReportingPolicy;
/**
 * Created by tao.
 * Date: 2021/9/2 11:04
 * 描述:
 */
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper extends BaseMapper<UserDto, User> {
}

目录结构大概是这样的:(具体的项目代码下篇博客详细介绍)
Spring学习笔记(三十)——SpringBoot对象拷贝总结&Mapstruct-左眼会陪右眼哭の博客
现在为止,Mapstruct工具就集成完成了,
接下来就可以进行愉快的使用Mapstruct进行数据拷贝了。.

看下面一个示例:

import cn.kt.mapstructdemo.domin.Dept;
import cn.kt.mapstructdemo.domin.User;
import cn.kt.mapstructdemo.service.dto.UserDto;
import cn.kt.mapstructdemo.service.mapstruct.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * Created by tao.
 * Date: 2021/9/7 15:08
 * 描述:
 */
@SpringBootTest
public class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void userToUserDto() {
        User user = new User();
        user.setName("路飞");
        user.setAge(21);
        user.setEmail("6666@qq.com");
        Dept dept = new Dept();
        dept.setName("海贼王");
        dept.setId(2L);
        user.setDept(dept);
        System.out.println(user);
        UserDto userDto = userMapper.toDto(user);
        System.out.println(userDto);
    }
}
  • 首先注入UserMapper
  • 然后使用UserMapper继承的方法userMapper.toDto(user);就可以实现数据映射拷贝了

运行结果如下:
Spring学习笔记(三十)——SpringBoot对象拷贝总结&Mapstruct-左眼会陪右眼哭の博客
把我们需要的字段拷贝到了userDto里面了。

虽说这个Mapstruct集成会有点麻烦,好像也感觉不出来有什么好处。那你就要往下看了:Mapstruct效率是真的强!

怎么来体现Mapstruct工具好用又高效呢?
我们要使用Apache的BeanUtils、Spring的BeanUtils对同样的数据拷贝做一个对比。

Apache的BeanUtils、Spring的BeanUtils、Mapstruct对比

Apache的BeanUtils和Spring的BeanUtils的使用相对比较简单,这两种工具也比较类似。
Apache的BeanUtils
引入依赖:

        <!-- Apache的BeanUtils依赖 -->
        <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
            <version>1.8.3</version>
        </dependency>

对象拷贝语句:将user的属性值拷贝到userDto中
BeanUtils.copyProperties(userDto, user);

Spring的BeanUtils
引入依赖:springboot自带的拷贝工具

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

对象拷贝语句:将user的属性值拷贝到userDto中
org.springframework.beans.BeanUtils.copyProperties(user, userDto);
发现这两个工具类其实是差不多的,使用区别是拷贝的对象和实体位置不一样,这两种工具也是都使用了反射机制,相对来说是Spring的BeanUtils性能相对优秀一点。

Apache的BeanUtils、Spring的BeanUtils、Mapstruct三者的性能测试
仍然使用上面两个实体类:User 和 UserDto

测试代码如下:

import cn.kt.mapstructdemo.domin.User;
import cn.kt.mapstructdemo.service.dto.UserDto;
import cn.kt.mapstructdemo.service.mapstruct.UserMapper;
import org.apache.commons.beanutils.BeanUtils;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * Created by tao.
 * Date: 2021/9/7 16:05
 * 描述:
 */
@SpringBootTest
public class TransferUtilsTest {
    @Autowired
    private UserMapper userMapper;

    @Test
    void contextLoads() {
        //这里拿100w数据做数据初始化
        List<User> userList = new ArrayList<User>();
        for (int i = 0; i < 100000; i++) {
            User user = new User(UUID.randomUUID().toString(), 22, UUID.randomUUID().toString());
            userList.add(user);
        }
        System.out.println("userList.size():" + userList.size());
        System.out.println("开始拷贝---------------------------------------");
        testBeanUtils(userList);
        testSpringBeanUtils(userList);
        testMapStruct(userList);
    }

    /**
     * Apache的BeanUtils
     *
     * @param userList
     */
    public static void testBeanUtils(List<User> userList) {
        long start = System.currentTimeMillis();
        List<UserDto> userDtos = new ArrayList<>();
        userList.forEach(item -> {
            UserDto userDto = new UserDto();
            try {
                //对象拷贝语句:将item的属性值拷贝到userDto中
                BeanUtils.copyProperties(userDto, item);
                userDtos.add(userDto);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        long end = System.currentTimeMillis();
        System.out.println("拷贝对象数据集大小:" + userDtos.size() + "——>Apache的BeanUtils耗时:" + (end - start) + "ms");
    }

    /**
     * Spring的BeanUtils
     *
     * @param userList
     */
    public static void testSpringBeanUtils(List<User> userList) {
        long start = System.currentTimeMillis();
        List<UserDto> userDtos = new ArrayList<>();
        userList.forEach(item -> {
            UserDto userDto = new UserDto();
            try {
                //对象拷贝语句:将item的属性值拷贝到userDto中
                org.springframework.beans.BeanUtils.copyProperties(item, userDto);
                userDtos.add(userDto);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        long end = System.currentTimeMillis();
        System.out.println("拷贝对象数据集大小:" + userDtos.size() + "——>Spring的BeanUtils耗时:" + (end - start) + "ms");
    }

    /**
     * mapStruct拷贝
     *
     * @param userList
     */
    public void testMapStruct(List<User> userList) {
        long start = System.currentTimeMillis();
        //对象拷贝语句:MapStruct内置拷贝
        List<UserDto> userDtos = userMapper.toDto(userList);
        long end = System.currentTimeMillis();
        System.out.println("拷贝对象数据集大小:" + userDtos.size() + "——>mapStruct耗时:" + (end - start) + "ms");
    }
}

测试结果
1. 拷贝1000条数据测试结果:
Spring学习笔记(三十)——SpringBoot对象拷贝总结&Mapstruct-左眼会陪右眼哭の博客
2. 拷贝10000条数据测试结果
Spring学习笔记(三十)——SpringBoot对象拷贝总结&Mapstruct-左眼会陪右眼哭の博客
3. 拷贝100000条数据测试结果
Spring学习笔记(三十)——SpringBoot对象拷贝总结&Mapstruct-左眼会陪右眼哭の博客
4. 拷贝1000000条数据测试结果
Spring学习笔记(三十)——SpringBoot对象拷贝总结&Mapstruct-左眼会陪右眼哭の博客
可以看到拷贝一百万条数据MapStruct的耗时32ms,完胜有木有,数据量越大越能看到差异,所以MapStruct的性能不得不让人拍手说妙啊。

小结

由结果可以看出数据量越大MapStruct>Spring>Apache,这个性能优势越来越明显,日常开发中对象拷贝只是代码中的一小部分逻辑,如果数据量大的话还是建议大家使用MapStruct的方式,提高接口的性能。数据量不大的话Spring的BeanUtils也行,还是看实际业务场景吧!!!

源码下载

链接:https://pan.baidu.com/s/1RL25QwUGzVKWObZlMowH3w
提取码:m13f

0 条回应
在线人数:1人
隐藏