Java模型转换神器MapStruct

769 阅读5分钟

1. 背景

在日常业务开发过程中,我们经常会用到Java pojo(Plain Old Java Object,即Java普通对象)类的转换,列如从:

  • 数据库层模型对象(DO)转换为业务领域模型(model)
  • 业务领域模型转换为VO(前端响应对象)
  • 数据传输对象(DTO)转换为业务领域模型等。

以一次简单的web请求为例,完整的请求过程中模型转换如下:

Web请求过程.png

1.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;
    }
}
  1. 服务层响应前端请求
@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框架已自动生成源码: image.png

  • 调用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,实现方式如下: image.png

生成代码如下: image.png

在调用处,使用Spring bean注入方式即可: image.png

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引入

image.png

  • 生成代码

image.png

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);
}
  • 生成源码

image.png

3.5 idea插件

image.png 使用idea插件,我们可以快速发现遗漏的未映射目标字段并进行补齐映射关系。