Spring Boot MapStruct Lombok không sử dụng Maven Compiler Plugin
Giới thiệu
Các bạn đã quá mệt mõi với việc sử dụng MapStruct trong dự án Spring Boot của mình khi nó:
- Thường xuyên thông báo lỗi compile với Lombok.
- Config thủ công và rườm rà.
Đừng lo lẵng nữa bài viết này sẽ giúp các bạn giải thoát khỏi những phiền toái đó 🥳.
Hướng giải quyết
Các bạn tạo dự án Spring Boot bằng Spring Initializr nhé Tải dự án xuống và mở bằng IDE (ở đây mình sử dụng Intellij IDEA)
Dựa theo trang chủ của MapStruct, các bạn copy dependencies vào file pom.xml nhé.
<properties>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
Dependency trên có vai trò cung cấp các Annotation, chúng ta sử dụng chúng để thiết lập những Mapper Utilities logic.
Các bạn có thể thấy trên trang chủ của MapStruct họ có gợi ý chúng ta sử dụng Maven Compiler Plugin để quét các MapStruct Annotation xuất hiện trong dự án, tuy nhiên theo mình đánh giá cách này không thuật tiện trong dự án Spring Boot , thế nên chúng ta sẽ sử dụng MapStruct Annotation Processor để cài đặt MapStruct nhé.
Nguyên lý hoạt động của nó sẽ giống với Lombok Spring Boot Starter, các bạn có thể research để tìm hiểu thêm.
Các bạn thêm dependency này vào dự án của mình nhé, nó sẽ hoạt động cùng với dependency ở trên.
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
Cuối cùng file pom.xml của chúng ta sẽ có cấu trúc như thế này.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>io.huyvu</groupId>
<artifactId>mapstruct</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mapstruct</name>
<description>Demo project for Spring Boot x MapStruct x Lombok</description>
<properties>
<java.version>21</java.version>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Viết một vài MapStruct mapper để kiểm tra kết quả nào.
.
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── io
│ │ └── huyvu
│ │ └── mapstruct
│ │ ├── MapstructApplication.java
│ │ ├── UserDTO.java
│ │ ├── UserEntity.java
│ │ └── UserMapper.java
│ └── resources
│ └── application.properties
└── test
└── java
└── io
└── huyvu
└── mapstruct
└── MapstructApplicationTests.java
13 directories, 10 files
package io.huyvu.mapstruct;
public class UserDTO {
private Long id;
private String username;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
package io.huyvu.mapstruct;
public class UserEntity {
private Long userId;
private String username;
public Long getUserId() {
return this.userId;
}
public String getUsername() {
return this.username;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public void setUsername(String username) {
this.username = username;
}
}
package io.huyvu.mapstruct;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper( UserMapper.class );
@Mapping(source = "id", target = "userId")
UserEntity toEntity(UserDTO dto);
}
Build dự án bằng lệnh
mvn clean package
Kết quả
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.897 s -- in io.huyvu.mapstruct.MapstructApplicationTests
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- jar:3.3.0:jar (default-jar) @ mapstruct ---
[INFO] Building jar: /Users/mac/Downloads/mapstruct/target/mapstruct-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot:3.2.5:repackage (repackage) @ mapstruct ---
[INFO] Replacing main artifact /Users/mac/Downloads/mapstruct/target/mapstruct-0.0.1-SNAPSHOT.jar with repackaged archive, adding nested dependencies in BOOT-INF/.
[INFO] The original artifact has been renamed to /Users/mac/Downloads/mapstruct/target/mapstruct-0.0.1-SNAPSHOT.jar.original
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.760 s
[INFO] Finished at: 2024-05-02T22:09:43+07:00
[INFO] ------------------------------------------------------------------------
.
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
├── src
└── target
├── classes
├── generated-sources
│ └── annotations
│ └── io
│ └── huyvu
│ └── mapstruct
│ └── UserMapperImpl.java
├── generated-test-sources
├── mapstruct-0.0.1-SNAPSHOT.jar
├── mapstruct-0.0.1-SNAPSHOT.jar.original
├── maven-archiver
├── maven-status
Hãy để ý file UserMapperImpl.java, file này được MapStruct tạo ra dựa trên Interface UserMapper.java trong dự án. Điều này chứng tỏ MapStruct đã hoạt động đúng 👏.
package io.huyvu.mapstruct;
import javax.annotation.processing.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2024-05-02T22:09:41+0700",
comments = "version: 1.5.5.Final, compiler: javac, environment: Java 21.0.3 (Oracle Corporation)"
)
public class UserMapperImpl implements UserMapper {
@Override
public UserEntity toEntity(UserDTO dto) {
if ( dto == null ) {
return null;
}
UserEntity userEntity = new UserEntity();
userEntity.setUserId( dto.getId() );
userEntity.setUsername( dto.getUsername() );
return userEntity;
}
}
UserMapper.java đã hoàn thành công việc chuyển đổi từ lớp UserDTO thành lớp UserEntity.
Tuy nhiên nếu để ý các bạn sẽ thấy 2 lớp POJO trên mình tạo Getter, Setter theo cách thủ công.
Chúng ta hãy thử sử dụng Lombok để tự động tạo Getter Setter xem chuyện gì sẽ sảy ra.
package io.huyvu.mapstruct;
import lombok.*;
@Data
public class UserEntity {
private Long userId;
private String username;
}
package io.huyvu.mapstruct;
import lombok.*;
@Data
public class UserDTO {
private Long id;
private String username;
}
Build lại dự án bằng lệnh:
mvn clean package
Kết quả
[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] /Users/mac/Downloads/mapstruct/src/main/java/io/huyvu/mapstruct/UserMapper.java:[11,23] No property named "id" exists in source parameter(s). Type "UserDTO" has no properties.
[INFO] 1 error
[INFO] -------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.952 s
[INFO] Finished at: 2024-05-02T22:22:09+07:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile (default-compile) on project mapstruct: Compilation failure
[ERROR] /Users/mac/Downloads/mapstruct/src/main/java/io/huyvu/mapstruct/UserMapper.java:[11,23] No property named "id" exists in source parameter(s). Type "UserDTO" has no properties.
[ERROR]
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
Các bạn cũng đã thấy, Maven trả lỗi với nội dung không tìm thấy property với tên 'id' trong đối tượng nguồn (ý chỉ source trong UserMapper - UserDTO).
Lý giải cho điều này đó chính là do trong quá trình biên dịch, MapStruct dựa vào các Getter hoặc Setter để tìm các properties của đối tượng nguồn.
Thay vì sử dụng Getter Setter được cấu hình thủ công như lúc đầu, chúng ta đã sử dụng Lombok để thay thế, vì một lý do nào đó MapStruct đã không nhận diện được các Getter Setter do Lombok tạo ra.
Chúng ta hãy xem lại file cấu hình dependencies pom.xml
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Rõ ràng nhận ra về mặt thứ tự, dependency MapStruct được cấu hình trước so với dependency Lombok.
MapStruct và Lombok sử dụng chung một cơ chế hook vào kỳ biên dịch của Java Compiler.
Thế nên thứ tự sắp xếp các dependencies trong dự án cũng sẽ ảnh hưởng đến thứ tự hoạt động của chúng.
Điều này sẽ dẫn đến lỗi trên tức MapStruct không thể nhận diện được những Getter Setter do Lombok tạo ra.
Cách giải quyết cực kỳ đơn giản, chúng ta chỉ cần đảo ngược thứ tự của chúng.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Build lại dự án và xem kết quả
mvn clean build
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.897 s -- in io.huyvu.mapstruct.MapstructApplicationTests
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- jar:3.3.0:jar (default-jar) @ mapstruct ---
[INFO] Building jar: /Users/mac/Downloads/mapstruct/target/mapstruct-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot:3.2.5:repackage (repackage) @ mapstruct ---
[INFO] Replacing main artifact /Users/mac/Downloads/mapstruct/target/mapstruct-0.0.1-SNAPSHOT.jar with repackaged archive, adding nested dependencies in BOOT-INF/.
[INFO] The original artifact has been renamed to /Users/mac/Downloads/mapstruct/target/mapstruct-0.0.1-SNAPSHOT.jar.original
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.819 s
[INFO] Finished at: 2024-05-02T22:39:27+07:00
[INFO] ------------------------------------------------------------------------
.
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
├── src
└── target
├── classes
├── generated-sources
│ └── annotations
│ └── io
│ └── huyvu
│ └── mapstruct
│ └── UserMapperImpl.java
├── generated-test-sources
├── mapstruct-0.0.1-SNAPSHOT.jar
├── mapstruct-0.0.1-SNAPSHOT.jar.original
├── maven-archiver
├── maven-status
├── surefire-reports
└── test-classes
37 directories, 27 files
Hãy cùng kiểm tra lại file UserMapperImpl.java nhé.
package io.huyvu.mapstruct;
import javax.annotation.processing.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2024-05-02T22:39:25+0700",
comments = "version: 1.5.5.Final, compiler: javac, environment: Java 21.0.3 (Oracle Corporation)"
)
public class UserMapperImpl implements UserMapper {
@Override
public UserEntity toEntity(UserDTO dto) {
if ( dto == null ) {
return null;
}
UserEntity userEntity = new UserEntity();
userEntity.setUserId( dto.getId() );
userEntity.setUsername( dto.getUsername() );
return userEntity;
}
}
Tadaaa, nó đã hoạt động trở lại rồi 🥳.
Các bạn có thể thấy cách triển khai dự án của mình rất gọn gàng và đơn giản, không như những example khác đưa một đống Maven Compiler Plugin vào làm conflict một đống haha 🤣.
Mình mong bài viết này sẽ giúp ích cho các bạn, cảm ơn các bạn đã đọc.
Nếu thấy hay hãy cho mình một upvote nhé 😘.
source: github.com/huyvu8051
All rights reserved