⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 Java技术栈 「栈长」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

开发背景

你有没有遇到过这样的开发场景?

服务通过接口对外提供数据,或者服务之间进行数据交互,首先查询数据库并映射成数据对象(XxxDO)。

正常情况下,接口是不允许直接以数据库数据对象 XxxDO 形式对外提供数据的,而是要再封装成数据传输对象(XxxDTO)提供出去。

为什么不能直接提供 DO?

1)根据单一设计原则,DO 只能对应数据实体对象,不能承担其他职责;

2)DO 可能包含表所有字段数据,不符合接口的参数定义,数据如果过大会影响传输速度,也不符合数据安全原则;

3)根据《阿里 Java 开发手册》分层领域模型规约,不能一个对象走天下,需要定义成 POJO/DO/BO/DTO/VO/Query 等数据对象,完整的定义可以参考阿里开发手册。

传统 DO -> DTO 做法

XxxDTO 可能包含 XxxDO 大部分数据,或者组合其他 DO 的部分数据,传统的做法有以下几种:

  • get/ set
  • 构造器
  • BeanUtils 工具类
  • Builder 模式

我相信大部分人的做法都是这样的,虽然很直接,但是普遍真的很 Low,耦合性又强,还经常丢参数,或者搞错参数值,在这个开发场景,我个人觉得这些都不是最佳的方式。

这种开发场景又实在是太常见了,那有没有一种 Java bean 自动映射工具?

没错——正是 MapStruct!!

MapStruct 简介

官网地址:

https://mapstruct.org/

开源地址:

https://github.com/mapstruct/mapstruct

Java bean mappings, the easy way!

以简单的方式进行 Java bean 映射。

MapStruct 是一个代码生成器,它和 Spring Boot、Maven 一样也是基于约定优于配置的理念,极大地简化了 Java bean 之间数据映射的实现。

MapStruct 的优势:

1、MapStruct 使用简单的方法调用生成映射代码,因此速度非常快

2、类型安全,避免出错,只能映射相互映射的对象和属性,因此不会错误将用户实体错误地映射到订单 DTO;

3、只需要 JDK 1.8+,不用其他任何依赖,自包含所有代码

4、易于调试

5、易于理解

支持的方式:

MapStruct 支持命令行编译,如:纯 javac 命令、Maven、Gradle、Ant 等等,也支持 Eclipse、IntelliJ IDEA 等 IDEs。

MapStruct 实战

本文栈长基于 IntelliJ IDEA、Spring Boot、Maven 进行演示。

基本准备

新增两个数据库 DO 类:

一个用户主类,一个用户扩展类。

@Data
public class UserDO {

private String name;

private int sex;

private int age;

private Date birthday;

private String phone;

private boolean married;

private Date regDate;

private Date loginDate;

private String memo;

private UserExtDO userExtDO;


}

@Data
public class UserExtDO {

private String regSource;

private String favorite;

private String school;

private int kids;

private String memo;

}

新增一个数据传输 DTO 类:

用户展示类,包含用户主类、用户扩展类的部分数据。

@Data
public class UserShowDTO {

private String name;

private int sex;

private boolean married;

private String birthday;

private String regDate;

private String registerSource;

private String favorite;

private String memo;

}

开始实战

重点来了,不要 get/set,不要 BeanUtils,怎么把两个用户对象的数据封装到 DTO 对象?

引入 MapStruct 依赖:

<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>

Maven 插件相关配置:

MapStruct 和 Lombok 结合使用会有版本冲突问题,注意以下配置。

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<!-- 使用 Lombok 需要添加 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
<!-- Lombok 1.18.16 及以上需要添加,不然报错 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>

添加 MapStruct 映射:

@Mapper
public interface UserStruct {

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

@Mappings({
@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
@Mapping(target = "regDate", expression = "java(org.apache.commons.lang3.time.DateFormatUtils.format(userDO.getRegDate(),\"yyyy-MM-dd HH:mm:ss\"))")
@Mapping(source = "userExtDO.regSource", target = "registerSource")
@Mapping(source = "userExtDO.favorite", target = "favorite")
@Mapping(target = "memo", ignore = true)
})
UserShowDTO toUserShowDTO(UserDO userDO);

List<UserShowDTO> toUserShowDTOs(List<UserDO> userDOs);

}

重点说明:

1)添加一个 interface 接口,使用 MapStruct 的 @Mapper 注解修饰,这里取名 XxxStruct,是为了不和 MyBatis 的 Mapper 混淆;

2)使用 Mappers 添加一个 INSTANCE 实例,也可以使用 Spring 注入,后面会讲到;

3)添加两个映射方法,返回单个对象、对象列表;

4)使用 @Mappings + @Mapping 组合映射,如果两个字段名相同可以不用写,可以指定映射的日期格式、数字格式、表达式等,ignore 表示忽略该字段映射;

5)List 方法的映射会调用单个方法映射,不用单独映射,后面看源码就知道了;

另外,Java 8+ 以上版本不需要 @Mappings 注解,直接使用 @Mapping 注解就行了:

Java 8 修改之后:

@Mapper
public interface UserStruct {

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

@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
@Mapping(target = "regDate", expression = "java(org.apache.commons.lang3.time.DateFormatUtils.format(userDO.getRegDate(),\"yyyy-MM-dd HH:mm:ss\"))")
@Mapping(source = "userExtDO.regSource", target = "registerSource")
@Mapping(source = "userExtDO.favorite", target = "favorite")
@Mapping(target = "memo", ignore = true)
UserShowDTO toUserShowDTO(UserDO userDO);

List<UserShowDTO> toUserShowDTOs(List<UserDO> userDOs);

}

测试一下:

public class UserStructTest {

@Test
public void test1() {
UserExtDO userExtDO = new UserExtDO();
userExtDO.setRegSource("Java技术栈");
userExtDO.setFavorite("写代码");
userExtDO.setSchool("社会大学");

UserDO userDO = new UserDO();
userDO.setName("栈长");
userDO.setSex(1);
userDO.setAge(18);
userDO.setBirthday(new Date());
userDO.setPhone("18888888888");
userDO.setMarried(true);
userDO.setRegDate(new Date());
userDO.setMemo("666");
userDO.setUserExtDO(userExtDO);

UserShowDTO userShowDTO = UserStruct.INSTANCE.toUserShowDTO(userDO);
System.out.println("=====单个对象映射=====");
System.out.println(userShowDTO);

List<UserDO> userDOs = new ArrayList<>();
UserDO userDO2 = new UserDO();
BeanUtils.copyProperties(userDO, userDO2);
userDO2.setName("栈长2");
userDOs.add(userDO);
userDOs.add(userDO2);
List<UserShowDTO> userShowDTOs = UserStruct.INSTANCE.toUserShowDTOs(userDOs);
System.out.println("=====对象列表映射=====");
userShowDTOs.forEach(System.out::println);
}
}

输出结果:

来看结果,数据转换结果成功。

什么原理?

如上我们知道,通过一个注解修饰接口就可以搞定了,是什么原理呢?

来看编译后的目录:

原理就是在编译期间生成了一个该接口的实现类。

打开看下其源码:

public class UserStructImpl implements UserStruct {
public UserStructImpl() {
}

public UserShowDTO toUserShowDTO(UserDO userDO) {
if (userDO == null) {
return null;
} else {
UserShowDTO userShowDTO = new UserShowDTO();
if (userDO.getBirthday() != null) {
userShowDTO.setBirthday((new SimpleDateFormat("yyyy-MM-dd")).format(userDO.getBirthday()));
}

userShowDTO.setRegisterSource(this.userDOUserExtDORegSource(userDO));
userShowDTO.setFavorite(this.userDOUserExtDOFavorite(userDO));
userShowDTO.setName(userDO.getName());
userShowDTO.setSex(userDO.getSex());
userShowDTO.setMarried(userDO.isMarried());
userShowDTO.setRegDate(DateFormatUtils.format(userDO.getRegDate(), "yyyy-MM-dd HH:mm:ss"));
return userShowDTO;
}
}

public List<UserShowDTO> toUserShowDTOs(List<UserDO> userDOs) {
if (userDOs == null) {
return null;
} else {
List<UserShowDTO> list = new ArrayList(userDOs.size());
Iterator var3 = userDOs.iterator();

while(var3.hasNext()) {
UserDO userDO = (UserDO)var3.next();
list.add(this.toUserShowDTO(userDO));
}

return list;
}
}

private String userDOUserExtDORegSource(UserDO userDO) {
if (userDO == null) {
return null;
} else {
UserExtDO userExtDO = userDO.getUserExtDO();
if (userExtDO == null) {
return null;
} else {
String regSource = userExtDO.getRegSource();
return regSource == null ? null : regSource;
}
}
}

private String userDOUserExtDOFavorite(UserDO userDO) {
if (userDO == null) {
return null;
} else {
UserExtDO userExtDO = userDO.getUserExtDO();
if (userExtDO == null) {
return null;
} else {
String favorite = userExtDO.getFavorite();
return favorite == null ? null : favorite;
}
}
}
}

其实实现类就是调用了对象的 get/set 等其他常规操作,而 List 就是循环调用的该对象的单个映射方法,这下就清楚了吧!

Spring 注入法

上面的示例创建了一个 UserStruct 实例:

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

如 @Mapper 注解源码所示:

参数 componentModel 默认值是 default,也就是手动创建实例,也可以通过 Spring 注入。

Spring 修改版如下:

干掉了 INSTANCE,@Mapper 注解加入了 componentModel = "spring" 值。

@Mapper(componentModel = "spring")
public interface UserSpringStruct {

@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
@Mapping(target = "regDate", expression = "java(org.apache.commons.lang3.time.DateFormatUtils.format(userDO.getRegDate(),\"yyyy-MM-dd HH:mm:ss\"))")
@Mapping(source = "userExtDO.regSource", target = "registerSource")
@Mapping(source = "userExtDO.favorite", target = "favorite")
@Mapping(target = "memo", ignore = true)
UserShowDTO toUserShowDTO(UserDO userDO);

List<UserShowDTO> toUserShowDTOs(List<UserDO> userDOS);

}

测试一下:

本文用到了 Spring Boot,所以这里就要用到 Spring Boot 的单元测试方法。

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserSpringStructTest {

@Autowired
private UserSpringStruct userSpringStruct;

@Test
public void test1() {
UserExtDO userExtDO = new UserExtDO();
userExtDO.setRegSource("Java技术栈");
userExtDO.setFavorite("写代码");
userExtDO.setSchool("社会大学");

UserDO userDO = new UserDO();
userDO.setName("栈长Spring");
userDO.setSex(1);
userDO.setAge(18);
userDO.setBirthday(new Date());
userDO.setPhone("18888888888");
userDO.setMarried(true);
userDO.setRegDate(new Date());
userDO.setMemo("666");
userDO.setUserExtDO(userExtDO);

UserShowDTO userShowDTO = userSpringStruct.toUserShowDTO(userDO);
System.out.println("=====单个对象映射=====");
System.out.println(userShowDTO);

List<UserDO> userDOs = new ArrayList<>();
UserDO userDO2 = new UserDO();
BeanUtils.copyProperties(userDO, userDO2);
userDO2.setName("栈长Spring2");
userDOs.add(userDO);
userDOs.add(userDO2);
List<UserShowDTO> userShowDTOs = userSpringStruct.toUserShowDTOs(userDOs);
System.out.println("=====对象列表映射=====");
userShowDTOs.forEach(System.out::println);
}
}

如上所示,直接使用 @Autowired 注入就行,使用更方便。

输出结果:

没毛病,稳如狗。

总结

本文栈长只是介绍了 MapStruct 的简单用法,使用 MapStruct 可以使代码更优雅,还能避免出错,其实还有很多复杂的、个性化用法,一篇难以写完,栈长后面有时间会整理出来,陆续给大家分享。

文章目录
  1. 1. 开发背景
    1. 1.0.1. 你有没有遇到过这样的开发场景?
    2. 1.0.2. 为什么不能直接提供 DO?
    3. 1.0.3. 传统 DO -> DTO 做法
  • 2. MapStruct 简介
  • 3. MapStruct 实战
    1. 3.0.1. 基本准备
    2. 3.0.2. 开始实战
  • 4. 什么原理?
  • 5. Spring 注入法
  • 6. 总结