当 Lombok 遇见了 MapStruct の「坑」

5,159 阅读3分钟

前言

2021 年了,相信搞 Java 的小伙伴们不会还没有人没用过 Lombok 吧?

Lombok 是一款通过「注解」的形式简化并消除冗余代码的 Java 插件,利用「Annotation Processor」原理,在编译时生成一些「重复」代码。另外需要注意的是,在 IDEA 环境下,需要额外安装一个 Lombok 插件。(本文不会专门介绍 Lombok 的使用方法,想要深入学习的小伙伴可以去 官方文档 学习 Lombok 提供的所有注解的使用方法。)

可能一些朋友对 MapStruct 就有点陌生了,但是我敢肯定的是,你们一定用过和他功能类似的工具。比如 Apache Commons BeanUtils、Spring BeanUtils、BeanCopier、Dozer 等等。没错,MapStruct 也是为了解决对象属性拷贝这一个通用需求的。传统使用「反射」进行属性拷贝的方式,在大数据量的场景下,性能低下,效率堪忧。MapStruct 底层则是通过 getter/setter 的方式提升属性拷贝的性能的,跟 Lombok 一样利用「Annotation Processor」的原理,在编译时生成代码。

踩坑

首先我们按照 MapStruct 官方文档介绍,搭一个简单的栗子🌰~

引入依赖:

<properties>
    <mapstruct.version>1.4.2.Final</mapstruct.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${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>
                    <!-- MapStruct 在编译时会通过这个插件生成代码 -->
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

创建两个类,用于属性拷贝。

// 源对象
public class CarDO {
    private String make;
    private int numberOfSeats;
    private CarType type;

    public CarDO() {
    }

    public CarDO(String make, int numberOfSeats, CarType type) {
        this.make = make;
        this.numberOfSeats = numberOfSeats;
        this.type = type;
    }

    public String getMake() {
        return make;
    }

    public void setMake(String make) {
        this.make = make;
    }

    public int getNumberOfSeats() {
        return numberOfSeats;
    }

    public void setNumberOfSeats(int numberOfSeats) {
        this.numberOfSeats = numberOfSeats;
    }

    public CarType getType() {
        return type;
    }

    public void setType(CarType type) {
        this.type = type;
    }
}

// 测试枚举
public enum CarType {
    /**
     * 普通
     */
    COMMON,
    /**
     * 老爷车
     */
    OLD,
    /**
     * 跑车
     */
    SPORTS;
}

// 目标对象
public class CarDTO {
    private String make;
    private int seatCount;
    private String type;

    public CarDTO() {
    }

    public CarDTO(String make, int seatCount, String type) {
        this.make = make;
        this.seatCount = seatCount;
        this.type = type;
    }

    public String getMake() {
        return make;
    }

    public void setMake(String make) {
        this.make = make;
    }

    public int getSeatCount() {
        return seatCount;
    }

    public void setSeatCount(int seatCount) {
        this.seatCount = seatCount;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
}

创建一个对象拷贝接口,用于告诉 MapStruct 需要生成哪个对象的属性拷贝。

注意:这里 @Mapper / Mappers / @Mapping 都是 org.mapstruct 包下的,当和 Mybatis 一起使用时,不要引用错了~

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

    @Mapping(source = "numberOfSeats", target = "seatCount")
    CarDTO carToCarDto(CarDO car);
}

最后创建测试类,测试一把!

public class TestMapStruct {
    @Test
    public void shouldMapCarToDto() {
        //given
        CarDO car = new CarDO("Morris", 5, CarType.SPORTS);

        //when
        CarDTO carDto = CarMapper.INSTANCE.carToCarDto(car);

        //then
        Assert.assertNotNull(carDto);
        Assert.assertEquals("Morris", carDto.getMake());
        Assert.assertEquals(5, carDto.getSeatCount());
        Assert.assertEquals("SPORTS", carDto.getType());
    }
}

按照官方文档的做法,十分顺利,测试通过!

如果我们把 2 个实体类里的 getter/setter 替换成 Lombok,会发生什么事情呢?

我们先改造 pom.xml 文件,增加 Lombok 相关依赖:

<properties>
+    <lombok.version>1.18.12</lombok.version>
    <mapstruct.version>1.4.2.Final</mapstruct.version>
</properties>

<dependencies>
+    <dependency>
+        <groupId>org.projectlombok</groupId>
+        <artifactId>lombok</artifactId>
+        <version>${lombok.version}</version>
+    </dependency>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${mapstruct.version}</version>
    </dependency>
</dependencies>

此时,我们的实体类代码应该是这样子的:

// 源对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CarDO {
    private String make;
    private int numberOfSeats;
    private CarType type;
}

// 目标对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CarDTO {
    private String make;
    private int seatCount;
    private String type;
}

再来执行一遍测试类,编译时就会出现类似这种「java: 找不到符号 符号: 方法 getXXX()」错误提示,具体如下图:

异常

当出现这种错误,分明就是 Lombok 没有生效嘛,那么该怎么解决这个问题呢?

解坑

还记得文章开头介绍的,Lombok 和 MapStruct 都是利用「Annotation Processor」在程序编译时生成代码的吗?

了解原理,问题就容易解决了。

前文我们在测试 MapStruct 的时候,在 pom.xml 文件中添加了一个一个插件,用于告诉 Maven 编译时,需要额外执行 MapStruct 的代码生成逻辑,但是我们没有告诉 Maven 在编译时,Lombok 也需要生成代码。

我们在 pom.xml 文件的 build 节点中加上这么一段:

<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>
                    <!-- Lombok 在编译时会通过这个插件生成代码 -->
+                    <path>
+                        <groupId>org.projectlombok</groupId>
+                        <artifactId>lombok</artifactId>
+                        <version>${lombok.version}</version>
+                    </path>
                    <!-- MapStruct 在编译时会通过这个插件生成代码 -->
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

这个时候,再去运行一下通过测试类,就不会出现上述找不到 get 方法的错误了~


创建时间:2021-04-22 14:01:21

原文链接:xkcoding.com/2021/04/22/…

配套代码:github.com/xkcoding/pr…