实体类的属性映射怎么可以少了它?

开发 前端
我们都知道,随着一个工程的越来越成熟,模块划分会越来越细,其中实体类一般存于 domain 之中,但 domain 工程最好不要被其他工程依赖,所以其他工程想获取实体类数据时就需要在各自工程写 model,自定义 model 可以根据自身业务需要映射相应的实体属性。

 我们都知道,随着一个工程的越来越成熟,模块划分会越来越细,其中实体类一般存于 domain 之中,但 domain 工程最好不要被其他工程依赖,所以其他工程想获取实体类数据时就需要在各自工程写 model,自定义 model 可以根据自身业务需要映射相应的实体属性。这样一来,这个映射工程貌似并不简单了。阿粉差点就犯难了……

[[323180]]

所以阿粉今天就要给大家安利一款叫 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语句的编写,例如:

  1. @Select("select * from user where name = #{name}"
  2. 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的实现。
  1. <dependency> 
  2.         <groupId>org.mapstruct</groupId> 
  3.         <!-- jdk8以下就使用mapstruct --> 
  4.         <artifactId>mapstruct-jdk8</artifactId> 
  5.         <version>1.2.0.Final</version> 
  6.     </dependency> 
  7.     <dependency> 
  8.         <groupId>org.mapstruct</groupId> 
  9.         <artifactId>mapstruct-processor</artifactId> 
  10.         <version>1.2.0.Final</version> 
  11.     </dependency> 

好了,准备工作做完了,接下来我们就看看巧媳妇儿巧在什么地方吧。

 

二、先简单玩一把

2.1、定义实体类以及被映射类

  1. // 实体类 
  2. @Data 
  3. @NoArgsConstructor 
  4. @AllArgsConstructor 
  5. @Builder 
  6. public class User { 
  7.     private Integer id; 
  8.     private String name
  9.     private String createTime; 
  10.     private LocalDateTime updateTime; 
  11.  
  12. // 被映射类VO1:和实体类一模一样 
  13. @Data 
  14. @NoArgsConstructor 
  15. @AllArgsConstructor 
  16. @Builder 
  17. public class UserVO1 { 
  18.     private Integer id; 
  19.     private String name
  20.     private String createTime; 
  21.     private LocalDateTime updateTime; 
  22.  
  23. // 被映射类VO1:比实体类少一个字段 
  24. @Data 
  25. @NoArgsConstructor 
  26. @AllArgsConstructor 
  27. @Builder 
  28. public class UserVO2 { 
  29.     private Integer id; 
  30.     private String name
  31.     private String createTime; 
  32.  

2.2、定义接口:

当实体类和被映射对象属性相同或者被映射对象属性值少几个时:

  1. @Mapper(componentModel = "spring"
  2. public interface UserCovertBasic { 
  3.     UserCovertBasic INSTANCE = Mappers.getMapper(UserCovertBasic.class); 
  4.  
  5.     /** 
  6.      * 字段数量类型数量相同,利用工具BeanUtils也可以实现类似效果 
  7.      * @param source 
  8.      * @return 
  9.      */ 
  10.     UserVO1 toConvertVO1(User source); 
  11.     User fromConvertEntity1(UserVO1 userVO1); 
  12.  
  13.     /** 
  14.      * 字段数量类型相同,数量少:仅能让多的转换成少的,故没有fromConvertEntity2 
  15.      * @param source 
  16.      * @return 
  17.      */ 
  18.     UserVO2 toConvertVO2(User source); 

从上面的代码可以看出:接口中声明了一个成员变量INSTANCE,母的是让客户端可以访问 Mapper 接口的实现。

2.3、使用

  1. @RestController 
  2. public class TestController { 
  3.  
  4.     @GetMapping("convert"
  5.     public Object convertEntity() { 
  6.         User user = User.builder() 
  7.                 .id(1) 
  8.                 .name("张三"
  9.                 .createTime("2020-04-01 11:05:07"
  10.                 .updateTime(LocalDateTime.now()) 
  11.                 .build(); 
  12.         List<Object> objectList = new ArrayList<>(); 
  13.  
  14.         objectList.add(user); 
  15.  
  16.         // 使用mapstruct 
  17.         UserVO1 userVO1 = UserCovertBasic.INSTANCE.toConvertVO1(user); 
  18.         objectList.add("userVO1:" + UserCovertBasic.INSTANCE.toConvertVO1(user)); 
  19.         objectList.add("userVO1转换回实体类user:" + UserCovertBasic.INSTANCE.fromConvertEntity1(userVO1)); 
  20.         // 输出转换结果 
  21.         objectList.add("userVO2:" + " | " + UserCovertBasic.INSTANCE.toConvertVO2(user)); 
  22.         // 使用BeanUtils 
  23.         UserVO2 userVO22 = new UserVO2(); 
  24.         BeanUtils.copyProperties(user, userVO22); 
  25.         objectList.add("userVO22:" + " | " + userVO22); 
  26.  
  27.         return objectList; 
  28.     } 

2.4、查看编译结果

通过IDE的反编译功能查看编译后自动生成 UserCovertBasic 的实现类 UserCovertBasicImpl ,内容如下:

  1. @Component 
  2. public class UserCovertBasicImpl implements UserCovertBasic { 
  3.     public UserCovertBasicImpl() { 
  4.     } 
  5.  
  6.     public UserVO1 toConvertVO1(User source) { 
  7.         if (source == null) { 
  8.             return null
  9.         } else { 
  10.             UserVO1 userVO1 = new UserVO1(); 
  11.             userVO1.setId(source.getId()); 
  12.             userVO1.setName(source.getName()); 
  13.             userVO1.setCreateTime(source.getCreateTime()); 
  14.             userVO1.setUpdateTime(source.getUpdateTime()); 
  15.             return userVO1; 
  16.         } 
  17.     } 
  18.  
  19.     public User fromConvertEntity1(UserVO1 userVO1) { 
  20.         if (userVO1 == null) { 
  21.             return null
  22.         } else { 
  23.             User user = new User(); 
  24.             user.setId(userVO1.getId()); 
  25.             user.setName(userVO1.getName()); 
  26.             user.setCreateTime(userVO1.getCreateTime()); 
  27.             user.setUpdateTime(userVO1.getUpdateTime()); 
  28.             return user
  29.         } 
  30.     } 
  31.  
  32.     public UserVO2 toConvertVO2(User source) { 
  33.         if (source == null) { 
  34.             return null
  35.         } else { 
  36.             UserVO2 userVO2 = new UserVO2(); 
  37.             userVO2.setId(source.getId()); 
  38.             userVO2.setName(source.getName()); 
  39.             userVO2.setCreateTime(source.getCreateTime()); 
  40.             return userVO2; 
  41.         } 
  42.     } 

2.5、浏览器查看结果

 

好了,一个流程就走完了,是不是感觉贼简单呢?

而且呀,阿粉温馨提醒:如果是要转换一个集合的话,只需要把这里的实体类换成集合就行了,例如:

  1. List<UserVO1> toConvertVOList(List<User> source); 

 

三、不简单的情况

上面已经把整个流程都给过了一遍了,相信大家对 mapstruct 也有了一个基础的了解了,所以接下来的情况我们就不展示全部代码了,毕竟篇幅也有限,所以就直接上关键代码(因为不关键的和上面内容一样,哈哈)

3.1、类型不一致

实体类我们还是沿用 User;被映射对象 UserVO3 改为:

  1. @Data 
  2. @NoArgsConstructor 
  3. @AllArgsConstructor 
  4. @Builder 
  5. public class UserVO3 { 
  6.     private String id; 
  7.     private String name
  8.     // 实体类该属性是String 
  9.     private LocalDateTime createTime; 
  10.     // 实体类该属性是LocalDateTime 
  11.     private String updateTime; 

那么我们定义的接口就要稍稍修改一下了:

  1. @Mappings({ 
  2.             @Mapping(target = "createTime", expression = "java(com.java.mmzsblog.util.DateTransform.strToDate(source.getCreateTime()))"), 
  3.     }) 
  4.     UserVO3 toConvertVO3(User source); 
  5.  
  6.     User fromConvertEntity3(UserVO3 userVO3); 

上面 expression 指定的表达式内容如下:

  1. public class DateTransform { 
  2.     public static LocalDateTime strToDate(String str){ 
  3.         DateTimeFormatter df = DateTimeFormatter.ofPattern("yyy-MM-dd HH:mm:ss"); 
  4.         return LocalDateTime.parse("2018-01-12 17:07:05",df); 
  5.     } 
  6.  

通过IDE的反编译功能查看编译后的实现类,结果是这样子的:

 

从图中我们可以看到,编译时使用了expression中定义的表达式对目标字段 createTime 进行了转换;然后你还会发现 updateTime 字段也被自动从 LocalDateTime 类型转换成了 String 类型。

阿粉小结:

当字段类型不一致时,以下的类型之间是 mapstruct 自动进行类型转换的:

1、基本类型及其他们对应的包装类型。此时 mapstruct 会自动进行拆装箱。不需要人为的处理

2、基本类型的包装类型和string类型之间

除此之外的类型转换我们可以通过定义表达式来进行指定转换。

3.2、字段名不一致

实体类我们还是沿用 User;被映射对象 UserVO4 改为:

  1. @Data 
  2. @NoArgsConstructor 
  3. @AllArgsConstructor 
  4. @Builder 
  5. public class UserVO4 { 
  6.     // 实体类该属性名是id 
  7.     private String userId; 
  8.     // 实体类该属性名是name 
  9.     private String userName; 
  10.     private String createTime; 
  11.     private String updateTime; 

那么我们定义的接口就要稍稍修改一下了:

  1. @Mappings({ 
  2.             @Mapping(source = "id", target = "userId"), 
  3.             @Mapping(source = "name", target = "userName"
  4.     }) 
  5.     UserVO4 toConvertVO(User source); 
  6.      
  7.     User fromConvertEntity(UserVO4 userVO4); 

通过IDE的反编译功能查看编译后的实现类,编译后的结果是这样子的:

 

阿粉小结:

当字段名不一致时,通过使用 @Mappings 注解指定对应关系,编译后即可实现对应字段的赋值。

很明显, mapstruct 通过读取我们配置的字段名对应关系,帮我们把它们赋值在了相对应的位置上,可以说是相当优秀了,但这也仅仅是优秀,而更秀的还请继续往下看:

 

3.3、属性是枚举类型

实体类我们还是改用 UserEnum:

  1. @Data 
  2. @NoArgsConstructor 
  3. @AllArgsConstructor 
  4. @Builder 
  5. public class UserEnum { 
  6.     private Integer id; 
  7.     private String name
  8.     private UserTypeEnum userTypeEnum; 

被映射对象 UserVO5 改为:

  1. @Data 
  2. @NoArgsConstructor 
  3. @AllArgsConstructor 
  4. @Builder 
  5. public class UserVO5 { 
  6.     private Integer id; 
  7.     private String name
  8.     private String type; 

枚举对象是:

  1. @Getter 
  2. @AllArgsConstructor 
  3. public enum UserTypeEnum { 
  4.     Java("000""Java开发工程师"), 
  5.     DB("001""数据库管理员"), 
  6.     LINUX("002""Linux运维员"); 
  7.      
  8.     private String value; 
  9.     private String title; 
  10.  

那么我们定义的接口还是照常定义,不会受到它是枚举就有所变化:

  1. @Mapping(source = "userTypeEnum", target = "type"
  2.     UserVO5 toConvertVO5(UserEnum source); 
  3.  
  4.     UserEnum fromConvertEntity5(UserVO5 userVO5); 

通过IDE的反编译功能查看编译后的实现类,编译后的结果是这样子的:

 

很明显, mapstruct 通过枚举类型的内容,帮我们把枚举类型转换成字符串,并给type赋值,可谓是小心使得万年船啊。

看来这巧媳妇儿不仅仅优秀还心细啊……

文章中的所有例子已上传github:https://github.com/mmzsblog/mapstructDemo

 

责任编辑:武晓燕 来源: Java极客技术
相关推荐

2009-09-10 10:09:46

LINQ to SQL

2024-05-16 08:28:20

类型处理器D3BootJSON

2011-06-01 15:45:28

实体类序列化

2023-01-04 08:53:52

JPA实体类注解

2023-01-12 09:13:49

Mybatis数据库

2022-04-18 09:54:37

JDK8日期前端

2020-11-20 08:36:59

Jpa数据代码

2017-07-20 17:05:04

JavaScriptswagger-decSwagger

2011-04-26 15:26:38

PostgreSQL

2011-04-26 14:21:50

MySQL

2024-10-21 07:15:08

2021-06-28 07:09:24

MybatisresultMapJava

2009-09-09 13:07:37

创建Linq数据库

2013-09-08 22:12:02

EF Code Fir数据迁移MVC架构设计

2022-12-27 08:41:51

FastjsonJson字段

2009-09-18 15:22:14

DataContextLINQ to SQL

2023-07-05 08:28:35

Core 模块SqlSugar

2009-12-30 15:06:22

ADO.NET分析

2017-11-02 13:35:39

2009-09-24 10:07:21

Hibernate M
点赞
收藏

51CTO技术栈公众号