第八章. MapStruct-Plus:集合、流与分页
摘要:在前面的章节中,我们已经打通了单体对象(UserBO -> UserVO)的映射通道。但在现实业务中,我们更多时候是在处理“列表”。本章我们将基于已有的单体映射配置,通过实战解锁 MSP 的 自动集合映射 和 MyBatis-Plus 分页集成 能力,并结合 Java 8 Stream API 实现高效的数据清洗。
本章学习路径
- 数据准备:在 Service 层快速构建模拟批量数据,为实战做准备。
- 集合实战:无需新增任何注解,直接实现
List<BO> 到 List<VO> 的转换。 - 流式结合:在 Stream 流处理中融入 Converter,实现“过滤+排序+转换”一条龙。
- 分页实战:体验 MSP 与 MyBatis-Plus 的深度集成,一行代码完成
Page 对象的整体转换。
8.1. 准备工作:构建数据源
为了验证批量转换的效果,我们首先需要在 UserService 中模拟一些数据。为了专注于映射本身,我们暂时不操作数据库,而是直接在内存中生成对象。
请打开或新建 src/main/java/com/example/demo/service/UserService.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 35 36
| package com.example.demo.service;
import cn.hutool.core.map.MapUtil; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.demo.domain.bo.UserBO; import com.example.demo.domain.enums.UserStatus; import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.List;
@Service public class UserService {
public List<UserBO> mockBatchQuery() { List<UserBO> list = new ArrayList<>(); for (int i = 1; i <= 5; i++) { UserBO bo = new UserBO(); bo.setId((long) i); bo.setUsername("User_" + i); bo.setPhone("1380000000" + i); bo.setStatus(i % 2 != 0 ? UserStatus.ENABLE : UserStatus.DISABLE); bo.setExtra(MapUtil.of("score", i * 100)); list.add(bo); } return list; } }
|
8.2. 场景一:自动集合映射 (List)
很多同学会有疑问:“我在第七章只定义了 UserBO 到 UserListVO 的一对一映射,现在我要转一个 List,需要再去写一个 toVOList 方法吗?”
答案是:完全不需要。
MapStruct Plus 的底层机制非常智能,它只要发现你定义了元素 A 到 B 的映射,就会自动支持 List<A> 到 List<B> 的转换。
8.2.1. 编写 Controller 验证
我们在 UserViewController 中增加一个接口,直接返回转换后的列表。
文件路径:src/main/java/com/example/demo/controller/UserViewController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
private final UserService userService;
@GetMapping("/list/all") public List<UserListVO> getAllUsers() { List<UserBO> boList = userService.mockBatchQuery(); List<UserListVO> voList = converter.convert(boList, UserListVO.class); return voList; }
|
8.2.2. 验证结果
启动项目,访问 http://localhost:8080/users/list/all。
观察响应:
1 2 3 4 5
| [ { "username": "User_1", "phoneMask": "138****0001" }, { "username": "User_2", "phoneMask": "138****0002" }, ... ]
|
我们看到,虽然我们从未显式定义 List 的转换规则,但所有数据都成功转换成了 UserListVO,并且手机号都应用了脱敏规则。
8.3. 场景二:流式处理 (Stream + Convert)
实际业务往往更复杂:我们需要先过滤掉“禁用”的用户,再按“积分”排序,最后才输出 VO。这时,将 MSP 结合 Java 8 Stream API 使用是最佳实践。
MSP 的 converter 接口设计得非常符合函数式编程习惯,可以完美嵌入 map 操作中。
8.3.1. 编写带逻辑的转换代码
继续在 UserViewController 中添加接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @GetMapping("/list/active") public List<UserListVO> getActiveUsers() { List<UserBO> boList = userService.mockBatchQuery();
return boList.stream() .filter(bo -> bo.getStatus() == UserStatus.ENABLE) .map(bo -> converter.convert(bo, UserListVO.class)) .toList(); }
|
8.3.2. 验证结果
访问 http://localhost:8080/users/list/active。
观察响应:
1 2 3 4 5
| [ { "username": "User_1", "phoneMask": "138****0001" }, { "username": "User_3", "phoneMask": "138****0003" }, { "username": "User_5", "phoneMask": "138****0005" } ]
|
结果中只剩下了 User_1, 3, 5。这证明了我们可以在数据流转的任意环节插入 converter,实现灵活的业务编排。
8.4. 场景三:分页映射 (IPage)
这是 Web 开发中最高频的场景。MyBatis-Plus 查询返回的是 IPage<UserPO>(或 BO),但前端接口文档要求返回 IPage<UserVO>。
由于 Java 的 泛型擦除 机制,直接尝试将 Page<UserBO> 强转为 Page<UserVO> 是极其危险的。为了保证类型安全并精确控制元数据,标准做法分为两步:
- 转换内容:提取
records 列表,利用 MSP 进行批量转换。 - 重组对象:创建一个新的
Page 对象,填入转换后的列表,并拷贝 total、current 等分页参数。
8.4.1. 模拟分页数据
回到 UserService,添加一个模拟分页返回的方法:
1 2 3 4 5 6 7 8 9 10 11 12
| public IPage<UserBO> mockPageQuery(int current, int size) { List<UserBO> allData = mockBatchQuery(); IPage<UserBO> page = new Page<>(current, size); page.setTotal(allData.size()); page.setPages(1); page.setRecords(allData); return page; }
|
8.4.2. 编写分页接口
在 UserViewController 中添加分页接口。这里我们展示最稳健的 “三步走” 写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @GetMapping("/page") public IPage<UserListVO> getPage(@RequestParam(defaultValue = "1") int current, @RequestParam(defaultValue = "10") int size) { IPage<UserBO> boPage = userService.mockPageQuery(current, size);
List<UserListVO> voList = converter.convert(boPage.getRecords(), UserListVO.class);
IPage<UserListVO> resultPage = new Page<>(boPage.getCurrent(), boPage.getSize(), boPage.getTotal()); resultPage.setRecords(voList); resultPage.setPages(boPage.getPages());
return resultPage; }
|
8.4.3. 封装通用工具 (推荐)
虽然上面的代码很稳,但在每个 Controller 里都写这几行略显繁琐。我们可以封装一个简单的工具类来简化操作。
文件路径:src/main/java/com/example/demo/infrastructure/utils/PageUtils.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
| package com.example.demo.infrastructure.utils;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import io.github.linpeilie.Converter; import java.util.List;
public class PageUtils {
public static <T, R> IPage<R> toPage(IPage<T> sourcePage, Class<R> targetClass, Converter converter) { List<R> targetList = converter.convert(sourcePage.getRecords(), targetClass); IPage<R> resultPage = new Page<>(sourcePage.getCurrent(), sourcePage.getSize(), sourcePage.getTotal()); resultPage.setRecords(targetList); resultPage.setPages(sourcePage.getPages()); return resultPage; } }
|
Controller 调用优化:
1 2 3 4 5 6 7
| @GetMapping("/page-util") public IPage<UserListVO> getPageUtil(@RequestParam(defaultValue = "1") int current, @RequestParam(defaultValue = "10") int size) { IPage<UserBO> boPage = userService.mockPageQuery(current, size); return PageUtils.toPage(boPage, UserListVO.class, converter); }
|
8.4.4. 验证结果
访问 http://localhost:8080/users/page?current=1&size=10。
观察响应:
1 2 3 4 5 6 7 8 9 10 11
| { "records": [ { "username": "User_1", "phoneMask": "138****0001" }, ], "total": 5, "size": 10, "current": 1, "pages": 1, "searchCount": true }
|
结果确认:
- 数据转换成功:
records 中的字段已根据 UserListVO 的规则进行了脱敏。 - 结构保持一致:分页元数据完整保留。
- 零报错风险:完全遵循 Java 强类型规范,避开了运行时类型转换异常。
8.5. 本章总结与集合映射速查
本章我们攻克了批量数据处理的三大关卡:列表自动映射、Stream 流式编排以及 MyBatis-Plus 分页集成。
遇到以下 3 种批量场景时,请直接 Copy 下方的标准代码模版:
8.5.1. 场景一:普通 List 转换
需求:Service 返回 List<BO>,Controller 需要返回 List<VO>。
方案:直接调用 convert,MSP 自动支持集合遍历。
1 2 3
| List<UserBO> boList = service.findAll();
List<UserVO> voList = converter.convert(boList, UserVO.class);
|
8.5.2. 场景二:Stream 流式处理
需求:在转换前需要进行过滤(Filter)、排序(Sorted)或去重。
方案:在 Stream.map 中嵌入 converter。
1 2 3 4 5
| List<UserVO> voList = boList.stream() .filter(bo -> bo.getIsActive()) .map(bo -> converter.convert(bo, UserVO.class)) .sorted(Comparator.comparing(UserVO::getId)) .collect(Collectors.toList());
|
8.5.3. 场景三:MyBatis-Plus 分页转换
需求:数据库查出 IPage<PO>,接口返回 IPage<VO>,且必须保留分页元数据。
方案:为了绝对的类型安全,建议解包后重组,或使用工具类。
1 2 3 4 5 6 7 8
| IPage<UserPO> poPage = userMapper.selectPage(...);
List<UserVO> voList = converter.convert(poPage.getRecords(), UserVO.class);
IPage<UserVO> voPage = new Page<>(poPage.getCurrent(), poPage.getSize(), poPage.getTotal()); voPage.setRecords(voList);
|