【Java生态圈技术总结】之深度剖析MapStruct对象拷贝工具(上)

简介: 【Java生态圈技术总结】之深度剖析MapStruct对象拷贝工具(上)

正文


一、常用的对象拷贝工具基本介绍


属性拷贝工具有很多,也许你用过如下的一些:

  • Apache commons-beanutils
  • Spring BeanUtils
  • cglib BeanCopier
  • HuTool BeanUtils
  • MapStruct
  • getter & setter


这些属性拷贝工具各自有什么特点和区别?在日常开发使用中,我们该如何做出选择?


1.1 Apache BeanUtils


参数顺序和其它的工具正好相反,导致使用不顺手,容易产生问题;

阿里巴巴代码扫描插件会给出明确的告警;

基于反射实现,性能较差;

不推荐使用;


1.2 Spring BeanUtils


基于内省+反射,借助getter/setter方法实现属性拷贝,性能比apache高;

在简单的属性拷贝场景下推荐使用;


1.3 cglib BeanCopier


通过动态代理的方式来实现属性拷贝;

性能高效;

在简单的属性拷贝场景下推荐使用;


1.4 HuTool BeanUtils


性能介于apache和Spring之间;

需要额外引入HuTool的依赖;


1.5 MapStruct


基于getter/setter方法实现属性拷贝,在编译时自动生成实现类的代码;

性能媲美getter & setter;

强大的功能可以实现深度拷贝;

缺点是需要声明bean的转换接口类;


1.6 getter & setter


性能最高,但是需要手动拷贝;


1.7 总结


经过第三方的对比结果,总的下来,推荐使用顺序为:

apache < HuTool < Spring < cglib < Mapstruct


二、使用介绍


2.1 准备工作


<!-- 导入MapStruct的核心注释 -->
<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>
...
<!-- MapStruct在编译时工作,并且会集成到像Maven和Gradle这样的构建工具上,我们还必须在<build中/>标签中添加一个插件maven-compiler-plugin,并在其配置中添加annotationProcessorPaths,该插件会在构建时生成对应的代码。 -->
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>


2.2 映射


2.2.1 基本映射


我们现在需要实现一个场景,Car是一个domain层的对象实例,在从数据库读取出来后传递给service层需要转换为CarDTO,这两个实例的所有属性全部相同,现在需要使用mapstruct来完成这个目标。


public class Car {
    private String brand;
    private Double price;
    private Boolean onMarket;
    ...
    // setters + getters + toString
}
public class CarDTO {
    private String brand;
    private Double price;
    private Boolean onMarket;
    ...
    // setters + getters + toString
}

我们需要新建一个Mapper接口,来映射这两个对象之间的属性。

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    CarDTO carToCarDTO(Car car);
}

然后就可以进行测试了:

@Test
public void test1(){
    Car wuling = new Car();
    wuling.setBrand("wuling");
    wuling.setPrice(6666.66);
    wuling.setOnMarket(true);
    CarDTO wulingDTO = CarMapper.INSTANCE.carToCarDTO(wuling);
    // 结果为:Car{brand='wuling', price=6666.66, onMarket=true}
    System.out.println("结果为:" + wulingDTO);
}

可以看到,mapstruct很好地完成了我们的目标,那么它是如何做到的呢?我们查看CarMapper.INSTANCE.carToCarDTO(wuling)的实现类,可以看到在编译过程中自动生成了如下内容的接口实现类:

public class CarMapperImpl implements CarMapper {
    @Override
    public CarDTO carToCarDTO(Car car) {
        if ( car == null ) {
            return null;
        }
        CarDTO carDTO = new CarDTO();
        carDTO.setBrand( car.getBrand() );
        carDTO.setPrice( car.getPrice() );
        carDTO.setOnMarket( car.getOnMarket() );
        return carDTO;
    }
}

所以,mapstruct并没有使用反射的机制,而是使用了普通的set和get方法来进行属性拷贝的,因此要求我们的对象也一定要有set和get方法。


2.2.2 不同属性名映射


在如上示例中,我们源对象和目标对象的属性名称全都一致,但是在很多的场景下,源对象和目标对象的同一个字段很可能名称是不同的,这种情况下,只需要在映射接口类中指定即可:


public class Car {
    ...
    private Boolean onMarket;
    ...
    // setters + getters + toString
}
public class Car {
    ...
    private Boolean onMarket;
    ...
    // setters + getters + toString
}
@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    @Mapping(source = "car.onMarket", target = "onSale")
    CarDTO carToCarDTO(Car car);
}

如此,生成的接口实现类如下:

@Override
public CarDTO carToCarDTO(Car car) {
    if ( car == null ) {
        return null;
    }
    CarDTO carDTO = new CarDTO();
    carDTO.setOnSale( car.getOnMarket() );
    carDTO.setBrand( car.getBrand() );
    carDTO.setPrice( car.getPrice() );
    return carDTO;
}


2.2.3 不同个数属性映射


我们假设Car和CarDTO各有一个对方没有的属性,那么在进行对象拷贝时会发生什么?


public class Car {
    ...
    private Date birthdate;
    // setters + getters + toString
}
public class CarDTO {
    ...
    private String owner;
    // setters + getters + toString
}

p

@Test
public void test1(){
    Car wuling = new Car();
    wuling.setBrand("wuling");
    wuling.setPrice(6666.66);
    wuling.setOnMarket(true);
    wuling.setBirthdate(new Date());
    CarDTO wulingDTO = CarMapper.INSTANCE.carToCarDTO(wuling);
    System.out.println("结果为:" + wulingDTO);
}

然后我们执行如上转换的案例,发现并没有报错,从Car拷贝属性到CarDTO时,CarDTO由于没有birthdate属性,则不会赋值;同时,CarDTO的owner因为Car中没有,因此也不会被赋值,生成的接口实现类如下:

@Override
public CarDTO carToCarDTO(Car car) {
    if ( car == null ) {
        return null;
    }
    CarDTO carDTO = new CarDTO();
    carDTO.setOnSale( car.getOnMarket() );
    carDTO.setBrand( car.getBrand() );
    carDTO.setPrice( car.getPrice() );
    return carDTO;
}

因此,mapstruct只会对共有的交集属性进行拷贝操作。


2.2.4 多个源合并映射


我们新增一个Person类,其中的name属性对应CarDTO中的owner属性。

public class Person {
    private String name;
    private String age;
    // setters + getters + toString
}
@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    @Mapping(source = "car.onMarket", target = "onSale")
    @Mapping(source = "person.name", target = "owner")
    CarDTO carToCarDTO(Car car, Person person);
}

自动生成的接口实现类如下:

@Override
public CarDTO carToCarDTO(Car car, Person person) {
    if ( car == null && person == null ) {
        return null;
    }
    CarDTO carDTO = new CarDTO();
    if ( car != null ) {
        carDTO.setOnSale( car.getOnMarket() );
        carDTO.setBrand( car.getBrand() );
        carDTO.setPrice( car.getPrice() );
    }
    if ( person != null ) {
        carDTO.setOwner( person.getName() );
    }
    return carDTO;
}


2.2.5 子对象映射


如果需要转换的Car对象中的某个属性不是基本数据类型,而是一个对象怎么处理呢。

public class Person {
    private String name;
    private String age;
    // setters + getters + toString
}
public class PersonDTO {
    private String name;
    private String age;
    // setters + getters + toString
}
public class Car {
    private String brand;
    private Double price;
    private Boolean onMarket;
    private Person owner;
    // setters + getters + toString
}
public class CarDTO {
    private String brand;
    private Double price;
    private Boolean onMarket;
    private PersonDTO owner;
    // setters + getters + toString
}
@Mapper
public interface PersonMapper {
    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
    PersonDTO personToPersonDTO(Person person);
}
@Mapper(uses = {PersonMapper.class})
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    @Mapping(source = "onMarket", target = "onSale")
    CarDTO carToCarDTO(Car car);
}
@Test
public void test1(){
    Person jack = new Person();
    jack.setName("jack");
    jack.setAge("22");
    Car wuling = new Car();
    wuling.setBrand("wuling");
    wuling.setPrice(6666.66);
    wuling.setOnMarket(true);
    wuling.setOwner(jack);
    CarDTO wulingDTO = CarMapper.INSTANCE.carToCarDTO(wuling);
    // 结果为:CarDTO{brand='wuling', price=6666.66, onSale=true, owner=Person{name='jack', age='22'}}
    System.out.println("结果为:" + wulingDTO);
}

这里最重要的是:

  • 需要增加PersonMapper接口,让mapstruct能够对Person和PersonDTO进行转换;
  • CarMapper中需要引入PersonMapper,如果存在多个对象属性,此处就要引入多个对象属性的Mapper接口;
相关文章
|
10天前
|
Java
java基础(12)抽象类以及抽象方法abstract以及ArrayList对象使用
本文介绍了Java中抽象类和抽象方法的使用,以及ArrayList的基本操作,包括添加、获取、删除元素和判断列表是否为空。
13 2
java基础(12)抽象类以及抽象方法abstract以及ArrayList对象使用
|
6天前
|
Java
java小工具util系列4:基础工具代码(Msg、PageResult、Response、常量、枚举)
java小工具util系列4:基础工具代码(Msg、PageResult、Response、常量、枚举)
20 5
|
5天前
|
Java 数据库
java小工具util系列1:日期和字符串转换工具
java小工具util系列1:日期和字符串转换工具
15 3
|
6天前
|
Java
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
18 4
|
11天前
|
存储 Java
Java编程中的对象和类
【8月更文挑战第55天】在Java的世界中,“对象”与“类”是构建一切的基础。就像乐高积木一样,类定义了形状和结构,而对象则是根据这些设计拼装出来的具体作品。本篇文章将通过一个简单的例子,展示如何从零开始创建一个类,并利用它来制作我们的第一个Java对象。准备好让你的编程之旅起飞了吗?让我们一起来探索这个神奇的过程!
25 10
|
5天前
|
SQL 安全 Java
JAVA代码审计SAST工具使用与漏洞特征
JAVA代码审计SAST工具使用与漏洞特征
18 1
|
5天前
|
SQL Java 索引
java小工具util系列2:字符串工具
java小工具util系列2:字符串工具
8 2
|
7天前
|
JSON Java fastjson
java小工具util系列3:JSON和实体类转换工具
java小工具util系列3:JSON和实体类转换工具
11 2
|
8天前
|
Java
Java-FileInputStream和FileOutputStream的使用,txt文件及图片文件的拷贝
这篇文章介绍了Java中FileInputStream和FileOutputStream的使用,包括如何读取和写入txt文件以及如何拷贝图片文件。
Java-FileInputStream和FileOutputStream的使用,txt文件及图片文件的拷贝
|
14天前
|
存储 Java 开发者
Java编程中的对象序列化与反序列化
【9月更文挑战第20天】在本文中,我们将探索Java编程中的一个核心概念——对象序列化与反序列化。通过简单易懂的语言和直观的代码示例,你将学会如何将对象状态保存为字节流,以及如何从字节流恢复对象状态。这不仅有助于理解Java中的I/O机制,还能提升你的数据持久化能力。准备好让你的Java技能更上一层楼了吗?让我们开始吧!
下一篇
无影云桌面