MapStruct对象拷贝工具介绍

2022-01-19  本文已影响0人  文景大大

一、基本介绍

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

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

1.1 Apache BeanUtils

1.2 Spring BeanUtils

1.3 cglib BeanCopier

1.4 HuTool BeanUtils

1.5 MapStruct

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 CarDTO {
    ...
    private Boolean onSale;
    ...
    // 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
}
    @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);
}
public class TestMapper1 {

    @Test
    public void test1(){
        Car wuling = new Car();
        wuling.setBrand("wuling");
        wuling.setPrice(6666.66);
        wuling.setOnMarket(true);
        wuling.setBirthdate(new Date());

        Person jack = new Person();
        jack.setName("jack");
        jack.setAge("22");

        CarDTO wulingDTO = CarMapper.INSTANCE.carToCarDTO(wuling, jack);
        // 结果为:CarDTO{brand='wuling', price=6666.66, onSale=true, owner='jack'}
        System.out.println("结果为:" + wulingDTO);
    }

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

    @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);
}

这里最重要的是:

2.2.6 集合属性映射

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

public class Car {
    private String brand;
    private Double price;
    private Boolean onMarket;
    private List<Person> ownerList;
    
    // setters + getters + toString
}

同2.2.5内容。

2.2.7 枚举映射

枚举映射的工作方式与字段映射相同。MapStruct会对具有相同名称的枚举进行映射。

public enum PayType {
    CASH,
    ALIPAY,
    WEPAY,
    DIGITAL_CASH,
    CARD_VISA,
    CARD_CREDIT;
}

public enum PayTypeNew {
    CASH,
    ALIPAY,
    WEPAY,
    DIGITAL_CASH,
    CARD_VISA,
    CARD_CREDIT;
}
@Mapper
public interface PayTypeMapper {
    PayTypeMapper INSTANCE = Mappers.getMapper(PayTypeMapper.class);

    PayTypeNew payTypeToPayTypeNew(PayType payType);
}
    @Test
    public void test2(){
        PayType p1 = PayType.ALIPAY;

        PayTypeNew p2 = PayTypeMapper.INSTANCE.payTypeToPayTypeNew(p1);
        // 结果为:ALIPAY
        System.out.println("结果为:" + p2);
    }

但是在更多的场景下,源枚举和目标枚举并不是一一对应的,比如目标枚举如下:

public enum PayTypeNew {
    CASH,
    NETWORK,
    CARD;
}

此时,我们就需要手动指定源枚举和目标枚举之间的对应关系:

@Mapper
public interface PayTypeMapper {
    PayTypeMapper INSTANCE = Mappers.getMapper(PayTypeMapper.class);

    @ValueMappings({
            @ValueMapping(source = "ALIPAY", target = "NETWORK"),
            @ValueMapping(source = "WEPAY", target = "NETWORK"),
            @ValueMapping(source = "DIGITAL_CASH", target = "CASH"),
            @ValueMapping(source = "CARD_VISA", target = "CARD"),
            @ValueMapping(source = "CARD_CREDIT", target = "CARD")
    })
    PayTypeNew payTypeToPayTypeNew(PayType payType);
}

如果对应CARD的场景比较多,手动一个个地对应会比较繁琐,因此还有一种方式能实现相同的效果,而且比较简洁:

@Mapper
public interface PayTypeMapper {
    PayTypeMapper INSTANCE = Mappers.getMapper(PayTypeMapper.class);

    @ValueMappings({
            @ValueMapping(source = "ALIPAY", target = "NETWORK"),
            @ValueMapping(source = "WEPAY", target = "NETWORK"),
            @ValueMapping(source = "DIGITAL_CASH", target = "CASH"),
            @ValueMapping(source = MappingConstants.ANY_REMAINING, target = "CARD")
    })
    PayTypeNew payTypeToPayTypeNew(PayType payType);
}

MappingConstants.ANY_REMAINING表示剩下其它的源枚举和目标枚举对应不上的全部映射为指定的枚举对象。

还有一种方式,使用MappingConstants.ANY_UNMAPPED表示所有未显示指定目标枚举的都会被映射为CARD:

@Mapper
public interface PayTypeMapper {
    PayTypeMapper INSTANCE = Mappers.getMapper(PayTypeMapper.class);

    @ValueMappings({
            @ValueMapping(source = "ALIPAY", target = "NETWORK"),
            @ValueMapping(source = "WEPAY", target = "NETWORK"),
            @ValueMapping(source = "DIGITAL_CASH", target = "CASH"),
            @ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "CARD")
    })
    PayTypeNew payTypeToPayTypeNew(PayType payType);
}

2.2.8 集合映射

如果源对象和目标对象都是集合,且对象中的属性都是基本数据类型,则映射方法和之前类似,映射接口改为如下即可:

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);

    List<CarDTO> carToCarDTOList(List<Car> carList);

    Set<CarDTO> carToCarDTOSet(Set<Car> carSet);

    Map<String, CarDTO> carToCarDTOMap(Map<String, Car> carMap);
}

如果对象中属性不仅是基本数据类型,还有对象类型或对象类型的集合类型的话,mapstruct也是支持映射的,详情参考官网文档,此处不再赘述。

2.3 转换

2.3.1 类型转换

mapstruct提供了基本数据类型和包装数据类型、一些常见场景下的自动转换;

2.3.2 格式转换

mapstruct可以对源对象的属性值进行格式化之后拷贝给目标对象的属性;

2.4 高级特性

2.4.1 依赖注入

在前面例子中的Mapper映射接口中,我们都需要CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);来创建一个实例,如果我们是Spring工程的话,就可以把这个实例托管给Spring进行管理。

@Mapper(componentModel = "spring")
public interface CarMapper {
    List<CarDTO> carToCarDTOList(List<Car> carList);
}

然后使用的时候,从Spring中自动注入接口对象即可:

@SpringBootTest
public class TestMapper1 {
    @Autowired
    private CarMapper carMapper;

    @Test
    public void test1(){
        Car wuling = new Car();
        wuling.setBrand("wuling");
        wuling.setPrice(6666.66);
        wuling.setMarketDate(LocalDate.now());
        Car changan = new Car();
        changan.setBrand("changan");
        changan.setPrice(7777.77);
        changan.setMarketDate(LocalDate.now());
        List<Car> carList = new ArrayList<>();
        carList.add(wuling);
        carList.add(changan);

        List<CarDTO> carDTOList = carMapper.carToCarDTOList(carList);
        System.out.println("结果为:" + carDTOList);
    }
}

2.4.2 设置默认值

2.4.3 使用表达式

mapstruct甚至允许在对象属性映射中使用java表达式:

@Mapper(componentModel = "spring", imports = {UUID.class, LocalDateTime.class})
public interface PersonMapper {

    @Mapping(target = "id", expression = "java(UUID.randomUUID().toString())")
    @Mapping(source = "birthdate", target = "birthdate", defaultExpression = "java(LocalDateTime.now())")
    PersonDTO personToPersonDTO(Person person);
}

或者等价写法为:

@Mapper(componentModel = "spring")
public interface PersonMapper {

    @Mapping(target = "id", expression = "java(java.util.UUID.randomUUID().toString())")
    @Mapping(source = "birthdate", target = "birthdate", defaultExpression = "java(java.time.LocalDateTime.now())")
    PersonDTO personToPersonDTO(Person person);
}

2.4.4 前置及后置方法

@Mapper(componentModel = "spring")
public abstract class PersonMapper {

    @BeforeMapping
    public void before(Person person){
        System.out.println("前置处理!!!");
        if(ObjectUtils.isEmpty(person.getName())){
            System.out.println("Person的name不能为空!");
            return;
        }
    }

    @Mapping(target = "id", expression = "java(java.util.UUID.randomUUID().toString())")
    @Mapping(source = "birthdate", target = "birthdate", defaultExpression = "java(java.time.LocalDateTime.now())")
    public abstract PersonDTO personToPersonDTO(Person person);

    @AfterMapping
    public void after(@MappingTarget PersonDTO personDTO){
        System.out.println("后置处理:" + personDTO.getName() + "!!!");
    }
}

三、参考文献

MapStruct 1.4.2.Final Reference Guide

MapStruct使用指南 - 知乎 (zhihu.com)

常见Bean拷贝框架使用姿势及性能对比 - 知乎 (zhihu.com)

四、补充填坑

在正文的实例中,我们对于Bean对象都是使用手动写getter、setter、toString方法的,但是在真实开发中,大家都是采用了Lombok插件,如果不做特殊配置,就会出现mapstruct运行时lombok不生效的问题,只需要在pom配置中增加如下内容:

<annotationProcessorPaths>
  <path>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>${lombok.version}</version>
   </path>                    
</annotationProcessorPaths>

具体原理和详情可以参考:当 Lombok 遇见了 MapStruct の「坑」 - 知乎 (zhihu.com)

上一篇下一篇

猜你喜欢

热点阅读