MapStruct(对象转换工具)
# MapStruct(对象映射工具)
前言
在现代 Java 企业级应用开发,尤其是遵循分层架构(如常见的 Controller-Service-Repository 模式)的项目中,数据在不同层级间的流转和转换是不可避免的核心环节。我们常常需要将数据库实体对象(Entity/POJO)、服务间传输的数据传输对象(DTO)、以及最终呈现给用户的视图对象(VO)进行相互映射。如果完全依赖手动编写 getter/setter
来完成这些对象的属性复制,代码将变得极其冗余、枯燥且极易出错,后期维护成本也会居高不下。
MapStruct 应运而生,它是一款基于 编译时代码生成 的 Java Bean 映射框架。它通过定义简单的接口和使用注解,就能自动生成高效、类型安全的映射代码,从而将开发者从繁琐、易错的对象转换工作中解放出来,极大地提高了开发效率和代码质量。与基于反射的工具(如 BeanUtils)相比,MapStruct 在性能和类型安全方面具有显著优势。
# 一、MapStruct 简介与应用背景
# 1.1 为何需要对象映射?分层架构下的必然选择
在典型的分层架构中,每一层都有其特定的数据表示形式:
- 领域层/数据访问层 (Domain/Entity): 通常与数据库表结构直接对应,包含所有持久化字段,位于
domain
或entity
包中。这些对象可能包含复杂的关联关系和持久化相关的注解(如 JPA 注解)。 - 服务层/业务逻辑层 (Service/DTO): 使用数据传输对象(Data Transfer Object, DTO)在不同服务或模块间传递数据。DTO 通常只包含业务逻辑所需的字段子集,可能对实体数据进行聚合或裁剪,目的是减少不必要的数据传输和解耦。
- 表现层/控制层 (Controller/VO): 使用视图对象(View Object, VO)或表示模型(Representation Model)来封装需要展示给前端的数据。VO 的结构完全服务于 UI 展示逻辑,可能包含格式化后的数据、计算字段或与特定视图相关的状态。
由于各层职责和数据需求的不同,这些对象之间必须进行转换。例如:
- 从 Repository 查询
User
Entity 后,需要转换为UserDTO
供 Service 处理。 - Service 处理完业务逻辑后,可能将
UserDTO
转换为UserVO
返回给 Controller,以便前端展示。 - Controller 接收到前端传入的
UserCreationRequest
DTO,需要将其转换为User
Entity 以便持久化。
手动实现这些转换逻辑,即使对于简单的对象,也会产生大量重复且易错的 getter/setter
代码。
// 手动转换示例 (繁琐且易错)
public UserDTO convertUserToDTO(User user) {
if (user == null) {
return null;
}
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setUsername(user.getUsername());
dto.setEmail(user.getEmail());
// ... 其他几十个字段 ...
// 容易漏掉字段或在类型转换时出错
if (user.getCreateTime() != null) {
dto.setCreateTimeFormatted(
user.getCreateTime().format(DateTimeFormatter.ISO_DATE_TIME)
);
}
return dto;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1.2 MapStruct 的核心优势:编译时生成,类型安全,高性能
MapStruct 通过一种优雅的方式解决了手动映射的痛点:
- 编译时代码生成 (Compile-time Code Generation): MapStruct 是一个注解处理器 (Annotation Processor)。它在 Java 编译期间扫描带有
@Mapper
注解的接口,并根据接口中定义的映射方法和@Mapping
等注解,自动生成实现这些接口的具体 Java 类。这些生成的类包含了所有必需的getter/setter
调用,完全是原生的 Java 代码。 - 类型安全 (Type Safety): 由于映射逻辑在编译时确定并生成代码,Java 编译器可以对生成的代码进行完整的类型检查。如果源类型和目标类型不兼容,或者映射配置有误(如字段名拼写错误),编译过程就会失败,从而将错误扼杀在开发阶段,而不是运行时。
- 高性能 (High Performance): 因为生成的代码是直接的
getter/setter
调用,没有任何运行时的反射或动态代理开销,MapStruct 的性能几乎等同于手动编写映射代码,远超基于反射的工具(如 Apache Commons BeanUtils, ModelMapper)。 - 易用性与声明式配置 (Ease of Use & Declarative Configuration): 开发者只需定义一个简单的 Java 接口,并使用直观的注解(主要是
@Mapper
和@Mapping
)来声明映射规则,MapStruct 会负责生成具体的实现细节。 - 高度可定制 (Highly Customizable): 支持处理各种复杂映射场景,包括:
- 不同名称字段的映射。
- 不同类型字段的映射(内置多种类型转换,并支持自定义转换方法)。
- 常量值、表达式映射。
- 集合、Map、嵌套对象的映射。
- 枚举类型的映射。
- 映射前/后回调方法。
- 映射器组合与继承。
- 与构建工具和 IDE 良好集成: 可以轻松集成到 Maven、Gradle 等构建流程中,并在 IntelliJ IDEA、Eclipse 等 IDE 中获得良好的代码提示和导航支持(需要安装相应插件)。
# 二、MapStruct 与 BeanUtils 的对比
在选择对象映射工具时,经常会与 Spring Framework 自带的 BeanUtils.copyProperties()
进行比较。虽然 BeanUtils
使用简单,但在功能和性能上与 MapStruct 存在显著差异。
# 2.1 BeanUtils 的局限性分析
Spring 的 BeanUtils
主要依赖运行时反射来复制属性。
import org.springframework.beans.BeanUtils;
import lombok.Data;
import java.time.LocalDateTime;
import java.time.format.DateTimeParseException;
// 假设 User 和 UserDTO 类定义如前文
@Data class User { private Integer id; private String name; private String createTime; /* yyyy-MM-dd HH:mm:ss */ private LocalDateTime updateTime; }
@Data class UserDTO { private Integer id; private String name; private String createTime; private LocalDateTime updateTime; }
/**
* BeanUtils 在处理类型不匹配时的局限性示例
*/
public class BeanUtilsLimitationExample {
public static void main(String[] args) {
// 源对象: createTime 是 String
User user = new User();
user.setId(1);
user.setName("张三");
user.setCreateTime("2023-09-01 10:00:00"); // String 类型
user.setUpdateTime(LocalDateTime.now());
// 目标对象: createTime 也是 String, updateTime 是 LocalDateTime
UserDTO userDTO = new UserDTO();
// 使用 BeanUtils 复制属性
// 它会成功复制 id, name, createTime (同名同类型 String)
// 它也会成功复制 updateTime (同名同类型 LocalDateTime)
try {
BeanUtils.copyProperties(user, userDTO);
System.out.println("BeanUtils 复制成功 (同类型): " + userDTO);
} catch (Exception e) {
System.err.println("BeanUtils 复制时出错: " + e.getMessage());
}
// 尝试将包含日期字符串的 UserDTO 复制回 User (假设 User 的 createTime 需要是 Date 类型)
@Data class UserWithDate { private Integer id; private String name; private java.util.Date createTime; }
UserDTO dtoWithStrDate = new UserDTO(1, "李四", "2023-10-01 12:00:00", null);
UserWithDate userWithDate = new UserWithDate();
try {
// BeanUtils 无法自动将 String "2023-10-01 12:00:00" 转换为 java.util.Date
// 这里不会抛出异常,但 createTime 字段会是 null
BeanUtils.copyProperties(dtoWithStrDate, userWithDate);
System.out.println("\nBeanUtils 尝试 String -> Date 复制后: " + userWithDate);
// 输出: BeanUtils 尝试 String -> Date 复制后: UserWithDate(id=1, name=李四, createTime=null)
} catch (Exception e) {
System.err.println("BeanUtils 在 String -> Date 复制时出错: " + e.getMessage());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
BeanUtils 的主要问题:
- 仅支持同名属性复制: 无法处理源和目标字段名称不一致的情况(如
userId
vsid
)。 - 有限的类型转换: 仅支持 Java 内置的兼容类型(如
int
<->Integer
)。无法自动处理复杂的类型转换,例如String
到Date
/LocalDateTime
,或者枚举到字符串/代码。通常会导致属性被忽略或在运行时失败(如果目标是基本类型且源为null
)。 - 性能较低: 基于运行时反射,涉及查找方法、调用方法等操作,相比直接方法调用有显著性能开销。
- 非类型安全: 属性名称以字符串形式传递(如
ignoreProperties
),无法在编译时检查拼写错误。类型不匹配的问题也只能在运行时发现。
# 2.2 MapStruct 如何解决这些问题
MapStruct 通过其设计机制优雅地解决了 BeanUtils 的局限。
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import lombok.Data;
// 假设 User 类如前: id(Int), name(Str), createTime(Str "yyyy-MM-dd HH:mm:ss"), updateTime(LDT)
// 假设 UserDTO 类如前: id(Int), name(Str), createTime(Str), updateTime(LDT)
// 定义一个目标 DTO,需要将 String 类型的 createTime 转换为 LocalDateTime
@Data class UserDtoWithLDT { private Integer id; private String name; private LocalDateTime creationTimestamp; private LocalDateTime updateTime; }
/**
* MapStruct 映射器接口,演示解决 BeanUtils 局限性
*/
@Mapper // 标记为 MapStruct 映射器
public interface UserMapperAdvanced {
UserMapperAdvanced INSTANCE = Mappers.getMapper(UserMapperAdvanced.class); // 获取实例的方式
/**
* 将 User 映射到 UserDtoWithLDT
* 处理不同名称 (createTime -> creationTimestamp)
* 处理不同类型 (String -> LocalDateTime)
*
* @param user 源 User 对象
* @return 映射后的 UserDtoWithLDT 对象
*/
@Mappings({
// 1. 处理不同名称: source 指定源字段名,target 指定目标字段名
@Mapping(source = "createTime", target = "creationTimestamp", dateFormat = "yyyy-MM-dd HH:mm:ss"),
// 2. 处理不同类型: 使用 dateFormat 属性,MapStruct 会自动生成 String 到 LocalDateTime 的转换代码
// 3. 同名同类型字段 (id, name, updateTime) 会被自动映射
})
UserDtoWithLDT userToUserDtoWithLDT(User user);
/**
* 将 UserDtoWithLDT 映射回 User
* 处理不同名称 (creationTimestamp -> createTime)
* 处理不同类型 (LocalDateTime -> String)
*
* @param dto 源 UserDtoWithLDT 对象
* @return 映射后的 User 对象
*/
@Mappings({
@Mapping(source = "creationTimestamp", target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
// 使用 dateFormat 将 LocalDateTime 格式化为 String
})
User userDtoWithLDTToUser(UserDtoWithLDT dto);
}
// --- 使用示例 ---
public class MapStructAdvantageDemo {
public static void main(String[] args) {
User user = new User(1, "王五", "2024-01-15 09:45:00", LocalDateTime.now());
// 使用 MapStruct 进行转换
UserDtoWithLDT dto = UserMapperAdvanced.INSTANCE.userToUserDtoWithLDT(user);
System.out.println("MapStruct 转换 User -> UserDtoWithLDT:");
System.out.println(dto);
// 输出: UserDtoWithLDT(id=1, name=王五, creationTimestamp=2024-01-15T09:45, updateTime=...)
User userConvertedBack = UserMapperAdvanced.INSTANCE.userDtoWithLDTToUser(dto);
System.out.println("\nMapStruct 转换 UserDtoWithLDT -> User:");
System.out.println(userConvertedBack);
// 输出: User(id=1, name=王五, createTime=2024-01-15 09:45:00, updateTime=...)
}
// 假设 User 和 UserDtoWithLDT 类已定义并包含 Lombok 注解
@Data @AllArgsConstructor @NoArgsConstructor static class User { private Integer id; private String name; private String createTime; private LocalDateTime updateTime; }
@Data @AllArgsConstructor @NoArgsConstructor static class UserDtoWithLDT { private Integer id; private String name; private LocalDateTime creationTimestamp; private LocalDateTime updateTime; }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
MapStruct 的优势体现:
- 支持不同名称字段映射: 通过
@Mapping(source = "...", target = "...")
轻松实现。 - 强大的类型转换:
- 内置了大量常用类型转换(基本类型、包装类型、日期时间 API、集合等)。
- 通过
dateFormat
属性方便地处理日期字符串与日期对象的转换。 - 支持通过
default
方法或引入其他 Mapper (uses
属性) 实现自定义复杂转换逻辑。
- 高性能: 生成的代码是直接的方法调用,无运行时反射开销。
- 编译时类型安全: 所有映射配置在编译期验证,错误提前暴露。
# 三、环境配置与依赖管理
要在项目中使用 MapStruct,需要正确配置构建工具(如 Maven 或 Gradle)以包含核心库和注解处理器。
# 3.1 Maven 依赖配置
在 pom.xml
中添加以下配置:
<properties>
<!-- 统一管理版本号 -->
<java.version>1.8</java.version> <!-- 或更高版本 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mapstruct.version>1.5.5.Final</mapstruct.version> <!-- 使用 MapStruct 最新稳定版本 -->
<lombok.version>1.18.30</lombok.version> <!-- 如果使用 Lombok,指定其版本 -->
</properties>
<dependencies>
<!-- MapStruct 核心库,包含 @Mapper, @Mapping 等注解 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<!-- Lombok (可选,但常用,用于简化 POJO) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional> <!-- 标记为可选,因为它是编译时工具 -->
</dependency>
<!-- Spring Boot 相关依赖 (如果是在 Spring Boot 项目中) -->
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency> -->
</dependencies>
<build>
<plugins>
<!-- Maven 编译器插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version> <!-- 或更新版本 -->
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<!-- 配置注解处理器路径 -->
<annotationProcessorPaths>
<!-- MapStruct 注解处理器,用于生成映射实现类 -->
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<!-- Lombok 注解处理器 (如果使用 Lombok) -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<!-- Lombok 与 MapStruct 集成的绑定器 (关键!) -->
<!-- 这个绑定器确保 Lombok 先运行,MapStruct 后运行,以便 MapStruct 能看到 Lombok 生成的方法 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version> <!-- 使用与 Lombok/MapStruct 兼容的版本 -->
</path>
</annotationProcessorPaths>
<!-- (可选) 如果希望 MapStruct 生成的类作为 Spring 组件 -->
<!-- <compilerArgs>
<compilerArg>
-Amapstruct.defaultComponentModel=spring
</compilerArg>
</compilerArgs> -->
</configuration>
</plugin>
</plugins>
</build>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
关键配置解释:
mapstruct
: 核心注解库。mapstruct-processor
: 注解处理器,必须添加,否则 MapStruct 不会生成代码。通常scope
设置为provided
或在<annotationProcessorPaths>
中指定。lombok
(可选): 如果使用 Lombok 简化代码,需要添加。lombok-mapstruct-binding
(如果同时使用 Lombok): 极其重要,用于协调 Lombok 和 MapStruct 注解处理器的执行顺序,确保 MapStruct 能正确访问 Lombok 生成的getter/setter
。
# 3.2 Gradle 依赖配置
在 build.gradle
或 build.gradle.kts
中添加类似配置:
// build.gradle (Groovy DSL)
plugins {
id 'java'
// id 'org.springframework.boot' version '...' // 如果是 Spring Boot
// id 'io.spring.dependency-management' version '...' // 如果是 Spring Boot
}
repositories {
mavenCentral()
}
ext { // 统一管理版本
mapstructVersion = '1.5.5.Final'
lombokVersion = '1.18.30'
}
dependencies {
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
// implementation "org.springframework.boot:spring-boot-starter" // Spring Boot
// Lombok (可选)
compileOnly "org.projectlombok:lombok:${lombokVersion}"
annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
// MapStruct 注解处理器
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
// Lombok 与 MapStruct 集成绑定器 (如果使用 Lombok)
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0"
// testImplementation ...
}
// (可选) 全局配置 MapStruct 生成 Spring 组件
/*
tasks.withType(JavaCompile) {
options.compilerArgs = [
'-Amapstruct.defaultComponentModel=spring'
]
}
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 3.3 验证配置是否成功
配置完成后,执行项目的编译命令(如 mvn clean compile
或 gradle build
)。编译成功后,检查项目的 target/generated-sources/annotations
(Maven) 或 build/generated/sources/annotationProcessor/java/main
(Gradle) 目录下是否生成了以 Impl
结尾的映射器实现类。如果生成了,说明 MapStruct 配置成功。
# 四、MapStruct 基础用法
掌握 MapStruct 的基础用法是高效利用该工具的第一步。
# 4.1 @Mapper
注解与组件模型 (Component Model)
@Mapper
是 MapStruct 的入口注解,用于标记一个接口为映射器接口。MapStruct 处理器会为带有此注解的接口生成实现类。
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers; // 用于 default 模式获取实例
// 示例 1: 默认组件模型 (default)
@Mapper // 没有指定 componentModel,默认为 default
public interface DefaultUserMapper {
// 需要通过 Mappers 工具类手动获取实例
DefaultUserMapper INSTANCE = Mappers.getMapper(DefaultUserMapper.class);
UserDTO userToDto(User user); // 映射方法定义
}
// 示例 2: Spring 组件模型 (spring)
@Mapper(componentModel = "spring") // 指定为 spring 模型
public interface SpringUserMapper {
// MapStruct 会为生成的实现类添加 @Component 注解
// 可以直接在 Spring 应用中使用 @Autowired 或构造器注入来获取实例
UserDTO userToDto(User user);
}
// 示例 3: 其他组件模型
// @Mapper(componentModel = "cdi") // 用于 CDI 环境
// @Mapper(componentModel = "jsr330") // 使用 JSR 330 注解 (@Named, @Singleton)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
componentModel
属性详解:
default
: 默认值。生成的实现类不带任何特定框架的注解。需要通过Mappers.getMapper(YourMapper.class)
静态方法获取 Mapper 实例。适用于不依赖特定 DI 框架的场景。spring
: 推荐在 Spring/Spring Boot 项目中使用。生成的实现类会添加@Component
注解,使其成为一个 Spring Bean,可以通过@Autowired
或构造器注入到其他 Spring 组件(如 Service, Controller)中使用。cdi
: 生成的实现类会添加@ApplicationScoped
注解,适用于 Jakarta EE/CDI 环境。jsr330
: 生成的实现类会添加@Named
和@Singleton
注解(来自javax.inject
或jakarta.inject
),适用于支持 JSR-330 注入规范的框架。
选择合适的 componentModel
可以让 MapStruct 生成的 Mapper 实例的生命周期和获取方式与你的应用程序框架保持一致。
# 4.2 定义源对象 (Source) 与目标对象 (Target)
在使用 MapStruct 进行映射前,需要明确定义源对象和目标对象的 Java 类。这些类通常是 POJO 或 JavaBean。
import lombok.Data; // 使用 Lombok 简化
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import java.time.LocalDateTime;
/**
* 用户实体类 (Source Object) - 通常对应数据库表
*/
@Data // 自动生成 getter/setter/toString/equals/hashCode
@NoArgsConstructor // 自动生成无参构造
@AllArgsConstructor // 自动生成全参构造
@Builder // 启用 Builder 模式创建对象
public class User {
private Integer id; // 用户 ID
private String username; // 用户名 (假设数据库字段是 username)
private String emailAddress; // 邮箱地址 (假设数据库字段是 email_address)
private String passwordHash; // 密码哈希 (通常不暴露给 DTO/VO)
private LocalDateTime createdAt; // 创建时间
private LocalDateTime lastLogin; // 最后登录时间
private boolean isActive; // 账户是否激活
}
/**
* 用户数据传输对象 (Target Object) - 用于 Service 层或 API 响应
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserDTO {
private Integer id; // 用户 ID
private String username; // 用户名
private String email; // 邮箱 (注意:字段名与 User 不同)
private String creationDate; // 创建日期 (注意:类型和名称都与 User 不同)
private boolean active; // 激活状态 (注意:名称与 User 不同)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
在这个例子中,User
是源对象,UserDTO
是目标对象。它们之间存在字段名称、类型、数量上的差异,这些都需要在 MapStruct 映射器中处理。
# 4.3 创建基础映射器接口
对于简单场景(大部分字段同名同类型),只需定义一个包含映射方法的接口。
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
/**
* 用户映射器接口 - 定义 User 与 UserDTO 之间的转换规则
*/
@Mapper(componentModel = "spring") // 使用 Spring 组件模型
public interface UserBaseMapper {
/**
* 将 User 实体映射到 UserDTO 对象。
* MapStruct 会自动处理同名同类型的字段 (id, username)。
* 对于名称或类型不同的字段,需要使用 @Mapping 注解进行配置。
*
* @param user 源 User 对象
* @return 映射后的 UserDTO 对象
*/
@Mappings({
// 将 User.emailAddress 映射到 UserDTO.email
@Mapping(source = "emailAddress", target = "email"),
// 将 User.createdAt (LocalDateTime) 映射到 UserDTO.creationDate (String)
// 使用 dateFormat 指定格式
@Mapping(source = "createdAt", target = "creationDate", dateFormat = "yyyy-MM-dd"),
// 将 User.isActive 映射到 UserDTO.active (注意这里名称不同,但 MapStruct 可能会智能匹配 is/get 前缀)
// 如果智能匹配失败,需要显式 @Mapping(source = "isActive", target = "active")
@Mapping(source = "active", target = "active"), // 显式指定以确保映射
// User.passwordHash 在 UserDTO 中不存在,会被自动忽略
// User.lastLogin 在 UserDTO 中不存在,会被自动忽略
})
UserDTO userToUserDTO(User user);
/**
* 将 UserDTO 对象映射回 User 实体。
* 用于例如从请求 DTO 创建实体。
*
* @param userDTO 源 UserDTO 对象
* @return 映射后的 User 对象
*/
@Mappings({
// 将 UserDTO.email 映射回 User.emailAddress
@Mapping(source = "email", target = "emailAddress"),
// 将 UserDTO.creationDate (String) 映射回 User.createdAt (LocalDateTime)
@Mapping(source = "creationDate", target = "createdAt", dateFormat = "yyyy-MM-dd"),
// 将 UserDTO.active 映射回 User.isActive
@Mapping(source = "active", target = "active"),
// User.passwordHash 通常在创建时不直接从 DTO 映射,需要单独处理(例如加密)
@Mapping(target = "passwordHash", ignore = true), // 明确忽略 passwordHash 的反向映射
// User.lastLogin 通常不由 DTO 设置
@Mapping(target = "lastLogin", ignore = true) // 明确忽略 lastLogin 的反向映射
})
User userDTOToUser(UserDTO userDTO);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
说明:
@Mapper(componentModel = "spring")
: 标记接口,并指定生成 Spring Bean。@Mappings({...})
: 当有多个@Mapping
注解时,用@Mappings
包裹。@Mapping(source = "...", target = "...")
: 用于指定名称不同的字段映射关系。@Mapping(..., dateFormat = "...")
: 用于在String
和日期/时间类型之间转换时指定格式。@Mapping(target = "...", ignore = true)
: 用于显式忽略某个目标字段的映射。
MapStruct 会根据这些注解生成一个 UserBaseMapperImpl
类,包含具体的转换逻辑。
# 4.4 字段数量不一致的映射处理
当目标对象的字段数量少于源对象时,MapStruct 默认会自动忽略目标对象中不存在的字段,无需任何额外配置。
import lombok.Data;
import lombok.Builder;
/**
* 用户视图对象 (VO) - 只包含部分字段,用于前端展示
*/
@Data
@Builder
class UserVO {
private Integer id;
private String username;
private String registrationDate; // 对应 User.createdAt,可能格式不同
}
// 在 UserBaseMapper 接口中添加新方法:
@Mapper(componentModel = "spring")
public interface UserBaseMapper {
// ... (之前的 userToUserDTO 和 userDTOToUser 方法) ...
/**
* 将 User 实体映射到字段更少的 UserVO 对象。
* MapStruct 会自动只映射 id 和 username。
* createdAt 需要显式映射到 registrationDate 并指定格式。
* emailAddress, passwordHash, lastLogin, isActive 会被忽略。
*
* @param user 源 User 对象
* @return 映射后的 UserVO 对象
*/
@Mapping(source = "createdAt", target = "registrationDate", dateFormat = "yyyy/MM/dd")
UserVO userToUserVO(User user);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
生成的 UserBaseMapperImpl
中的 userToUserVO
方法只会包含 id
, username
, 和 registrationDate
的设置代码。
# 4.5 如何在代码中使用映射器
根据 @Mapper
注解中 componentModel
的设置,获取和使用 Mapper 实例的方式不同。
1. 使用 Spring 依赖注入 (componentModel = "spring"
):
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime; // 需要引入
import lombok.extern.slf4j.Slf4j; // 引入日志库
/**
* 示例 Service 类,通过构造器注入 UserBaseMapper
*/
@Slf4j // Lombok 日志注解
@Service
public class UserProcessingService {
private final UserBaseMapper userMapper; // 声明为 final,推荐构造器注入
// private final UserRepository userRepository; // 假设有 UserRepository
@Autowired // Spring 自动注入
public UserProcessingService(UserBaseMapper userMapper /*, UserRepository userRepository*/) {
this.userMapper = userMapper;
// this.userRepository = userRepository;
log.info("UserProcessingService initialized with UserBaseMapper implementation: {}",
userMapper.getClass().getName()); // 打印 Mapper 实现类名
}
/**
* 业务方法,演示使用注入的 Mapper
* @param userId 用户 ID
* @return 用户 DTO
*/
public UserDTO processAndGetUserDTO(Integer userId) {
// 模拟获取 User 实体
User userEntity = findUserById(userId);
if (userEntity == null) {
// 处理用户不存在的情况
return null;
}
log.debug("Found user entity: {}", userEntity);
// 使用注入的 userMapper 进行转换
UserDTO userDTO = userMapper.userToUserDTO(userEntity);
log.debug("Converted to UserDTO: {}", userDTO);
// 可以进行其他业务处理...
return userDTO;
}
// 模拟根据 ID 查找用户的方法
private User findUserById(Integer userId) {
// 实际应从数据库查询
if (userId == 1) {
return User.builder()
.id(1)
.username("demoUser")
.emailAddress("demo@example.com")
.createdAt(LocalDateTime.now().minusDays(10))
.isActive(true)
.build();
}
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
2. 使用 Mappers
工厂类 (componentModel = "default"
):
import org.mapstruct.factory.Mappers;
import java.time.LocalDateTime; // 需要引入
/**
* 示例转换工具类,使用 Mappers 工厂获取 Mapper 实例
*/
public class UserConversionUtil {
// 获取 Mapper 实例 (线程安全,可以作为静态常量)
private static final UserBaseMapper MAPPER = Mappers.getMapper(UserBaseMapper.class);
/**
* 静态方法:将 User 转换为 UserDTO
* @param user 源 User 对象
* @return 转换后的 UserDTO 对象
*/
public static UserDTO convertToDto(User user) {
if (user == null) {
return null;
}
return MAPPER.userToUserDTO(user); // 使用静态实例调用映射方法
}
/**
* 静态方法:将 UserDTO 转换为 User
* @param dto 源 UserDTO 对象
* @return 转换后的 User 对象
*/
public static User convertToUser(UserDTO dto) {
if (dto == null) {
return null;
}
return MAPPER.userDTOToUser(dto); // 使用静态实例调用映射方法
}
// --- 使用示例 ---
public static void main(String[] args) {
User user = User.builder().id(2).username("staticUser").emailAddress("static@example.com").createdAt(LocalDateTime.now()).isActive(false).build();
UserDTO dto = UserConversionUtil.convertToDto(user);
System.out.println("Static conversion User -> DTO: " + dto);
User convertedUser = UserConversionUtil.convertToUser(dto);
System.out.println("Static conversion DTO -> User: " + convertedUser);
}
// 假设 User 和 UserDTO 类已定义
// @Data @Builder @NoArgsConstructor @AllArgsConstructor static class User { ... }
// @Data @Builder @NoArgsConstructor @AllArgsConstructor static class UserDTO { ... }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
选择哪种方式取决于你的项目是否使用了 Spring 或其他 DI 框架。在 Spring Boot 项目中,强烈推荐使用 componentModel = "spring"
。
# 4.6 查看生成的映射器实现代码
理解 MapStruct 如何工作的一个好方法是查看它生成的实现代码。在配置了注解处理器后,编译项目时会在 target
(Maven) 或 build
(Gradle) 目录下生成 YourMapperNameImpl.java
文件。
例如,对于上面的 UserBaseMapper
,生成的 UserBaseMapperImpl.java
可能类似于(已简化并添加注释):
// package com.example.mapper; // 包名取决于你的项目结构
import com.example.dto.UserDTO; // 假设 DTO 在此包
import com.example.model.User; // 假设 Entity 在此包
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import javax.annotation.processing.Generated; // 表明是生成的代码
import org.springframework.stereotype.Component; // 如果 componentModel="spring"
@Generated( // JSR-269 生成代码的标准注解
value = "org.mapstruct.ap.MappingProcessor",
date = "...", // 生成时间
comments = "..." // 版本信息等
)
@Component // 因为我们设置了 componentModel="spring"
public class UserBaseMapperImpl implements UserBaseMapper {
@Override // 实现接口方法
public UserDTO userToUserDTO(User user) {
if (user == null) { // 处理 null 输入
return null;
}
UserDTO userDTO = new UserDTO(); // 创建目标对象
// --- 属性复制逻辑 ---
userDTO.setId(user.getId()); // 直接复制同名同类型 id
userDTO.setUsername(user.getUsername()); // 直接复制同名同类型 username
// 处理 @Mapping(source = "emailAddress", target = "email")
userDTO.setEmail(user.getEmailAddress());
// 处理 @Mapping(source = "createdAt", target = "creationDate", dateFormat = "yyyy-MM-dd")
if (user.getCreatedAt() != null) {
// 使用指定的 DateTimeFormatter 进行格式化
userDTO.setCreationDate(DateTimeFormatter.ofPattern("yyyy-MM-dd").format(user.getCreatedAt()));
}
// 处理 @Mapping(source = "active", target = "active")
userDTO.setActive(user.isActive()); // 注意调用的是 isActive()
return userDTO; // 返回结果
}
@Override // 实现接口方法
public User userDTOToUser(UserDTO userDTO) {
if (userDTO == null) { // 处理 null 输入
return null;
}
User user = new User(); // 创建目标对象
// --- 属性复制逻辑 ---
user.setId(userDTO.getId());
user.setUsername(userDTO.getUsername());
// 处理 @Mapping(source = "email", target = "emailAddress")
user.setEmailAddress(userDTO.getEmail());
// 处理 @Mapping(source = "creationDate", target = "createdAt", dateFormat = "yyyy-MM-dd")
if (userDTO.getCreationDate() != null) {
try {
// 使用指定的 DateTimeFormatter 进行解析,注意这里需要处理可能的异常
// MapStruct 通常会生成更健壮的解析代码,或依赖转换方法
user.setCreatedAt(LocalDateTime.parse(userDTO.getCreationDate() + " 00:00:00", // 假设需要补全时间
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
} catch (DateTimeParseException e) {
// 处理解析异常,例如记录日志或抛出包装后的异常
throw new RuntimeException("无法解析日期字符串: " + userDTO.getCreationDate(), e);
}
}
// 处理 @Mapping(source = "active", target = "active") -> User.setActive()
user.setActive(userDTO.isActive());
// passwordHash 和 lastLogin 因为 ignore=true 或 DTO 中不存在而被忽略
return user; // 返回结果
}
// userToUserVO 方法的实现会类似地生成...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
查看生成的代码有助于:
- 理解 MapStruct 如何处理你的映射配置。
- 调试映射问题。
- 学习 MapStruct 的内部工作机制。
# 五、处理复杂映射场景
MapStruct 提供了丰富的注解和功能来应对各种复杂的映射需求。
# 5.1 不同字段名称映射 (@Mapping
)
这是最常用的功能之一,通过 @Mapping(source = "源字段名", target = "目标字段名")
来指定。
import lombok.Data;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
/**
* 源对象:包含下划线命名的字段
*/
@Data
class LegacySystemData {
private int user_id;
private String user_name;
private String contact_email;
}
/**
* 目标对象:使用驼峰命名
*/
@Data
class ModernUserDTO {
private int id;
private String name;
private String email;
}
/**
* 映射器:处理不同命名风格的字段
*/
@Mapper(componentModel = "spring")
public interface NamingConventionMapper {
/**
* 将下划线命名的源对象映射到驼峰命名的目标对象
* @param source LegacySystemData 实例
* @return ModernUserDTO 实例
*/
@Mapping(source = "user_id", target = "id")
@Mapping(source = "user_name", target = "name")
@Mapping(source = "contact_email", target = "email")
ModernUserDTO legacyToModern(LegacySystemData source);
/**
* 反向映射:将驼峰命名映射回下划线命名
* @param target ModernUserDTO 实例
* @return LegacySystemData 实例
*/
@Mapping(source = "id", target = "user_id")
@Mapping(source = "name", target = "user_name")
@Mapping(source = "email", target = "contact_email")
LegacySystemData modernToLegacy(ModernUserDTO target);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 5.2 不同类型字段映射
MapStruct 能够自动处理许多内置类型转换,对于无法自动处理的,可以通过 dateFormat
、numberFormat
或自定义方法解决。
示例 1: 日期与字符串转换 (dateFormat
)
import lombok.Data;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import java.time.LocalDate;
import java.util.Date; // 使用 java.util.Date
@Data class EventEntity { private Long id; private String eventName; private Date eventTimestamp; }
@Data class EventDTO { private Long id; private String name; private String eventDate; /* yyyy/MM/dd */ private String eventTime; /* HH:mm */ }
@Mapper(componentModel = "spring")
public interface DateTimeTypeMapper {
@Mapping(source = "eventName", target = "name") // 不同名称
@Mapping(source = "eventTimestamp", target = "eventDate", dateFormat = "yyyy/MM/dd") // Date -> String (只取日期部分)
@Mapping(source = "eventTimestamp", target = "eventTime", dateFormat = "HH:mm") // Date -> String (只取时间部分)
EventDTO entityToDto(EventEntity entity);
// 反向映射时,需要将两个 String 合并或只使用一个来构造 Date,可能需要自定义方法或表达式
// @Mapping(source = "eventDate", target = "eventTimestamp", dateFormat = "yyyy/MM/dd") // 这种方式只能映射日期部分
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
示例 2: 数字与字符串转换 (numberFormat
)
import lombok.Data;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import java.math.BigDecimal;
@Data class ProductEntity { private int id; private String name; private BigDecimal price; }
@Data class ProductVO { private String id; private String name; private String formattedPrice; /* $###,##0.00 */ }
@Mapper(componentModel = "spring")
public interface NumberTypeMapper {
@Mapping(source = "id", target = "id") // int -> String (自动转换)
@Mapping(source = "price", target = "formattedPrice", numberFormat = "$###,##0.00") // BigDecimal -> String (带格式)
ProductVO entityToVo(ProductEntity entity);
// 反向映射:String -> BigDecimal 可能需要自定义方法或表达式处理货币符号和逗号
// @Mapping(source = "formattedPrice", target = "price", ???)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
示例 3: 自定义类型转换方法 (default
方法)
当内置转换或格式化不满足需求时,可以在 Mapper 接口中定义 default
方法来实现自定义转换逻辑。
import lombok.Data;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import java.util.Base64; // 用于 Base64 示例
import java.nio.charset.StandardCharsets; // 用于 Base64 示例
@Data class ConfigEntity { private String key; private byte[] sensitiveValue; } // 值是 byte 数组
@Data class ConfigDTO { private String key; private String encodedValue; } // 值是 Base64 编码的字符串
@Mapper(componentModel = "spring")
public interface CustomTypeConversionMapper {
/**
* 映射方法,使用自定义的 default 方法进行 byte[] 和 String 之间的转换
*/
@Mapping(source = "sensitiveValue", target = "encodedValue") // MapStruct 会自动查找类型匹配的 default 方法
ConfigDTO entityToDto(ConfigEntity entity);
@Mapping(source = "encodedValue", target = "sensitiveValue") // 反向映射
ConfigEntity dtoToEntity(ConfigDTO dto);
/**
* 自定义转换方法:byte[] 转换为 Base64 String
* 方法签名需要匹配源类型和目标类型
* @param value byte 数组源值
* @return Base64 编码的字符串
*/
default String encodeBase64(byte[] value) {
if (value == null) {
return null;
}
return Base64.getEncoder().encodeToString(value);
}
/**
* 自定义转换方法:Base64 String 转换为 byte[]
* @param encodedValue Base64 编码的字符串源值
* @return 解码后的 byte 数组
*/
default byte[] decodeBase64(String encodedValue) {
if (encodedValue == null) {
return null;
}
try {
return Base64.getDecoder().decode(encodedValue.getBytes(StandardCharsets.UTF_8));
} catch (IllegalArgumentException e) {
// 处理无效的 Base64 字符串,例如返回 null 或抛出异常
System.err.println("无效的 Base64 字符串: " + encodedValue);
return null;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
MapStruct 会在生成的代码中调用这些 default
方法。
# 5.3 枚举类型映射
MapStruct 可以很好地处理枚举类型与字符串、代码或其他枚举类型之间的映射。
import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.Getter; // 使用 Getter
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ValueMapping; // 用于枚举常量映射
import org.mapstruct.ValueMappings; // 用于多个 ValueMapping
/**
* 订单状态枚举
*/
@Getter // Lombok 注解,生成 getter
@AllArgsConstructor
public enum OrderStatus {
PENDING("P", "待处理"),
PROCESSING("PROC", "处理中"),
SHIPPED("S", "已发货"),
DELIVERED("D", "已送达"),
CANCELLED("C", "已取消");
private final String code; // 状态代码
private final String description; // 状态描述
// 可选:提供从代码查找枚举的方法
public static OrderStatus fromCode(String code) {
for (OrderStatus status : values()) {
if (status.code.equals(code)) {
return status;
}
}
return null; // 或抛出异常
}
}
/**
* 订单实体类,状态是枚举类型
*/
@Data class OrderEntity { private Long id; private String orderNumber; private OrderStatus status; }
/**
* 订单 DTO,状态是字符串代码
*/
@Data class OrderDTO { private Long id; private String orderNumber; private String statusCode; }
/**
* 枚举映射器
*/
@Mapper(componentModel = "spring")
public interface OrderEnumMapper {
// --- 方法 1: 使用内置转换 (如果目标是 String,默认调用 toString()) ---
// @Mapping(source = "status", target = "statusCode") // 会将 status.toString() 赋给 statusCode
// --- 方法 2: 使用表达式获取枚举的属性 ---
// @Mapping(source = "status.code", target = "statusCode") // 直接访问枚举的 code 属性
// @Mapping(source = "status.description", target = "statusDesc") // 访问 description
// --- 方法 3: 使用 @ValueMapping (适用于枚举常量之间的映射) ---
// 假设有另一个状态枚举 OrderState { NEW, IN_PROGRESS, DONE, FAILED }
/*
@ValueMappings({
@ValueMapping(source = "PENDING", target = "NEW"),
@ValueMapping(source = "PROCESSING", target = "IN_PROGRESS"),
@ValueMapping(source = "SHIPPED", target = "DONE"),
@ValueMapping(source = "DELIVERED", target = "DONE"),
@ValueMapping(source = "CANCELLED", target = "FAILED")
})
OrderState orderStatusToOrderState(OrderStatus status);
*/
// --- 方法 4: 使用自定义方法 (最灵活) ---
@Mapping(source = "status", target = "statusCode") // MapStruct 会查找 OrderStatus -> String 的方法
OrderDTO entityToDto(OrderEntity entity);
@Mapping(source = "statusCode", target = "status") // MapStruct 会查找 String -> OrderStatus 的方法
OrderEntity dtoToEntity(OrderDTO dto);
/**
* 自定义方法:将 OrderStatus 枚举转换为 String 代码
* @param status 源枚举
* @return 状态代码字符串
*/
default String orderStatusToCode(OrderStatus status) {
return status != null ? status.getCode() : null;
}
/**
* 自定义方法:将 String 代码转换为 OrderStatus 枚举
* @param code 状态代码字符串
* @return 对应的枚举,如果找不到返回 null 或抛异常
*/
default OrderStatus codeToOrderStatus(String code) {
return OrderStatus.fromCode(code); // 调用枚举类中的静态查找方法
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
推荐使用自定义方法(方法 4)或表达式(方法 2),因为它们更清晰且灵活。@ValueMapping
主要用于两个不同枚举类型之间的常量映射。
# 六、高级功能与实践技巧
MapStruct 还提供了一些高级功能,帮助应对更复杂的场景和优化开发流程。
# 6.1 集合类型映射 (List
, Set
, Map
)
MapStruct 对集合映射提供了开箱即用的支持。只需在 Mapper 接口中定义接受和返回集合类型的方法,MapStruct 会自动生成遍历和逐个元素映射的代码。
import org.mapstruct.Mapper;
import java.util.List;
import java.util.Set;
import java.util.Map;
// 假设 User 和 UserDTO 类已定义
/**
* 集合映射器示例
*/
@Mapper(componentModel = "spring")
public interface CollectionMappingMapper {
// --- List 映射 ---
/**
* 将 List<User> 映射到 List<UserDTO>
* MapStruct 会自动调用 userToDto(User user) 方法来映射每个元素
* @param userList 源列表
* @return 目标列表
*/
List<UserDTO> mapUserListToDtoList(List<User> userList);
// 假设存在 userToDto(User user) 方法
// --- Set 映射 ---
/**
* 将 Set<User> 映射到 Set<UserDTO>
* @param userSet 源 Set
* @return 目标 Set
*/
Set<UserDTO> mapUserSetToDtoSet(Set<User> userSet);
// --- Map 映射 ---
/**
* 将 Map<Integer, User> 映射到 Map<Integer, UserDTO>
* Key 保持不变,Value 通过 userToDto 进行映射
* @param userMap 源 Map
* @return 目标 Map
*/
Map<Integer, UserDTO> mapUserMapToDtoMap(Map<Integer, User> userMap);
/**
* 将 Map<String, User> 映射到 Map<String, UserDTO>
* Key 是 String 类型
* @param userMap 源 Map
* @return 目标 Map
*/
Map<String, UserDTO> mapStringKeyUserMapToDtoMap(Map<String, User> userMap);
// --- 需要定义的单个元素映射方法 ---
UserDTO userToDto(User user); // MapStruct 会使用这个方法来映射集合中的元素
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
注意: MapStruct 在进行集合映射时,会查找并复用当前 Mapper 接口或其 uses
的其他 Mapper 中定义的单个元素的映射方法(如 userToDto
)。
# 6.2 使用表达式 (expression
) 进行复杂转换
当映射逻辑不仅仅是简单的字段复制或类型转换时,可以使用 @Mapping
注解的 expression
属性,直接嵌入 Java 代码片段。
import lombok.Data;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit; // 用于计算时间差
import java.util.List;
import java.util.stream.Collectors; // 用于 Stream API
@Data class Order { private Long id; private List<Item> items; private Customer customer; }
@Data class Item { private String name; private int quantity; private double price; }
@Data class Customer { private String firstName; private String lastName; }
@Data class OrderSummaryDTO { private Long orderId; private String customerFullName; private int totalItems; private double totalPrice; }
@Mapper(componentModel = "spring")
public interface OrderExpressionMapper {
@Mapping(source = "id", target = "orderId")
// 使用 expression 拼接客户全名
@Mapping(target = "customerFullName", expression = "java(order.getCustomer().getFirstName() + \" \" + order.getCustomer().getLastName())")
// 使用 expression 计算订单项总数
@Mapping(target = "totalItems", expression = "java(order.getItems().size())")
// 使用 expression 计算订单总价 (调用 default 方法)
@Mapping(target = "totalPrice", expression = "java(calculateTotalPrice(order.getItems()))")
OrderSummaryDTO orderToSummaryDTO(Order order);
/**
* default 方法用于计算订单总价,供 expression 调用
* @param items 订单项列表
* @return 总价格
*/
default double calculateTotalPrice(List<Item> items) {
if (items == null) {
return 0.0;
}
return items.stream()
.mapToDouble(item -> item.getPrice() * item.getQuantity())
.sum();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
表达式使用要点:
- 表达式内容必须是合法的 Java 代码片段,可以访问源对象的
getter
方法(如order.getCustomer().getFirstName()
)或调用当前 Mapper 中的default
方法(如calculateTotalPrice(...)
)。 - 使用
java(...)
将代码包裹起来。 - 表达式应返回目标字段所需的类型。
- 优点: 灵活性高,可以实现任意复杂的逻辑。
- 缺点:
- 将业务逻辑嵌入到映射层,可能违反职责分离原则。
- 表达式是字符串,缺乏编译时检查和 IDE 的良好支持。
- 过度复杂的表达式会降低代码可读性。
- 建议: 对于非常简单的逻辑(如字段拼接、简单计算)可以使用表达式;对于复杂的业务逻辑,优先考虑在 Mapper 中定义
default
方法或引入专门的 Service 类(通过uses
属性)。
# 6.3 生命周期回调 (@BeforeMapping
, @AfterMapping
)
允许在映射执行之前或之后插入自定义逻辑。
import lombok.Data;
import org.mapstruct.*; // 引入 @BeforeMapping, @AfterMapping, @MappingTarget
import java.time.LocalDateTime;
@Data class User { private Integer id; private String name; }
@Data class UserDTO { private Integer id; private String name; private String generatedInfo; }
@Mapper(componentModel = "spring")
public interface LifecycleCallbackMapper {
/**
* 映射方法
*/
UserDTO userToDtoWithCallbacks(User user);
/**
* 在映射执行之前调用。
* 可以用于验证、日志记录或预处理源对象。
* 方法可以有源对象参数。
* @param user 源 User 对象
*/
@BeforeMapping
default void beforeMapping(User user) {
System.out.println("开始映射 User: " + user.getName());
if (user.getId() == null) {
System.out.println("警告: User ID 为空,将生成默认 ID。");
// user.setId(generateDefaultId()); // 不推荐在 BeforeMapping 中修改源对象
}
}
/**
* 在映射执行之后调用。
* 可以用于后处理、补充数据、日志记录等。
* 方法可以有源对象和/或目标对象参数。
* 使用 @MappingTarget 注解目标对象参数,允许在方法内部修改目标对象。
* @param user 源 User 对象
* @param dto 目标 UserDTO 对象 (使用 @MappingTarget 标记)
*/
@AfterMapping
default void afterMapping(User user, @MappingTarget UserDTO dto) {
System.out.println("映射 User '" + user.getName() + "' 完成。");
// 在目标对象中添加额外生成的信息
dto.setGeneratedInfo("Mapped at " + LocalDateTime.now());
if (user.getId() != null && user.getId() < 0) {
dto.setName(dto.getName() + " [Invalid ID]"); // 根据源对象修改目标对象
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
回调方法要点:
- 必须是
default
方法或定义在uses
的类中的方法。 @BeforeMapping
: 方法参数可以是源对象类型、@MappingTarget
目标对象类型(用于更新映射)、或@TargetType
Class 对象。@AfterMapping
: 方法参数可以是源对象类型、@MappingTarget
目标对象类型、或@TargetType
Class 对象。- 使用
@MappingTarget
注解的参数允许在回调方法内部修改目标对象。
# 6.4 继承与组合映射器 (extends
, uses
)
对于大型项目,可以通过继承和组合来复用和组织 Mapper。
1. 继承 (extends
): 一个 Mapper 接口可以继承另一个 Mapper 接口,从而继承其所有映射方法和配置。
import org.mapstruct.Mapper;
/**
* 基础 Mapper,包含通用转换逻辑或配置
*/
@Mapper(componentModel = "spring")
public interface BaseMapperConfig {
// 可能包含通用的 @Mapper 配置,如 unmappedTargetPolicy
// 或通用的 default 转换方法
default String trimString(String input) { return input != null ? input.trim() : null; }
}
/**
* User Mapper 继承 BaseMapperConfig
* 会继承 BaseMapperConfig 中的配置和 default 方法
*/
@Mapper(componentModel = "spring")
public interface UserInheritanceMapper extends BaseMapperConfig {
@Mapping(target = "name", qualifiedByName = "trimString") // 可以使用继承来的 default 方法
UserDTO userToDto(User user);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2. 组合 (uses
): 一个 Mapper 可以通过 @Mapper(uses = {...})
属性来引用其他 Mapper 或包含自定义方法的类。MapStruct 在生成代码时,如果需要某个映射逻辑或自定义转换方法,会首先在当前 Mapper 中查找,如果找不到,则会去 uses
引用的类中查找。
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import java.util.Date;
import java.time.Instant;
import java.time.ZoneId;
/**
* 专门处理日期转换的工具类或 Mapper
*/
@Mapper(componentModel = "spring") // 也可以是普通类,只要方法是 public static 或 default (如果 uses 在接口上)
public interface DateMapper {
default LocalDateTime dateToLocalDateTime(Date date) {
return date != null ? Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault()).toLocalDateTime() : null;
}
default Date localDateTimeToDate(LocalDateTime localDateTime) {
return localDateTime != null ? Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()) : null;
}
}
/**
* Order Mapper,使用 DateMapper 来处理日期转换
*/
@Mapper(componentModel = "spring", uses = {DateMapper.class}) // 引用 DateMapper
public interface OrderCompositionMapper {
// MapStruct 会自动查找 DateMapper 中的 dateToLocalDateTime 方法
@Mapping(source = "orderDate", target = "orderDateTime")
OrderDTO orderToDto(OrderEntity entity);
// MapStruct 会自动查找 DateMapper 中的 localDateTimeToDate 方法
@Mapping(source = "orderDateTime", target = "orderDate")
OrderEntity dtoToEntity(OrderDTO dto);
}
// 假设 OrderEntity 使用 java.util.Date, OrderDTO 使用 LocalDateTime
@Data class OrderEntity { private Long id; private Date orderDate; /* 其他字段 */ }
@Data class OrderDTO { private Long id; private LocalDateTime orderDateTime; /* 其他字段 */ }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
继承 vs 组合:
- 继承: 更强的耦合,子 Mapper 隐式获得父 Mapper 的所有配置和方法。适用于共享通用配置或基础转换。
- 组合 (
uses
): 更松散的耦合,显式引用需要的辅助 Mapper 或工具类。更符合“组合优于继承”的原则,推荐优先使用。一个 Mapper 可以uses
多个其他类。
# 七、性能优化与最佳实践
虽然 MapStruct 本身性能优异,但在使用中遵循一些最佳实践可以进一步提升效率和代码质量。
# 7.1 MapStruct 性能优势分析
回顾一下 MapStruct 高性能的原因:
- 编译时生成代码: 避免了运行时的反射调用、字节码操作或动态代理。
- 直接方法调用: 生成的代码是简单的
getter/setter
调用,接近手动编写的性能。 - 无运行时开销: 映射逻辑在编译后固定,运行时没有额外的查找、匹配或转换决策过程。
与其他工具的性能对比总结:
工具 | 性能级别 | 主要特点 |
---|---|---|
MapStruct | 极高 (接近手动) | 编译时生成,无反射 |
手动编写 | 最高 | 基准线 |
CGLib BeanCopier | 非常高 | 运行时生成字节码,性能接近手动 |
Spring BeanUtils | 较好 | 反射,但有缓存 |
Apache PropertyUtils | 较好 | 反射,无类型转换,有缓存 |
Apache BeanUtils | 较差 | 反射,复杂类型转换,开销大 |
ModelMapper | 中等偏慢 | 反射,运行时配置,有缓存 |
Dozer | 较慢 | 反射,XML/API 配置,功能丰富 |
因此,在性能敏感的应用中,MapStruct 是兼顾开发效率和运行效率的理想选择。
# 7.2 编码最佳实践
保持 Mapper 接口简洁、职责单一:
- 每个 Mapper 接口应专注于特定领域或特定类型的映射(如
UserMapper
,OrderMapper
)。 - 避免创建包含所有映射逻辑的“上帝 Mapper”。
- 使用
uses
组合功能,而不是让单个 Mapper 过于庞大。
- 每个 Mapper 接口应专注于特定领域或特定类型的映射(如
使用清晰、一致的命名:
- 映射方法名应清晰表达转换方向,如
toDto()
,toEntity()
,entityListToDtoList()
。 - 自定义转换方法 (
default
或static
) 名称应明确其功能,如encodePassword()
,dateToStringUTC()
。
- 映射方法名应清晰表达转换方向,如
为复杂映射添加 Javadoc 注释:
- 解释
@Mapping
注解中expression
,qualifiedByName
,dateFormat
等的用途。 - 说明自定义转换方法 (
default
) 的逻辑和前提条件。 - 标注忽略的字段及其原因。
@Mapper(componentModel = "spring") public interface WellDocumentedMapper { /** * 将包含原始价格和折扣率的用户订单实体映射到包含最终价格的 DTO。 * * @param orderEntity 包含订单详情和客户信息的源实体 * @param discountRate 当前适用的折扣率 (例如 0.1 表示 10% 折扣) * @return 包含计算后最终价格的订单 DTO */ @Mapping(target = "finalPrice", expression = "java(orderEntity.getOriginalPrice() * (1 - discountRate))") OrderPriceDTO calculateFinalPrice(OrderEntity orderEntity, double discountRate); }
1
2
3
4
5
6
7
8
9
10
11
12- 解释
合理组织自定义逻辑:
- 简单的、与特定映射紧密相关的逻辑可以使用
expression
或当前 Mapper 的default
方法。 - 通用的、可在多个 Mapper 中复用的转换逻辑(如日期格式化、货币计算、数据脱敏),应提取到单独的工具类或专门的
Mapper
中,并通过uses
引用。 - 复杂的业务计算逻辑不应放在 Mapper 的
expression
或default
方法中,而应委托给专门的 Service 或 Helper 类。
- 简单的、与特定映射紧密相关的逻辑可以使用
# 7.3 避免常见陷阱
循环依赖 (Circular Dependencies):
- 问题: Mapper A
uses
Mapper B,同时 Mapper Buses
Mapper A。这会导致编译失败。 - 场景: 常见于双向关联的对象映射,如
Order
包含Customer
,Customer
包含List<Order>
。 - 解决方案:
- 合并 Mapper: 将双向映射逻辑放在同一个 Mapper 中。
- 使用
@Context
: 将一个 Mapper 作为上下文传递给另一个 Mapper 的方法(适用于更复杂的场景)。 - 通过
@AfterMapping
处理: 在一个方向映射完成后,手动设置另一个方向的关联(需要@MappingTarget
)。
// 使用 @Context 解决循环依赖示例 @Mapper(componentModel = "spring") public interface OrderCycleMapper { @Mapping(target = "customer", expression = "java(customerCycleMapper.toDto(order.getCustomer(), this))") // 传递 this 作为上下文 OrderDTO toDto(Order order, @Context CustomerCycleMapper customerCycleMapper); } @Mapper(componentModel = "spring") public interface CustomerCycleMapper { @Mapping(target = "orders", expression = "java(orderCycleMapper.toDtoList(customer.getOrders(), this))") // 传递 this 作为上下文 CustomerDTO toDto(Customer customer, @Context OrderCycleMapper orderCycleMapper); // 假设还有 toDtoList 方法 }
1
2
3
4
5
6
7
8
9
10
11
12
13- 问题: Mapper A
Null 值处理不当:
- 问题: 源对象或其嵌套属性为
null
时,访问getter
或在expression
中操作可能导致NullPointerException
。 - 解决方案:
- MapStruct 默认生成的代码通常会进行
null
检查。 - 在
expression
中务必添加null
判断:java(source.getNested() != null ? source.getNested().getProperty() : null)
。 - 在自定义
default
方法中处理null
输入。 - 考虑使用
NullValuePropertyMappingStrategy
(如IGNORE
) 或NullValueCheckStrategy
配置 Mapper 行为。
- MapStruct 默认生成的代码通常会进行
- 问题: 源对象或其嵌套属性为
过度依赖表达式 (
expression
):- 问题: 将复杂的业务逻辑、外部服务调用或数据库查询放入
expression
会导致 Mapper 职责不清、难以测试和维护。 - 解决方案: 保持
expression
简单,复杂逻辑移至 Service 层或专门的 Helper 类(可通过uses
引入 Helper 类)。
- 问题: 将复杂的业务逻辑、外部服务调用或数据库查询放入
# 八、与 Spring Boot 项目集成实战
在典型的 Spring Boot Web 应用中,MapStruct 主要用于 Controller、Service 和 Repository 层之间的数据转换。
# 8.1 依赖配置与组件扫描
如前文 3.1 和 3.3 所述,确保 pom.xml
或 build.gradle
中包含 mapstruct
, mapstruct-processor
(以及 Lombok 相关依赖,如果使用),并配置好注解处理器路径。
在 @SpringBootApplication
或专门的配置类中使用 @ComponentScan
确保包含 Mapper 接口所在的包路径,以便 Spring 能够发现并注册 @Mapper(componentModel = "spring")
生成的 Bean。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
// 扫描主应用包以及包含 Mapper 接口的包
@ComponentScan(basePackages = {"com.example.myapp", "com.example.myapp.mapper"})
public class MySpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApplication.class, args);
}
}
2
3
4
5
6
7
8
9
10
11
12
# 8.2 在 Service 和 Controller 中使用依赖注入
通过构造器注入(推荐)或 @Autowired
字段注入,在 Service 和 Controller 中使用 MapStruct Mapper 实例。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import org.springframework.http.ResponseEntity; // 引入 ResponseEntity
import org.springframework.http.HttpStatus; // 引入 HttpStatus
// 假设 User, UserDTO, UserMapper, UserRepository 已定义
@Service
public class UserManagementService {
private final UserRepository userRepository;
private final UserMapper userMapper; // 注入 Mapper
@Autowired
public UserManagementService(UserRepository userRepository, UserMapper userMapper) {
this.userRepository = userRepository;
this.userMapper = userMapper;
}
public UserDTO findUserById(Integer id) {
User user = userRepository.findById(id).orElse(null); // 简化处理
return userMapper.userToUserDTO(user); // 使用 Mapper 转换
}
public List<UserDTO> findAllUsers() {
List<User> users = userRepository.findAll();
return userMapper.usersToUserDTOs(users); // 使用 Mapper 批量转换
}
public UserDTO saveUser(UserDTO userDTO) {
User user = userMapper.userDTOToUser(userDTO); // DTO -> Entity
User savedUser = userRepository.save(user);
return userMapper.userToUserDTO(savedUser); // Entity -> DTO
}
}
@RestController
@RequestMapping("/api/v1/users")
public class UserApiController {
private final UserManagementService userService;
@Autowired
public UserApiController(UserManagementService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Integer id) {
UserDTO dto = userService.findUserById(id);
if (dto == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(dto);
}
@GetMapping
public List<UserDTO> listUsers() {
return userService.findAllUsers();
}
@PostMapping
public ResponseEntity<UserDTO> createUser(@RequestBody UserDTO userDTO) {
UserDTO savedDto = userService.saveUser(userDTO);
// 通常创建成功返回 201 Created 和资源 URI
return ResponseEntity.status(HttpStatus.CREATED).body(savedDto);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# 8.3 设计请求/响应专用 DTO
为了 API 的清晰和安全,通常会为 Controller 的请求 (@RequestBody
) 和响应 (@ResponseBody
或 ResponseEntity
) 设计专门的 DTO 类,而不是直接使用内部 DTO 或实体类。MapStruct 非常适合在这些专用 DTO 与内部模型之间进行转换。
import lombok.Data;
import javax.validation.constraints.*; // JSR 380 Bean Validation 注解
/**
* 创建用户的 API 请求体 DTO
* 包含验证注解
*/
@Data
class UserCreationRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 50)
private String username;
@NotBlank @Email
private String email;
@NotBlank @Size(min = 8)
private String password;
private String fullName;
}
/**
* 更新用户的 API 请求体 DTO (部分更新)
*/
@Data
class UserUpdateRequest {
@Email // 邮箱可选,但如果提供,必须格式正确
private String email;
private String fullName;
private Boolean active; // 允许更新状态
}
/**
* API 响应的用户信息 DTO (不含敏感信息)
*/
@Data
class UserApiResponse {
private Integer id;
private String username;
private String email;
private String fullName;
private String registrationDate; // 格式化日期
private boolean active;
}
// 在 Mapper 接口中添加这些 DTO 的映射方法
@Mapper(componentModel = "spring")
public interface UserApiMapper {
// 将 API 请求 DTO 转换为内部 User DTO 或 Entity
@Mapping(target = "id", ignore = true) // 创建时忽略 ID
@Mapping(target = "status", constant = "PENDING") // 设置默认状态
User internalDtoFromCreationRequest(UserCreationRequest request);
// 将内部 User DTO 或 Entity 转换为 API 响应 DTO
@Mapping(source = "createTime", target = "registrationDate", dateFormat = "yyyy-MM-dd")
UserApiResponse internalDtoToApiResponse(UserDTO internalDto);
// 用于部分更新的方法 (更新 Entity)
// target 不能是 DTO,必须是 Entity 或可变对象
void updateInternalUserFromUpdateRequest(UserUpdateRequest request, @MappingTarget User internalUser);
}
// Controller 中使用 UserApiMapper
@RestController
@RequestMapping("/api/v2/users")
class UserApiV2Controller {
// ... 注入 UserApiMapper 和 Service ...
@PostMapping
public ResponseEntity<UserApiResponse> createUser(@Valid @RequestBody UserCreationRequest request) {
User internalUser = userApiMapper.internalDtoFromCreationRequest(request);
// ... 调用 service 保存 internalUser ...
UserDTO savedInternalDto = userService.create(internalUser); // 假设 service 返回内部 DTO
UserApiResponse response = userApiMapper.internalDtoToApiResponse(savedInternalDto);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@PatchMapping("/{id}")
public ResponseEntity<UserApiResponse> updateUser(@PathVariable Integer id, @RequestBody UserUpdateRequest request) {
User existingInternalUser = userService.findInternalUserById(id); // 假设 service 返回内部 User
userApiMapper.updateInternalUserFromUpdateRequest(request, existingInternalUser);
// ... 调用 service 更新 existingInternalUser ...
UserDTO updatedInternalDto = userService.update(existingInternalUser);
UserApiResponse response = userApiMapper.internalDtoToApiResponse(updatedInternalDto);
return ResponseEntity.ok(response);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# 8.4 单元测试与集成测试
为 MapStruct Mapper 编写测试是保证映射逻辑正确性的关键。
单元测试 Mapper 接口:
由于 MapStruct 生成的是具体实现类,可以直接对 Mapper 接口进行单元测试(如果使用 componentModel="spring"
,需要 Spring 测试环境;如果用 default
,可以直接 Mappers.getMapper
)。
import org.junit.jupiter.api.Test;
import org.mapstruct.factory.Mappers;
import static org.junit.jupiter.api.Assertions.*;
import java.time.LocalDateTime;
// 假设 User, UserDTO, UserBaseMapper (default model) 定义如前
/**
* UserBaseMapper 的单元测试 (使用 default model)
*/
class UserBaseMapperTest {
// 获取 Mapper 实例
private final UserBaseMapper mapper = Mappers.getMapper(UserBaseMapper.class);
@Test
void testUserToUserDTO() {
// Arrange: 准备源对象
User user = User.builder()
.id(1)
.username("test")
.emailAddress("test@example.com")
.createdAt(LocalDateTime.of(2024, 5, 10, 12, 0, 0))
.isActive(true)
.build();
// Act: 执行映射
UserDTO dto = mapper.userToUserDTO(user);
// Assert: 验证结果
assertNotNull(dto);
assertEquals(user.getId(), dto.getId());
assertEquals(user.getUsername(), dto.getUsername());
assertEquals(user.getEmailAddress(), dto.getEmail()); // 验证 emailAddress -> email
assertEquals("2024-05-10", dto.getCreationDate()); // 验证 createdAt -> creationDate (格式 yyyy-MM-dd)
assertEquals(user.isActive(), dto.isActive());
}
@Test
void testUserDTOToUser() {
// Arrange: 准备源 DTO
UserDTO dto = UserDTO.builder()
.id(2)
.username("dtoUser")
.email("dto@example.com")
.creationDate("2024-06-15") // String 类型日期
.active(false)
.build();
// Act: 执行映射
User user = mapper.userDTOToUser(dto);
// Assert: 验证结果
assertNotNull(user);
assertEquals(dto.getId(), user.getId());
assertEquals(dto.getUsername(), user.getUsername());
assertEquals(dto.getEmail(), user.getEmailAddress()); // 验证 email -> emailAddress
assertNotNull(user.getCreatedAt());
assertEquals(2024, user.getCreatedAt().getYear());
assertEquals(6, user.getCreatedAt().getMonthValue());
assertEquals(15, user.getCreatedAt().getDayOfMonth()); // 验证 creationDate -> createdAt
assertEquals(dto.isActive(), user.isActive());
assertNull(user.getPasswordHash()); // 验证 passwordHash 被忽略
}
// 可以添加更多测试用例,覆盖 null 值、边界情况等
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
Spring Boot 集成测试 Controller:
在集成测试中,可以验证从 Controller 接收请求、Service 处理、Mapper 转换到最终响应的整个流程。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import com.fasterxml.jackson.databind.ObjectMapper; // Jackson 用于序列化请求体
// 假设 MySpringBootApplication, UserApiController, UserCreationRequest 等已定义
/**
* UserApiController 的集成测试
* 使用 MockMvc 模拟 HTTP 请求,验证端到端的流程
*/
@SpringBootTest // 加载完整的 Spring Boot 应用上下文
@AutoConfigureMockMvc // 自动配置 MockMvc
class UserApiControllerIntegrationTest {
@Autowired
private MockMvc mockMvc; // 用于执行模拟 HTTP 请求
@Autowired
private ObjectMapper objectMapper; // 用于将请求对象序列化为 JSON
// @Autowired private UserRepository userRepository; // 可以注入 Repository 来准备/验证数据
@Test
void testCreateUserApi() throws Exception {
// Arrange: 准备请求体 DTO
UserCreationRequest request = new UserCreationRequest();
request.setUsername("integrationTestUser");
request.setEmail("integration@example.com");
request.setPassword("password1234");
request.setFullName("Integration Test");
// Act & Assert: 执行 POST 请求并验证响应
mockMvc.perform(post("/api/v2/users") // 目标 API 端点
.contentType(MediaType.APPLICATION_JSON) // 设置请求类型为 JSON
.content(objectMapper.writeValueAsString(request))) // 将请求对象序列化为 JSON 字符串
.andExpect(status().isCreated()) // 验证 HTTP 状态码为 201 Created
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) // 验证响应类型
.andExpect(jsonPath("$.id").exists()) // 验证响应体中包含 id 字段 (jsonPath 表达式)
.andExpect(jsonPath("$.username").value(request.getUsername())) // 验证 username
.andExpect(jsonPath("$.email").value(request.getEmail())) // 验证 email
.andExpect(jsonPath("$.fullName").value(request.getFullName()))
.andExpect(jsonPath("$.registrationDate").exists()) // 验证日期存在
.andExpect(jsonPath("$.active").value(true)); // 假设创建后默认是 active
}
// 可以添加 GET, PATCH, DELETE 等其他端点的集成测试
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 8.5 常见问题与解决方案
Mapper Bean 未注入 (
NullPointerException
):- 检查
@Mapper
注解: 确保接口上有@Mapper(componentModel = "spring")
。 - 检查组件扫描: 确保
@SpringBootApplication
或@ComponentScan
的basePackages
包含了 Mapper 接口所在的包。 - 检查编译: 确保项目已成功编译,并且 MapStruct 生成了
Impl
类。清理并重新构建项目 (mvn clean install
或gradle clean build
)。 - 检查 Lombok 兼容性: 如果使用了 Lombok,确保添加了
lombok-mapstruct-binding
依赖并正确配置了maven-compiler-plugin
的annotationProcessorPaths
顺序。
- 检查
循环依赖 (Circular Dependency):
- 场景:
OrderMapper
uses
CustomerMapper
,同时CustomerMapper
uses
OrderMapper
。 - 解决方案:
- 合并: 将两个 Mapper 合并成一个,如
BusinessMapper
。 - 使用
@Context
: 在需要循环调用的方法参数中加入@Context
注解的其他 Mapper 实例。 - 重构设计: 审视对象模型和 DTO 设计,是否能避免双向强依赖。
- 合并: 将两个 Mapper 合并成一个,如
- 场景:
无法找到合适的映射方法:
- 检查方法签名: 确保源类型和目标类型与 Mapper 中定义的方法匹配。
- 检查
uses
: 如果依赖其他 Mapper 或工具类的方法,确保已通过uses = {...}
引入。 - 检查自定义方法: 如果使用
default
或static
方法,确保方法签名(参数类型、返回类型)正确。 - 检查
qualifiedByName
: 如果使用@Named
和qualifiedByName
,确保名称匹配且方法唯一。
复杂逻辑难以实现:
- 拆分逻辑: 不要试图在一个
@Mapping
或expression
中完成所有事情。 - 使用
default
方法: 将复杂转换逻辑封装在 Mapper 接口的default
方法中。 - 引入 Helper 类: 对于非常复杂的业务逻辑或需要外部依赖(如 Service),创建一个 Helper 类,在 Mapper 中通过
uses
引入,并在@Mapping(expression = "java(myHelper.doComplexLogic(source.getField()))")
中调用。
- 拆分逻辑: 不要试图在一个
更新映射 (
@MappingTarget
) 不生效:- 检查
@MappingTarget
: 确保更新方法的目标参数(通常是实体对象)使用了@MappingTarget
注解。 - 检查
ignore = true
: 确保没有在@Mapping
中意外地将需要更新的字段设置为ignore = true
。 - 检查 Setter 方法: 确保目标对象的字段有公开的
setter
方法。
- 检查
核心总结
MapStruct 作为一款顶级的 Java Bean 映射框架,通过其编译时代码生成的核心机制,完美地结合了高性能、类型安全和易用性。它有效解决了手动映射和基于反射的映射工具(如 BeanUtils)的诸多痛点,显著提高了开发效率和代码质量。
关键优势:
- 性能卓越: 接近手动编写
getter/setter
的性能。 - 类型安全: 编译时检查,错误早发现。
- 功能强大: 支持名称/类型不匹配、集合/嵌套对象、枚举、自定义逻辑等复杂场景。
- 声明式配置: 通过注解定义映射规则,代码清晰。
- 框架集成: 与 Spring Boot 等 DI 框架无缝集成。
适用场景:
- 任何需要进行对象间属性复制的 Java 项目。
- 分层架构中的 Entity, DTO, VO 转换。
- 微服务间的数据模型转换。
- 对性能和类型安全有较高要求的场景。
掌握 MapStruct 不仅能让你摆脱繁琐的对象转换代码,更能提升你构建高质量、易维护 Java 应用的能力。随着其社区的活跃和功能的不断完善,MapStruct 无疑是现代 Java 开发工具箱中不可或缺的一员。