mapstruct使用详解
我们都知道,随着一个工程的越来越成熟,模块划分会越来越细,其中实体类一般存于 domain 之中,但 domain 工程最好不要被其他工程依赖,所以其他工程想获取实体类数据时就需要在各自工程写 model,自定义 model 可以根据自身业务需要映射相应的实体属性。这样一来,这个映射工程貌似并不简单了。阿森差点就犯难了……
序
所以阿淼今天就要给大家安利一款叫 mapstruct
的插件,它就是专门用来处理 domin 实体类与 model 类的属性映射的,我们只需定义 mapper 接口,mapstruct 在编译的时候就会自动的帮我们实现这个映射接口,避免了麻烦复杂的映射实现。
那可能有的小伙伴就要问了?为啥不用 BeanUtils
的 copyProperties
方法呢?不也照样可以实现属性的映射么?
这个啊,阿淼我开始也是好奇,所以就和 BeanUtils
深入交流了一番,最后才发现,BeanUtils
就是一个大老粗,只能同属性映射,或者在属性相同的情况下,允许被映射的对象属性少;但当遇到被映射的属性数据类型被修改或者被映射的字段名被修改,则会导致映射失败。而 mapstruct
就是一个巧媳妇儿了,她心思细腻,把我们可能会遇到的情况都给考虑到了(要是阿淼我也能找一个这样的媳妇儿该多好,内心笑出了猪声)
如下是这个插件的开源项目地址和各种例子:
- Github地址:https://github.com/mapstruct/mapstruct/
- 使用例子:https://github.com/mapstruct/mapstruct-examples
一、准备工作
接下来,阿淼将和大家一起去解开这个巧媳妇儿的真正面纱,所以我们还需要做一点准备工作。
1.1、了解@Mapper
注解
从 mybatis3.4.0 开始加入的 @Mapper 注解,目的就是为了不再写mapper映射文件。
我们只需要在 dao 层定义的接口上使用注解就可以实现sql语句的编写,例如:
@Select("select * from user where name = #{name}")
public User find(String name);
如上就是一个简单的使用,虽然简单,但也确实体现出了这个注解的优越性,至少少写了一个xml文件。
但阿淼我今天可不是想跟你探讨 @Mapper
注解,我主要是想去看我的巧媳妇儿 mapstruct
,所以我就只是想说下 @Mapper
注解的 componentModel
属性,componentModel
属性用于指定自动生成的接口实现类的组件类型,这个属性支持四个值:
- default: 这是默认的情况,mapstruct 不使用任何组件类型, 可以通过Mappers.getMapper(Class)方式获取自动生成的实例对象。
- cdi: the generated mapper is an application-scoped CDI bean and can be retrieved via @Inject
- spring: 生成的实现类上面会自动添加一个@Component注解,可以通过Spring的 @Autowired方式进行注入
- jsr330: 生成的实现类上会添加@javax.inject.Named 和@Singleton注解,可以通过 @Inject注解获取
1.2、依赖包
首先需要把依赖包导入,主要由两个包组成:
org.mapstruct:mapstruct
:包含了一些必要的注解,例如@Mapping。r若我们使用的JDK版本高于1.8,当我们在pom里面导入依赖时候,建议使用坐标是:org.mapstruct:mapstruct-jdk8
,这可以帮助我们利用一些Java8的新特性。org.mapstruct:mapstruct-processor
:注解处理器,根据注解自动生成mapper的实现。
<dependency>
<groupId>org.mapstruct</groupId>
<!-- jdk8以下就使用mapstruct -->
<artifactId>mapstruct-jdk8</artifactId>
<version>1.2.0.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.2.0.Final</version>
</dependency>
好了,准备工作做完了,接下来我们就看看巧媳妇儿巧在什么地方吧。
二、先简单玩一把
2.1、定义实体类以及被映射类
// 实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
private Integer id;
private String name;
private String createTime;
private LocalDateTime updateTime;
}
// 被映射类VO1:和实体类一模一样
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserVO1 {
private Integer id;
private String name;
private String createTime;
private LocalDateTime updateTime;
}
// 被映射类VO1:比实体类少一个字段
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserVO2 {
private Integer id;
private String name;
private String createTime;
}
2.2、定义接口:
当实体类和被映射对象属性相同或者被映射对象属性值少几个时:
@Mapper(componentModel = "spring")
public interface UserCovertBasic {
UserCovertBasic INSTANCE = Mappers.getMapper(UserCovertBasic.class);
/**
* 字段数量类型数量相同,利用工具BeanUtils也可以实现类似效果
* @param source
* @return
*/
UserVO1 toConvertVO1(User source);
User fromConvertEntity1(UserVO1 userVO1);
/**
* 字段数量类型相同,数量少:仅能让多的转换成少的,故没有fromConvertEntity2
* @param source
* @return
*/
UserVO2 toConvertVO2(User source);
}
从上面的代码可以看出:接口中声明了一个成员变量INSTANCE,母的是让客户端可以访问 Mapper 接口的实现。
2.3、使用
@RestController
public class TestController {
@GetMapping("convert")
public Object convertEntity() {
User user = User.builder()
.id(1)
.name("张三")
.createTime("2020-04-01 11:05:07")
.updateTime(LocalDateTime.now())
.build();
List<Object> objectList = new ArrayList<>();
objectList.add(user);
// 使用mapstruct
UserVO1 userVO1 = UserCovertBasic.INSTANCE.toConvertVO1(user);
objectList.add("userVO1:" + UserCovertBasic.INSTANCE.toConvertVO1(user));
objectList.add("userVO1转换回实体类user:" + UserCovertBasic.INSTANCE.fromConvertEntity1(userVO1));
// 输出转换结果
objectList.add("userVO2:" + " | " + UserCovertBasic.INSTANCE.toConvertVO2(user));
// 使用BeanUtils
UserVO2 userVO22 = new UserVO2();
BeanUtils.copyProperties(user, userVO22);
objectList.add("userVO22:" + " | " + userVO22);
return objectList;
}
}
2.4、查看编译结果
通过IDE的反编译功能查看编译后自动生成 UserCovertBasic
的实现类 UserCovertBasicImpl
,内容如下:
@Component
public class UserCovertBasicImpl implements UserCovertBasic {
public UserCovertBasicImpl() {
}
public UserVO1 toConvertVO1(User source) {
if (source == null) {
return null;
} else {
UserVO1 userVO1 = new UserVO1();
userVO1.setId(source.getId());
userVO1.setName(source.getName());
userVO1.setCreateTime(source.getCreateTime());
userVO1.setUpdateTime(source.getUpdateTime());
return userVO1;
}
}
public User fromConvertEntity1(UserVO1 userVO1) {
if (userVO1 == null) {
return null;
} else {
User user = new User();
user.setId(userVO1.getId());
user.setName(userVO1.getName());
user.setCreateTime(userVO1.getCreateTime());
user.setUpdateTime(userVO1.getUpdateTime());
return user;
}
}
public UserVO2 toConvertVO2(User source) {
if (source == null) {
return null;
} else {
UserVO2 userVO2 = new UserVO2();
userVO2.setId(source.getId());
userVO2.setName(source.getName());
userVO2.setCreateTime(source.getCreateTime());
return userVO2;
}
}
}
2.5、浏览器查看结果
好了,一个流程就走完了,是不是感觉贼简单呢?
而且呀,阿淼温馨提醒:
如果是要转换一个集合的话,只需要把这里的实体类换成集合就行了,例如:
List<UserVO1> toConvertVOList(List<User> source);
三、不简单的情况
上面已经把整个流程都给过了一遍了,相信大家对 mapstruct
也有了一个基础的了解了,所以接下来的情况我们就不展示全部代码了,毕竟篇幅也有限,所以就直接上关键代码(因为不关键的和上面内容一样,哈哈)
3.1、类型不一致
实体类我们还是沿用 User
;被映射对象 UserVO3
改为:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserVO3 {
private String id;
private String name;
// 实体类该属性是String
private LocalDateTime createTime;
// 实体类该属性是LocalDateTime
private String updateTime;
}
那么我们定义的接口就要稍稍修改一下了:
@Mappings({
@Mapping(target = "createTime", expression = "java(com.java.mmzsblog.util.DateTransform.strToDate(source.getCreateTime()))"),
})
UserVO3 toConvertVO3(User source);
User fromConvertEntity3(UserVO3 userVO3);
上面 expression
指定的表达式内容如下:
public class DateTransform {
public static LocalDateTime strToDate(String str){
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyy-MM-dd HH:mm:ss");
return LocalDateTime.parse("2018-01-12 17:07:05",df);
}
}
通过IDE的反编译功能查看编译后的实现类,结果是这样子的:
从图中我们可以看到,编译时使用了expression中定义的表达式对目标字段 createTime
进行了转换;然后你还会发现 updateTime
字段也被自动从 LocalDateTime 类型转换成了 String 类型。
阿淼小结:
当字段类型不一致时,以下的类型之间是 mapstruct
自动进行类型转换的:
- 1、基本类型及其他们对应的包装类型。
此时mapstruct
会自动进行拆装箱。不需要人为的处理 - 2、基本类型的包装类型和string类型之间
除此之外的类型转换我们可以通过定义表达式来进行指定转换。
3.2、字段名不一致
实体类我们还是沿用 User
;被映射对象 UserVO4
改为:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserVO4 {
// 实体类该属性名是id
private String userId;
// 实体类该属性名是name
private String userName;
private String createTime;
private String updateTime;
}
那么我们定义的接口就要稍稍修改一下了:
@Mappings({
@Mapping(source = "id", target = "userId"),
@Mapping(source = "name", target = "userName")
})
UserVO4 toConvertVO(User source);
User fromConvertEntity(UserVO4 userVO4);
通过IDE的反编译功能查看编译后的实现类,编译后的结果是这样子的:
很明显, mapstruct
通过读取我们配置的字段名对应关系,帮我们把它们赋值在了相对应的位置上,可以说是相当优秀了,但这也仅仅是优秀,而更秀的还请继续往下看:
阿淼小结:
当字段名不一致时,通过使用 @Mappings
注解指定对应关系,编译后即可实现对应字段的赋值。
3.3、属性是枚举类型
实体类我们还是改用 UserEnum
:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserEnum {
private Integer id;
private String name;
private UserTypeEnum userTypeEnum;
}
被映射对象 UserVO5
改为:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserVO5 {
private Integer id;
private String name;
private String type;
}
枚举对象是:
@Getter
@AllArgsConstructor
public enum UserTypeEnum {
Java("000", "Java开发工程师"),
DB("001", "数据库管理员"),
LINUX("002", "Linux运维员");
private String value;
private String title;
}
那么我们定义的接口还是照常定义,不会受到它是枚举就有所变化:
@Mapping(source = "userTypeEnum", target = "type")
UserVO5 toConvertVO5(UserEnum source);
UserEnum fromConvertEntity5(UserVO5 userVO5);
通过IDE的反编译功能查看编译后的实现类,编译后的结果是这样子的:
很明显, mapstruct
通过枚举类型的内容,帮我们把枚举类型转换成字符串,并给type赋值,可谓是小心使得万年船啊。看来这巧媳妇儿不仅仅优秀还心细啊……
源代码
文章中的所有例子已上传github:https://github.com/mmzsblog/mapstructDemo
mapstruct使用详解的更多相关文章
- Java实体映射工具MapStruct使用详解
1.序 通常在后端开发中经常不直接返回实体Entity类,经过处理转换返回前端,前端提交过来的对象也需要经过转换Entity实体才做存储:通常使用的BeanUtils.copyProperties方法 ...
- 常用开发库 - 告別BeanUtils拷贝,MapStruct工具库最全详解
常用开发库 - MapStruct工具库详解 MapStruct是一款非常实用Java工具,主要用于解决对象之间的拷贝问题,比如PO/DTO/VO/QueryParam之间的转换问题.区别于BeanU ...
- 常用开发库 - MapStruct工具库详解
常用开发库 - MapStruct工具库详解 MapStruct是一款非常实用Java工具,主要用于解决对象之间的拷贝问题,比如PO/DTO/VO/QueryParam之间的转换问题.区别于BeanU ...
- Linq之旅:Linq入门详解(Linq to Objects)
示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集 ...
- 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)
一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...
- EntityFramework Core 1.1 Add、Attach、Update、Remove方法如何高效使用详解
前言 我比较喜欢安静,大概和我喜欢研究和琢磨技术原因相关吧,刚好到了元旦节,这几天可以好好学习下EF Core,同时在项目当中用到EF Core,借此机会给予比较深入的理解,这里我们只讲解和EF 6. ...
- Java 字符串格式化详解
Java 字符串格式化详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 文中如有纰漏,欢迎大家留言指出. 在 Java 的 String 类中,可以使用 format() 方法 ...
- Android Notification 详解(一)——基本操作
Android Notification 详解(一)--基本操作 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Notification 文中如有纰 ...
- Android Notification 详解——基本操作
Android Notification 详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 前几天项目中有用到 Android 通知相关的内容,索性把 Android Notificatio ...
随机推荐
- Range Module
2019-09-21 18:54:16 715. Range Module 问题描述: 问题求解: 用线段树解决了. class RangeModule { Node root; class Node ...
- 使用SparkSQL编写wordCount的词频统计
# 使用SparkSQL编写wordCount的词频统计 ## word.txt```hello hello scala sparkjava sql html java hellojack jack ...
- MRCTF 部分WriteUp
前言 周末做了一下北邮的CTF,这里记录一下做出来的几道题.(PS:比较菜有很多没做出来 >_< ,还是要更加努力学习啊(ง •̀o•́)ง,剩下的等大佬们出了wp后在复现一下) Web ...
- 编译原理-第三章 词法分析-3.7 从正则表达式到自动机-DFA最简化
DFA最简化 一.构造最简DFA 1.输入输出 2.步骤 3.注意点 4.代码 二.示例 例1: 例2: 参考--慕课-苏州大学
- 一文带你入门Java Stream流,太强了
两个星期以前,就有读者强烈要求我写一篇 Java Stream 流的文章,我说市面上不是已经有很多了吗,结果你猜他怎么说:"就想看你写的啊!"你看你看,多么苍白的喜欢啊.那就&qu ...
- 主从校验工具pt-table-checksum和pt-table-sync工作原理
pt-table-checksum和pt-table-sync是常用来做MySQL主从数据一致性校验的工具,pt-table-checksum只校验数据,不能对数据进行同步:pt-table-sync ...
- 一个完整的机器学习项目在Python中的演练(二)
大家往往会选择一本数据科学相关书籍或者完成一门在线课程来学习和掌握机器学习.但是,实际情况往往是,学完之后反而并不清楚这些技术怎样才能被用在实际的项目流程中.就像你的脑海中已经有了一块块"拼 ...
- adb的多种连接方式(二)
一,设备连接 1,USB数据线连接 win10下USB连接Android 1.手机端的设置,以红米4为例: a.打开开发者模式,小米手机打开开发者模式方法为,连续点击MIUI版本,就可以进入开发者模式 ...
- iOS 构建动态库
一.构建步骤 创建一个动态库 MyDynamicFramework 创建一个测试类 在 MyDynamicFramework.h(默认生成,可统一暴露头文件) 中 #import "Pers ...
- 1127: 【入门】A类多?B类多?
1127: [入门]A类多?B类多? 时间限制: 1 Sec 内存限制: 16 MB 提交: 3537 解决: 2406 [提交] [状态] [讨论版] [命题人:外部导入] 题目描述 一个自然数转换 ...