1. 背景
在日常业务开发过程中,我们经常会用到Java pojo(Plain Old Java Object,即Java普通对象)类的转换,列如从:
- 数据库层模型对象(DO)转换为业务领域模型(model)
- 业务领域模型转换为VO(前端响应对象)
- 数据传输对象(DTO)转换为业务领域模型等。
以一次简单的web请求为例,完整的请求过程中模型转换如下:
1.1 传统开发模式
- 仓储层查询数据详情
public interface TaskRepository {
Task queryById(long taskId);
}
@Service
public class TaskRepositoryImpl implements TaskRepository {
@Autowired
private TaskDAO taskDAO;
@Override
public Task queryById(long taskId) {
//从数据库查询任务详情
TaskDO taskDO = taskDAO.queryById(taskId);
//Dao模型->业务领域模型
Task task = new Task();
task.setId(taskDO.getId());
task.setName(taskDO.getName());
task.setBizNo(taskDO.getBizNo());
//省略多余代码
return task;
}
}
- 服务层响应前端请求
@RestController
@RequestMapping(value = "/task")
public class TaskController {
@Autowired
private TaskService taskService;
@RequestMapping("/detail/{id}")
public TaskVO queryTaskById(@PathVariable Long id) {
TaskVO taskVO = taskService.queryByTaskId(id);
return taskVO;
}
}
public interface TaskService {
/**
* 根据id查询任务详情
*
* @param taskId 任务id
* @return
*/
TaskVO queryTaskDetailById(long taskId);
}
@Service
@Slf4j
public class TaskServiceImpl implements TaskService {
@Autowired
private TaskRepository taskRepository;
@Override
public TaskVO queryByTaskId(long taskId) {
log.info("查询任务详情:{}", taskId);
//从数据库查询任务详情
Task task = taskRepository.queryById(taskId);
//模型转换
TaskVO taskVO = new TaskVO();
taskVO.setId(task.getId());
taskVO.setName(task.getName());
taskVO.setBizNo(task.getBizNo());
//省略多余代码
return taskVO;
}
}
1.2 传统模式的缺点
传统的开发模式基于getter和setter来实现pojo的的模型转换,不难看出,模型间的转换属于简单且重复劳动过程,当bean的字段较多时,除了代码的冗长不说,转换过程极其容易出错,列如:源字段和目标字段均为String类型,想必在开发过程中大概率会出现以下代码:
TaskVO taskVO = new TaskVO();
taskVO.setId(task.getId());
//目标字段为任务名称,源字段为业务名称
taskVO.setName(task.getBizName());
2. 工具化思维能力
想成为一名优秀的软件开发者,工具化思维能力是我们所必须具备的。在软件开发过程中,我们必须要具备DRY思想,即Don't Repeat Yourself(不要重复自己)。
优秀的程序员同时也是一个"懒"的程序员。任何重复的过程和代码,都需要用面向对象和工具化思维方式去重构。列如:使用抽象类抽取子类的公用代码,对于pojo类转换可以引入Java bean转换工具。
3. MapStruct使用
3.1 简介
官方地址:mapstruct.org/
官方简介:
MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types based on a convention over configuration approach. The generated mapping code uses plain method invocations and thus is fast, type-safe and easy to understand.
MapStruct 是一个代码生成器,它基于约定优于配置方法极大地简化了Java bean类型之间映射的实现。
生成的映射代码使用简单的方法调用,因此具备了速度快、类型安全,且易于理解的特点。
3.2 MapStruct引入
- maven依赖
<!--#region 对象转换工具-->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
<scope>provided</scope>
</dependency>
<!--#endregion 对象转换工具-->
3.3 MapStruct使用
下面以查询任务详情(Task)为例说明:
模型定义
- 数据库层模型定义:
@Data
public class TaskDO {
/**
* 任务id
*/
private Long id;
/**
* 任务名称
*/
private String name;
/**
* 任务状态
*/
private String status;
/**
* 业务编号
*/
private String bizNo;
/**
* 业务名称
*/
private String bizName;
/**
* 业务扩展信息
*/
private String bizExtInfo;
}
- 业务领域模型定义:
@Data
public class Task {
/**
* 任务id
*/
private Long id;
/**
* 任务名称
*/
private String name;
/**
* 任务状态
*/
private TaskStatusEnum status;
/**
* 业务编号
*/
private String bizNo;
/**
* 业务名称
*/
private String bizName;
/**
* 业务扩展信息
*/
private String bizExtInfo;
}
- 前端响应协议定义
@Data
public class TaskVO {
/**
* 任务id
*/
private Long id;
/**
* 任务名称
*/
private String name;
/**
* 任务状态
*/
private TaskStatusEnum status;
/**
* 业务编号
*/
private String bizNo;
/**
* 业务名称
*/
private String bizName;
/**
* 业务扩展信息
*/
private String bizExtInfo;
}
使用mapstruct模型转换
- 定义转换Mapper接口
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface TaskMapper {
//构建Mapper实例
TaskMapper INSTANTCE = Mappers.getMapper(TaskMapper.class);
/**
* 数据库DO模型转换为领域模型
*
* @param taskDO 数据库DO模型
* @return 任务领域模型
*/
Task toModel(TaskDO taskDO);
/**
* 领域模型转换为前端VO模型
*
* @param task 任务领域模型
* @return 前端VO模型
*/
TaskVO toVO(Task task);
}
- 查看生成源码
在定义完转换接口后,我们使用mvn clean compile编译工程,可以看到MapStruct框架已自动生成源码:
- 调用Mapper
仓储层(Repository)调用
public class TaskRepositoryImpl implements TaskRepository {
@Autowired
private TaskDAO taskDAO;
@Override
public Task queryById(long taskId) {
TaskDO taskDO = taskDAO.queryById(taskId);
//获取单例的Mapper实例转换
return TaskMapper.INSTANTCE.toModel(taskDO);
}
}
- 业务服务层调用
@Service
@Slf4j
public class TaskServiceImpl implements TaskService {
@Autowired
private TaskRepository taskRepository;
@Override
public TaskVO queryByTaskId(long taskId) {
log.info("查询任务详情:{}", taskId);
//从数据库查询任务详情
Task task = taskRepository.queryById(taskId);
//获取单例的Mapper实例转换
return TaskMapper.INSTANTCE.toVO(task);
}
}
可以看到,原有的冗长的getter->setter方式代码已大大简化,对于开发效率的提升效果非常明显。
3.4 Spring集成MapStruct
除了简单的通过Mappers获取实例Mapper方式外,MapStruct也支持将Mapper声明为Spring的bean,实现方式如下:
生成代码如下:
在调用处,使用Spring bean注入方式即可:
3.4 高级用法
以上我们展示了mapstruct的简单使用,即转换的源字段和目标字段名称和类型均相同。
1.源字段和目标字段名称不同
@Mapping(target = "b", source = "A")
Task toModel(TaskDO taskDO);
2.Mapper引入Mapper
针对源字段和目标字段类型不一样的场景,我们可以在Mapper中引入其他Mapper,列如:将字符串类型转换为时间类型(ZonedDateTime,Date等),将字符串类型转换为具体的结构化模型。
编写个性化Mapper
- 字符串->时间类型
@Mapper(componentModel = "spring")
public interface ZonedDateTimeMapper {
/**
* 将日期字符串转成ZonedDateTime
*
* @param dateStr yyyy-mm-dd hh:mm:ss
* @return -
*/
default ZonedDateTime parseStrToZonedDateTime(String dateStr) {
return Optional.ofNullable(dateStr)
.map(str -> ZonedDateTime.parse(dateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
.orElse(null);
}
}
- 字符串->结构化对象
@Mapper(componentModel = "spring")
public interface TaskExtInfoMapper {
/**
* 解析任务业务扩展信息
*
* @param extInfo 数据库业务扩展信息
* @return 业务扩展信息模型
*/
default TaskBizExtInfo pareTaskBizExtInfo(String extInfo) {
return Optional.ofNullable(extInfo).map(info -> JSON.parseObject(info, TaskBizExtInfo.class)).orElse(null);
}
}
源Mapper引入
- 生成代码
3. 自定义方法
@Mapper(componentModel = "spring")
public abstract class TaskMapper {
@Autowired
private ZonedDateTimeMapper zonedDateTimeMapper;
@Named("parseBizExtInfo")
protected TaskBizExtInfo parseBizExtInfo(TaskDO taskDO) {
TaskBizExtInfo taskBizExtInfo = JSON.parseObject(taskDO.getBizExtInfo(), TaskBizExtInfo.class);
ZonedDateTime taskCreateTime = zonedDateTimeMapper.parseStrToZonedDateTime(taskDO.getCreateTime());
taskBizExtInfo.setDurationTime(ChronoUnit.HOURS.between(taskCreateTime, ZonedDateTime.now()));
return taskBizExtInfo;
}
/**
* 数据库DO模型转换为领域模型
*
* @param taskDO 数据库DO模型
* @return 任务领域模型
*/
@Mapping(target = "bizExtInfo", source = "taskDO", qualifiedByName = "parseBizExtInfo")
public abstract Task toModel(TaskDO taskDO);
}
- 生成源码
3.5 idea插件
使用idea插件,我们可以快速发现遗漏的未映射目标字段并进行补齐映射关系。