详解Java中的mapstruct插件使用

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

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

所以阿淼今天就要给大家安利一款叫 mapstruct 的插件,它就是专门用来处理 domin 实体类与 model 类的属性映射的,我们只需定义 mapper 接口,mapstruct 在编译的时候就会自动的帮我们实现这个映射接口,避免了麻烦复杂的映射实现。

那可能有的小伙伴就要问了?为啥不用 BeanUtilscopyProperties 方法呢?不也照样可以实现属性的映射么?

这个啊,阿淼我开始也是好奇,所以就和 BeanUtils 深入交流了一番,最后才发现,BeanUtils 就是一个大老粗,只能同属性映射,或者在属性相同的情况下,允许被映射的对象属性少;但当遇到被映射的属性数据类型被修改或者被映射的字段名被修改,则会导致映射失败。而 mapstruct 就是一个巧媳妇儿了,她心思细腻,把我们可能会遇到的情况都给考虑到了(要是阿淼我也能找一个这样的媳妇儿该多好,内心笑出了猪声)

如下是这个插件的开源项目地址和各种例子:

一、准备工作

接下来,阿淼将和大家一起去解开这个巧媳妇儿的真正面纱,所以我们还需要做一点准备工作。

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

到此这篇关于详解Java中的mapstruct使用的文章就介绍到这了,更多相关Java mapstruct使用内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java实体映射工具MapStruct使用方法详解

    目录 1.序 2.简单用例 3.使用详解 1)关于接口注解@Mapper几种属性用法详解 2) 其他方法级别注解 总结 1.序 通常在后端开发中经常不直接返回实体Entity类,经过处理转换返回前端,前端提交过来的对象也需要经过转换Entity实体才做存储:通常使用的BeanUtils.copyProperties方法也比较粗暴,不仅效率低下(使用反射)而且仅映射相同名的属性,多数情况下还需要手动编写对应的转换方法实现. 插件MapStruct以接口方法结合注解优雅实现对象转换,MapStruc

  • 解析MapStruct转换javaBean时出现的诡异事件

    在项目中用到了MapStruct,对其可以转换JavaBean特别好奇,因为之前都是使用Vo的方式手动set转换,但是接触到MapStruct后感觉以前使用Vo的方式确实有点low 于是就想着去看一下,写了个demo.于是这诡异的时间就开始了 我得代码如下: 这是pom文件 这是我的两个javaBean 大家可以看到那个发灰的序列化接口,那是因为我再运行出结果是转换后的JavaBean属性为空,我以为是序列化时导致的所以我去掉了, 可是后来发现完全不是这个原因 这是我的mapper转换类 还有我

  • Java MapStruct解了对象映射的毒

    前言 MVC模式是目前主流项目的标准开发模式,这种模式下框架的分层结构清晰,主要分为Controller,Service,Dao.分层的结构下,各层之间的数据传输要求就会存在差异,我们不能用一个对象来贯穿3层,这样不符合开发规范且不够灵活. 我们常常会遇到层级之间字段格式需求不一致的情况,例如数据库中某个字段是datetime日期格式,这个时间戳在数据库中的存储值为2020-11-06 23:59:59.999999,但是传递给前端的时候要求接口返回yyyy-MM-dd的格式,或者有些数据在数据

  • Java中的MapStruct用法详解

    目录 1 MapStruct配置 2 原理&性能 2.1 实现原理 3 使用方法 3.1 转换器的检索 3.1.1 使用Mappers工厂获取 3.1.2 通过依赖注入的方式获取 3.2 简单映射 3.2.1 基本映射 3.2.2 多源参数映射 3.2.3 更新对象 3.3 数据类型转换 3.3.1 对于基础数据类型会进行自动隐式的转换 3.3.2 指定转换格式 3.3.3 属性为复杂对象的映射 3.3.4 自定义转换器 3.3.5 使用限定符限定使用转换方法 3.4 Map的映射 3.5 枚举

  • 详解Java中的mapstruct插件使用

    实体类的属性映射怎么可以少了它? 我们都知道,随着一个工程的越来越成熟,模块划分会越来越细,其中实体类一般存于 domain 之中,但 domain 工程最好不要被其他工程依赖,所以其他工程想获取实体类数据时就需要在各自工程写 model,自定义 model 可以根据自身业务需要映射相应的实体属性.这样一来,这个映射工程貌似并不简单了.阿森差点就犯难了…… 序 所以阿淼今天就要给大家安利一款叫 mapstruct 的插件,它就是专门用来处理 domin 实体类与 model 类的属性映射的,我们

  • 详解Java中的OkHttp JSONP爬虫

    目录 什么是JSOUP 什么是OkHttp 爬虫需要掌握的技术 需要的依赖 JSON入门Demo JSOUP常用方法 使用JSOUP 方式连接 User-Agent(随机) 后台爬虫的三大问题 selenium+phantomjs(维护中…内容重新整理) 什么是JSOUP JSOUP 是一款Java 的HTML解析器,可直接解析某个URL地址.HTML文本内容.它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据. 官网 jsoup实现了WHATWG

  • 详解Java中@Override的作用

    详解Java中@Override的作用 @Override是伪代码,表示重写(当然不写也可以),不过写上有如下好处: 1.可以当注释用,方便阅读: 2.编译器可以给你验证@Override下面的方法名是否是你父类中所有的,如果没有则报错.例如,你如果没写@Override,而你下面的方法名又写错了,这时你的编译器是可以编译通过的,因为编译器以为这个方法是你的子类中自己增加的方法. 举例:在重写父类的onCreate时,在方法前面加上@Override 系统可以帮你检查方法的正确性. @Overr

  • 详解Java中多线程异常捕获Runnable的实现

    详解Java中多线程异常捕获Runnable的实现 1.背景: Java 多线程异常不向主线程抛,自己处理,外部捕获不了异常.所以要实现主线程对子线程异常的捕获. 2.工具: 实现Runnable接口的LayerInitTask类,ThreadException类,线程安全的Vector 3.思路: 向LayerInitTask中传入Vector,记录异常情况,外部遍历,判断,抛出异常. 4.代码: package step5.exception; import java.util.Vector

  • 详解java 中Spring jsonp 跨域请求的实例

    详解java 中Spring jsonp 跨域请求的实例 jsonp介绍 JSONP(JSON with Padding)是JSON的一种"使用模式",可用于解决主流浏览器的跨域数据访问的问题.由于同源策略,一般来说位于 server1.example.com 的网页无法与不是 server1.example.com的服务器沟通,而 HTML 的<script> 元素是一个例外.利用 <script> 元素的这个开放策略,网页可以得到从其他来源动态产生的 JSO

  • 详解Java 中的嵌套类与内部类

    详解Java 中的嵌套类与内部类 在Java中,可以在一个类内部定义另一个类,这种类称为嵌套类(nested class).嵌套类有两种类型:静态嵌套类和非静态嵌套类.静态嵌套类较少使用,非静态嵌套类使用较多,也就是常说的内部类.其中内部类又分为三种类型: 1.在外部类中直接定义的内部类. 2.在函数中定义的内部类. 3.匿名内部类. 对于这几种类型的访问规则, 示例程序如下: package lxg; //定义外部类 public class OuterClass { //外部类静态成员变量

  • 详解Java中Collections.sort排序

    Comparator是个接口,可重写compare()及equals()这两个方法,用于比价功能:如果是null的话,就是使用元素的默认顺序,如a,b,c,d,e,f,g,就是a,b,c,d,e,f,g这样,当然数字也是这样的. compare(a,b)方法:根据第一个参数小于.等于或大于第二个参数分别返回负整数.零或正整数. equals(obj)方法:仅当指定的对象也是一个 Comparator,并且强行实施与此 Comparator 相同的排序时才返回 true. Collections.

  • 详解Java中HashSet和TreeSet的区别

    详解Java中HashSet和TreeSet的区别 1. HashSet HashSet有以下特点: 不能保证元素的排列顺序,顺序有可能发生变化 不是同步的 集合元素可以是null,但只能放入一个null 当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置. 简单的说,HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个

  • 详解java中this.getClass()和super.getClass()的实例

    详解java中this.getClass()和super.getClass()的实例 前言: 遇到this.getClass()和super.getClass()的返回值感到疑惑,经过探索豁然开朗. getClass()是java中Object类的一个方法,其原型为: public final Class<?> getClass() 返回值为 当前运行时类的Class对象. 所以写了一段代码来说明: getClass()不受this和super影响,而是有当前的运行类决定的. 代码如下: 父类

  • 详解Java中Checked Exception与Runtime Exception 的区别

    详解Java中Checked Exception与Runtime Exception 的区别 Java里有个很重要的特色是Exception ,也就是说允许程序产生例外状况.而在学Java 的时候,我们也只知道Exception 的写法,却未必真能了解不同种类的Exception 的区别. 首先,您应该知道的是Java 提供了两种Exception 的模式,一种是执行的时候所产生的Exception (Runtime Exception),另外一种则是受控制的Exception (Checked

随机推荐