第五章. MapStruct-Plus :数据传输对象 DTO 到业务对象 BO 的相互映射

第五章. MapStruct-Plus :数据传输对象 DTO 到业务对象 BO 的相互映射

摘要:本章我们将正式引入 MapStruct-Plus (MSP),通过“入站”数据流(DTO -> BO)的实战,彻底重构之前的开发模式。我们将建立符合阿里巴巴规范的分层架构,深入理解 MSP 如何通过 @AutoMapper 注解消除繁琐的接口定义,并掌握全局 Converter 的依赖注入机制。

本章学习路径

  1. 环境重塑:清理旧的 MapStruct 依赖,引入 MSP Starter,并配置关键的编译顺序(AST 冲突解决)。
  2. 架构落地:初始化 client (传输层) 与 domain (领域层) 的包结构。
  3. 核心对比:通过 Tab 对比,直观感受“接口优先”与“注解优先”的差异。
  4. 入站实战:编写 UserCreateDTO,使用 @AutoMapper 建立通往 UserBO 的数据桥梁。
  5. 全局调用:掌握 io.github.linpeilie.Converter 的统一调用方式。

5.1. 环境依赖清洗与重构

在开始新的架构之前,我们需要确保工程环境的纯净。MSP (MapStruct-Plus) 是基于 MapStruct 的增强封装,为了避免类加载冲突(Jar Hell),我们需要移除原生的 MapStruct 依赖,转而使用 MSP 的全家桶 Starter。

5.1.1. 依赖变更

文件路径pom.xml

请打开项目根目录下的 pom.xml 文件,执行以下操作:

  1. 删除:移除原有的 org.mapstruct:mapstructmapstruct-processor 依赖。
  2. 新增:引入 mapstruct-plus-spring-boot-starter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependencies>
<!-- 移除旧的 mapstruct 依赖,替换为 MSP Starter -->
<dependency>
<groupId>io.github.linpeilie</groupId>
<artifactId>mapstruct-plus-spring-boot-starter</artifactId>
<version>1.4.5</version>
</dependency>

<!-- 保持 Lombok 依赖 (POJO 基础) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
</dependencies>

5.1.2. 编译插件配置(至关重要)

MapStruct 和 Lombok 都是基于 JSR-269 的注解处理器(Annotation Processor)。它们在编译期修改字节码(AST 修改)。

  • Lombok:生成 getter/setter。
  • MapStruct:读取 getter/setter 生成转换代码。

如果 MapStruct 先执行,它会发现对象里没有 getter/setter(因为 Lombok 还没干活),从而导致无法生成映射代码。因此,必须严格控制插件的执行顺序

文件路径pom.xml -> <build><plugins>

1
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
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<!-- 1. Lombok 必须排在第一位 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>

<!-- 2. MSP 处理器排在第二位 -->
<path>
<groupId>io.github.linpeilie</groupId>
<artifactId>mapstruct-plus-processor</artifactId>
<version>1.4.5</version>
</path>

<!-- 3. (可选) 如果使用 Lombok 的 Builder 模式,需添加此绑定 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

5.2. 初始化分层包结构

根据阿里巴巴 Java 开发手册的分层规范,我们不再把所有类都堆在 entity 包下。我们需要明确区分 数据传输对象 (DTO)业务对象 (BO)

5.2.1. 创建目录

请在 IDE 中按照以下结构创建包:

目录树结构

1
2
3
4
5
6
7
8
9
src/main/java/com/example/demo/
├── client/
│ └── dto/ # 存放 DTO (Data Transfer Object)
│ └── UserCreateDTO.java
├── domain/
│ └── bo/ # 存放 BO (Business Object)
│ └── UserBO.java
├── controller/ # 存放 Web 接口
└── config/ # 存放全局配置
  • client.dto:这是“入站”的最前线,接收前端传来的 JSON 参数。
  • domain.bo:这是业务的内核,Service 层只处理 BO,不关心 DTO 的存在。

5.3. MSP 核心理念:零接口开发

在编写代码前,我们必须理解 MSP 究竟改变了什么。它将 MapStruct 的 Interface-First(接口定义优先) 模式转变为 Annotation-First(注解绑定优先) 模式。

以下通过对比展示两种模式在实现 DTO -> BO 时的差异:

繁琐的接口定义模式

在原生模式下,你需要手动创建一个接口文件,添加 @Mapper 注解,定义方法签名。随着业务增长,这个接口文件会变得极其庞大且难以维护。

1
2
3
4
5
6
7
8
9
// 必须手动创建 Mapper 接口
@Mapper(componentModel = "spring")
public interface UserMapper {

UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);

// 必须手动写出转化方法签名
UserBO toBO(UserCreateDTO dto);
}

极速的注解驱动模式

在 MSP 模式下,Mapper 接口文件消失了。你只需要在类头上加一个 @AutoMapper 注解,编译器会自动帮你生成背后的接口和实现类。

1
2
3
4
5
// 直接在类上声明转化关系
@AutoMapper(target = UserBO.class) // 一行注解搞定
public class UserCreateDTO {
// 属性...
}

5.4. 入站实战:DTO 到 BO 的转化

现在我们模拟一个用户注册场景。前端提交了用户名、手机号和密码,我们需要将这些数据转化为业务对象,以便在 Service 层进行处理。

5.4.1. 定义业务对象 (BO)

首先定义转化的 目标,即业务对象。BO 对象应该包含业务逻辑所需的所有属性,它是纯净的,不包含任何 @NotNull 等前端校验注解。

文件路径src/main/java/com/example/demo/domain/bo/UserBO.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.demo.domain.bo;

import lombok.Data;

@Data
public class UserBO {
/** 用户名 */
private String username;

/** 手机号 */
private String phone;

/** 密码 (业务层处理加密) */
private String password;

/** 来源 (如: Android, iOS) */
private String source;
}

5.4.2. 定义传输对象 (DTO) 并绑定映射

接下来定义 源头,即传输对象。DTO 负责接收外部参数,并承载基础的格式校验。

关键操作:我们需要在 DTO 上添加 @AutoMapper 注解,告诉 MSP:“请在编译时生成代码,将本类转换为 UserBO”。

文件路径src/main/java/com/example/demo/client/dto/UserCreateDTO.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.demo.client.dto;

import com.example.demo.domain.bo.UserBO;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;

@Data
// 核心注解:声明当前类 (DTO) 可以转换为 目标类 (BO)
// MSP 会在编译期自动生成 UserCreateDTOToUserBOMapper 接口及其实现
@AutoMapper(target = UserBO.class)
public class UserCreateDTO {

private String username;

private String phone;

private String password;

// 假设前端传入的字段叫 "platform",而 BO 中叫 "source"
// 我们暂时不处理这个异名映射,先观察默认行为
private String platform;
}

5.4.3. 编写 Controller 进行全链路验证

配置完成后,我们不需要写任何 Mapper 接口,直接在 Controller 中注入 MSP 的全局转换器 Converter

文件路径src/main/java/com/example/demo/controller/UserRegistrationController.java

1
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
package com.example.demo.controller;

import com.example.demo.client.dto.UserCreateDTO;
import com.example.demo.domain.bo.UserBO;
import io.github.linpeilie.Converter; // 1. 引入 MSP 核心转换器
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserRegistrationController {

// 2. 注入全局转换器 (底层会自动路由到生成的 Mapper)
private final Converter converter;

@PostMapping("/register")
public UserBO register(@RequestBody UserCreateDTO dto) {
// 3. 模拟接收到的数据
System.out.println("接收到 DTO: " + dto);

// 4. 执行转化:DTO -> BO
// 语法:converter.convert(源对象, 目标类.class)
UserBO bo = converter.convert(dto, UserBO.class);

System.out.println("转化为 BO: " + bo);

// 返回 BO 仅供测试验证
return bo;
}
}

5.4.4. 启动验证与代码审计

步骤 1:执行编译
在终端执行 mvn clean compile。此时 MSP 的注解处理器开始工作。

步骤 2:生成代码审计
请到项目的 target/generated-sources/annotations 目录下查看。你应该能找到一个名为 com.example.demo.client.dto.UserCreateDTOToUserBOMapper 的类。

1
2
3
4
5
6
7
8
9
10
11
12
// 自动生成的代码片段
@Component
public class UserCreateDTOToUserBOMapper extends BaseMapper<UserCreateDTO, UserBO> {
@Override
public UserBO convert(UserCreateDTO source, UserBO target) {
// ... 自动生成的 setter/getter 代码 ...
// 注意:由于 platform 和 source 名字不同,这里并没有生成赋值代码
target.setUsername(source.getUsername());
// ...
return target;
}
}

步骤 3:Postman 调用
发送 POST 请求到 http://localhost:8080/users/register

Body:

1
2
3
4
5
6
{
"username": "linus",
"phone": "13800000000",
"password": "123",
"platform": "IOS"
}

响应结果:

1
2
3
4
5
6
{
"username": "linus",
"phone": "13800000000",
"password": "123",
"source": null
}

结果分析

  • username, phone 转换成功:证明 @AutoMapper 生效。
  • sourcenull:证明 MSP 默认只处理同名属性,异名属性(platform vs source)被忽略了。我们在下一章处理这个问题。

5.5. 全局策略配置

在刚才的实战中,platform 字段因为没有匹配到目标字段而被静默忽略了。在生产环境中,这种“静默”是非常危险的,可能导致数据丢失而不自知。

我们需要配置 MSP,使其在发现未映射字段时发出警告。

5.5.1. 创建配置类

MSP 提供了 @MapperConfig 注解来控制全局行为,我们首先需要开启 pom.xml 下的 maven 编译预警

1
2
3
4
5
6
7
8
9
10
11
12
<configuration>
<source>17</source>
<target>17</target>
<!-- 显示编译期告警,便于查看 MapStruct 未映射字段等提示 -->
<showWarnings>true</showWarnings>
<compilerArgs>
<arg>-Xlint:all</arg>
</compilerArgs>
<annotationProcessorPaths>
....
</annotationProcessorPaths>
</configuration>

然后再配置类中新增配置

文件路径src/main/java/com/example/demo/config/MapStructPlusConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.demo.config;

import io.github.linpeilie.annotations.MapperConfig;
import org.mapstruct.ReportingPolicy;

// 全局配置:作用于所有自动生成的 Mapper
@MapperConfig(
// 1. 当源对象 (DTO) 有字段未被映射到目标对象时 -> 忽略 (DTO 字段通常多于 BO)
unmappedSourcePolicy = ReportingPolicy.IGNORE,

// 2. 当目标对象 (BO) 有字段未被赋值时 -> 警告 (防止业务属性遗漏)
unmappedTargetPolicy = ReportingPolicy.WARN
)
public class MapStructPlusConfig {
// 这是一个标记类,不需要写代码
}

配置完成后,再次执行 mvn compile,如果 UserBO 中有字段未被赋值,控制台将会打印 Warning 日志,提醒开发者检查映射规则。

image-20251211164316399


5.6. 本章小结

本章我们完成了从“传统 Mapper 接口”到“MSP 注解驱动”的架构转型,并打通了 入站 (DTO -> BO) 的数据链路。

核心要点

  1. 架构分层:DTO 用于传输,BO 用于业务,两者通过 MSP 解耦。
  2. 零接口:在 Source 类上使用 @AutoMapper(target = Target.class) 即可自动生成转换器。
  3. 统一调用:注入 Converter 接口,使用 .convert(source, targetClass) 方法,无需关心底层实现。

场景化代码速查

场景:前端传入注册表单,需要转为业务对象。
方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. DTO 定义 (Source)
@Data
@AutoMapper(target = UserBO.class) // 指向目标
public class UserRegisterDTO {
private String username;
}

// 2. BO 定义 (Target)
@Data
public class UserBO {
private String username;
}

// 3. Controller 调用
@Autowired Converter converter;
UserBO bo = converter.convert(dto, UserBO.class);

在下一章中,我们将深入业务核心层。UserBO 需要被持久化到数据库(转换 UserPO),这中间将面临 枚举转换 (Enum vs Int)复杂 JSON 字段 的挑战,我们将展示 MSP 如何与 MyBatis-Plus 完美配合解决这些难题。