MapStruct - Java对象转换器

Posted by KANG's BLOG on Tuesday, March 15, 2022

如何干掉BeanUtils?

写业务代码时,为了保证service的复用和逻辑关系,我们通常需要保持良好的分层结构,比如controller -> service -> dao,职责分明,复用性强,但是带来的问题就是频繁的对象转换。

有一种方式就是每两个对象的转化都编写对应的converter方法,方法内部直接新建对象并为其赋值。这样做最大问题就是代码量骤增,虽然我们可以放在assembler中保持service代码干净,但依旧避免不了开发成本高的问题,而且后期DO变化时,项目中所有的converter都需要修改。

还有一种解决方案是BeanUtils的对象复制功能,但它是在运行期通过反射做的对象转换,性能非常差。

有没有一种方法在编译器动态进行对象转化呢?

MapStruct介绍

MapStruct是一个在编译器进行代码增强来帮你进行对象转换的工具。

这里放上GitHub的说明:

MapStruct is a Java annotation processor for the generation of type-safe and performant mappers for Java bean classes.

如何使用

Maven坐标

<properties>
    <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.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>

代码实例

我们有两个类CarCarDto,声明如下:

@Data
public class Car { 
    private String make;
    private int numberOfSeats;
    private CarType type;
}
@Data
public class CarDto {
     private String make;
     private int seatCount;
     private String type;
 }

定义一个类型转化接口:

@Mapper
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class ); 
 
    @Mapping(source = "numberOfSeats", target = "seatCount")
    CarDto carToCarDto(Car car);
}

接下来我们看看如何使用这个接口:

@Test
public void shouldMapCarToDto() {
    //given
    Car car = new Car( "Morris", 5, CarType.SEDAN);
 
    //when
    CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
 
    //then
    assertThat(carDto).isNotNull();
    assertThat(carDto.getMake()).isEqualTo("Morris");
    assertThat(carDto.getSeatCount()).isEqualTo(5);
    assertThat(carDto.getType()).isEqualTo("SEDAN");
}

打开编译后的class文件,发现自动帮我们把方法生成好了,所以不会有反射实现方式造成的性能问题。

img

与lombok的冲突

1. 转化的对象所有属性都是空

MapStruct生成的代码中,属性赋值部分依赖于原类中的set/get方法,所以当MapStruct和Lombok同时使用时,如果MapStruct先于Lombok编织代码,由于此时类中还不存在set/get方法,所以会出现new了一个空对象的现象。

解决方式为使Lombok在MapStruct之前引用或者增加maven编译时的执行顺序。

2. 转化的对象没有对父类属性进行赋值

MapStruct默认使用builder模式,而lombok的build没有父类属性(除非父子类均加上@SuperBuilder注解,而这样做代码侵入较大)

解决方式为在@Mapper注解中手动关闭builder方式

@Mapper(builder = @Builder(disableBuilder = true))