Spring Boot 项目集成与使用 MapStruct(超详细步骤)
适用人群:会用 Spring Boot,想在项目里把 DTO ⇄ Entity 的样板代码(getter/setter、拷贝字段)换成高性能、可编译期校验的 MapStruct。
示例构建工具:Maven(附 Gradle 版)
示例 JDK:17(11+ 都可)
示例 Spring Boot:3.2+(2.7+ 也可)
示例 MapStruct:1.5.5.Final(写作时的稳定版)
记得点赞加收藏哦😁😁😁
目录
- 为什么选 MapStruct
- 准备工作
- Maven 集成步骤(推荐)
- Gradle 集成步骤
- 创建示例实体与 DTO
- 编写 Mapper 接口(Spring 集成)
- 常见映射技巧
- 双向映射与反向配置复用
- 增量更新(部分字段更新)
- 集合、枚举、日期格式化
- 使用 @AfterMapping、@BeforeMapping 做后/前处理
- 在 Spring Boot 中调用与测试
- 与 Lombok 共存注意事项
- 常见报错与排查
- 生产最佳实践清单
为什么选 MapStruct
- 编译期生成:没有运行时反射,性能高,启动开销小。
- 类型安全:编译期就能发现字段不匹配等问题,更早暴露错误。
- 可维护:映射规则集中在接口/注解上,团队更易读更稳定。
- 可扩展:支持表达式、自定义方法、生命周期回调、依赖注入。
准备工作
- JDK 11+(示例使用 17)
- 一个可运行的 Spring Boot 项目(Web/Service 都可)
- IDE:开启 Annotation Processing(注解处理),否则不会生成实现类
- IntelliJ IDEA:
Settings -> Build, Execution, Deployment -> Compiler -> Annotation Processors -> Enable
- IntelliJ IDEA:
Maven 集成步骤(推荐)
1)在 pom.xml 添加依赖与注解处理器
<properties>
<java.version>17</java.version>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
<lombok.version>1.18.34</lombok.version>
</properties>
<dependencies>
<!-- MapStruct API -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<!-- Lombok(如项目使用) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
<compilerArgs>
<arg>-Amapstruct.defaultComponentModel=spring</arg>
</compilerArgs>
<annotationProcessorPaths>
<!-- MapStruct 注解处理器(生成实现类) -->
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<!-- Lombok 注解处理器(如项目使用) -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
说明:
-Amapstruct.defaultComponentModel=spring可全局指定生成@Component/@Mapper(componentModel="spring")的 Spring Bean;也可以逐个 Mapper 上写注解。
Gradle 集成步骤
Kotlin DSL(build.gradle.kts)
plugins {
java
id("org.springframework.boot") version "3.3.4"
id("io.spring.dependency-management") version "1.1.6"
}
java.sourceCompatibility = JavaVersion.VERSION_17
dependencies {
implementation("org.mapstruct:mapstruct:1.5.5.Final")
annotationProcessor("org.mapstruct:mapstruct-processor:1.5.5.Final")
compileOnly("org.projectlombok:lombok:1.18.34")
annotationProcessor("org.projectlombok:lombok:1.18.34")
}
tasks.withType<JavaCompile> {
options.compilerArgs.addAll(listOf("-Amapstruct.defaultComponentModel=spring"))
options.encoding = "UTF-8"
}
Groovy DSL(build.gradle)
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
}
sourceCompatibility = '17'
dependencies {
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
compileOnly 'org.projectlombok:lombok:1.18.34'
annotationProcessor 'org.projectlombok:lombok:1.18.34'
}
tasks.withType(JavaCompile) {
options.compilerArgs += ['-Amapstruct.defaultComponentModel=spring']
options.encoding = 'UTF-8'
}
创建示例实体与 DTO
domain/User.java
package com.example.demo.domain;
import lombok.Data;
import java.time.LocalDate;
import java.util.List;
@Data
public class User {
private Long id;
private String username;
private String email;
private LocalDate birthday;
private Address address;
private List<String> roles;
private Gender gender;
@Data
public static class Address {
private String province;
private String city;
private String detail;
}
public enum Gender {
MALE, FEMALE, OTHER
}
}
dto/UserDTO.java
package com.example.demo.dto;
import lombok.Data;
import java.util.List;
@Data
public class UserDTO {
private String id; // 注意:字符串
private String name; // username -> name
private String email;
private String birthday; // 字符串日期,格式:yyyy-MM-dd
private String fullAddress; // 由 address 组合
private List<String> roles;
private String gender; // enum -> 字符串
}
编写 Mapper 接口(Spring 集成)
mapper/UserMapper.java
package com.example.demo.mapper;
import com.example.demo.domain.User;
import com.example.demo.dto.UserDTO;
import org.mapstruct.*;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.IGNORE // 未映射字段忽略(也可 WARN/ERROR)
)
public interface UserMapper {
// 也可以用 Spring 注入,不需要 INSTANCE
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
@Mappings({
@Mapping(source = "id", target = "id"),
@Mapping(source = "username", target = "name"),
@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd"),
@Mapping(target = "fullAddress", expression =
"java(user.getAddress()==null? null : " +
"user.getAddress().getProvince()+\"-\"+user.getAddress().getCity()+\"-\"+user.getAddress().getDetail())"),
@Mapping(source = "gender", target = "gender") // enum -> String,MapStruct 会调用 name()
})
UserDTO toDTO(User user);
// 反向映射由 @InheritInverseConfiguration 提供(见下一节)
}
常见映射技巧
1)字段名不同(source/target)
@Mapping(source = "username", target = "name")
2)类型不同(日期、枚举、数值)
@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
3)表达式 / 自定义方法
@Mapping(target = "fullAddress",
expression = "java(combineAddress(user))")
default String combineAddress(User user) {
if (user.getAddress() == null) return null;
var a = user.getAddress();
return String.join("-", a.getProvince(), a.getCity(), a.getDetail());
}
4)忽略字段
@Mapping(target = "roles", ignore = true)
5)统一策略(全局)
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR) // 强制必须映射
双向映射与反向配置复用
UserMapper.java(续)
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper {
UserDTO toDTO(com.example.demo.domain.User user);
@InheritInverseConfiguration(name = "toDTO")
@Mappings({
// 反向时,字符串 -> Long
@Mapping(target = "id", expression = "java( userDTO.getId()==null? null : Long.valueOf(userDTO.getId()) )"),
// 字符串 -> LocalDate
@Mapping(target = "birthday", dateFormat = "yyyy-MM-dd"),
// 反向需要拆分 fullAddress
@Mapping(target = "address", expression = "java(splitAddress(userDTO.getFullAddress()))")
})
com.example.demo.domain.User toEntity(UserDTO userDTO);
default com.example.demo.domain.User.Address splitAddress(String full) {
if (full == null || full.isBlank()) return null;
String[] parts = full.split("-", 3);
var a = new com.example.demo.domain.User.Address();
a.setProvince(parts.length > 0 ? parts[0] : null);
a.setCity(parts.length > 1 ? parts[1] : null);
a.setDetail(parts.length > 2 ? parts[2] : null);
return a;
}
}
增量更新(部分字段更新)
适用于 PATCH 场景:用 DTO 的非空字段更新已有实体。
import org.mapstruct.BeanMapping;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.MappingTarget;
@Mapper(componentModel = "spring")
public interface UserUpdateMapper {
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateFromDto(UserDTO dto, @MappingTarget com.example.demo.domain.User entity);
}
说明:当 DTO 某个字段为
null时,不覆盖实体已有值。
集合、枚举、日期格式化
- 集合:
List<A>↔List<B>,若存在A↔B的映射方法,集合会自动映射。 - 枚举:默认使用
name()/valueOf;可通过@ValueMapping自定义映射。 - 日期:
@Mapping(dateFormat = "yyyy-MM-dd HH:mm:ss");或注册自定义DateMapper。
自定义日期转换器示例
@Component
public class DateMapper {
public String asString(LocalDate date) { return date == null ? null : date.toString(); }
public LocalDate asLocalDate(String date) { return date == null ? null : LocalDate.parse(date); }
}
@Mapper(componentModel = "spring", uses = DateMapper.class)
public interface UserMapper { /* ... */ }
使用 @AfterMapping/@BeforeMapping 做后/前处理
@Mapper(componentModel = "spring")
public interface TrimMapper {
@Mapping(target = "name", source = "username")
UserDTO toDTO(User user);
@AfterMapping
default void trimAll(@MappingTarget UserDTO dto) {
if (dto.getName() != null) dto.setName(dto.getName().trim());
if (dto.getEmail() != null) dto.getEmail().trim();
}
}
@BeforeMapping也可用于输入预处理,参数既可以是源对象也可以是@Context上下文对象。
在 Spring Boot 中调用与测试
service/UserService.java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserMapper userMapper; // Spring 自动注入
private final UserUpdateMapper updateMapper; // 增量更新示例
public UserDTO getUserDTO(User user) {
return userMapper.toDTO(user);
}
public void patchUser(UserDTO patch, User entity) {
updateMapper.updateFromDto(patch, entity);
}
}
UserMapperTest.java
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void testToDTO() {
User u = new User();
u.setId(1L);
u.setUsername("alice");
u.setEmail("a@x.com");
u.setBirthday(LocalDate.of(1990,1,1));
User.Address a = new User.Address();
a.setProvince("ZJ"); a.setCity("HZ"); a.setDetail("WestLake");
u.setAddress(a);
u.setGender(User.Gender.FEMALE);
UserDTO dto = userMapper.toDTO(u);
assertEquals("1", dto.getId());
assertEquals("alice", dto.getName());
assertEquals("1990-01-01", dto.getBirthday());
assertEquals("ZJ-HZ-WestLake", dto.getFullAddress());
assertEquals("FEMALE", dto.getGender());
}
}
与 Lombok 共存注意事项
- 同时使用时,确保 两个注解处理器都启用(
mapstruct-processor与lombok)。 - IDE 必须开启 Annotation Processing。
- 不再需要历史上的
lombok-mapstruct-binding(新版本大多无需)。若遇到 builder 兼容性问题,再考虑加:<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok-mapstruct-binding</artifactId> <version>0.2.0</version> <scope>provided</scope> </dependency>
常见报错与排查
-
没有生成实现类(
UserMapperImpl不存在)- IDE 未开启 Annotation Processing。
- 忘记添加
mapstruct-processor到annotationProcessorPaths。 - 清理重建:
mvn -U clean compile/./gradlew clean build。
-
unmapped target property(未映射字段告警/错误)
- 设置
unmappedTargetPolicy = ReportingPolicy.IGNORE|WARN|ERROR。 - 明确加
@Mapping(ignore = true)。
- 设置
-
类型不匹配
- 提供自定义转换方法(
default方法或uses)。 - 用
expression处理复杂逻辑。
- 提供自定义转换方法(
-
循环依赖映射(A 包含 B,B 又包含 A)
- 拆分为两个 Mapper,并使用
uses注入彼此,或在其中一个方向上忽略回指字段。
- 拆分为两个 Mapper,并使用
-
Spring 注入失败(
NoSuchBeanDefinitionException)- 确认
componentModel="spring"或编译参数全局开启; - Mapper 包路径在
@SpringBootApplication扫描范围内。
- 确认
生产最佳实践清单
- ✅ 给每个 Mapper 显式设置
componentModel="spring"与unmappedTargetPolicy。 - ✅ 把「复杂组装逻辑」封装为可单测的
default方法或独立的@Component,通过uses复用。 - ✅
@BeanMapping(nullValuePropertyMappingStrategy = IGNORE)做增量更新(避免空值覆盖)。 - ✅ 对 DTO 做最小字段暴露,防止过度映射。
- ✅ 加上 单元测试 覆盖关键映射。
- ✅ 对外接口层尽量用 DTO,Domain 保持纯净。
参考命令(Maven)
mvn -U clean compile # 触发注解处理器,生成 *MapperImpl
mvn test # 运行单测
mvn spring-boot:run # 启动应用
8066

被折叠的 条评论
为什么被折叠?



