zoukankan      html  css  js  c++  java
  • 干掉 BeanUtils!试试这款 Bean 自动映射工具,真心强大!!

    开发背景

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

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

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

    为什么不能直接提供 DO?

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

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

    3)根据《阿里 Java 开发手册》分层领域模型规约,不能一个对象走天下,需要定义成 POJO/DO/BO/DTO/VO/Query 等数据对象,完整的定义可以参考阿里开发手册,关注公众号:Java技术栈,在后台回复:手册,可以获取最新高清完整版。

    传统 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 类:

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

    /**
     * 微信公众号:Java技术栈
     * @author 栈长
     */
    @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;
    
    
    }
    
    /**
     * 微信公众号:Java技术栈
     * @author 栈长
     */
    @Data
    public class UserExtDO {
    
        private String regSource;
    
        private String favorite;
    
        private String school;
    
        private int kids;
    
        private String memo;
    
    }
    

    新增一个数据传输 DTO 类:

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

    /**
     * 微信公众号:Java技术栈
     * @author 栈长
     */
    @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 对象?

    Spring Boot 基础这篇就不介绍了,系列基础教程和示例源码可以看这里:https://github.com/javastacks/spring-boot-best-practice

    引入 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 映射:

    /**
     * 微信公众号:Java技术栈
     * @author 栈长
     */
    @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 修改之后:

    /**
     * 微信公众号:Java技术栈
     * @author 栈长
     */
    @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);
    
    }
    

    测试一下:

    /**
     * 微信公众号:Java技术栈
     * @author 栈长
     */
    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" 值。

    /** * 微信公众号:Java技术栈 * @author 栈长 */@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 的单元测试方法。Spring Boot 单元测试不懂的可以关注公众号:Java技术栈,在后台回复:boot,系列教程都整理好了。

    /** * 微信公众号:Java技术栈 * @author 栈长 */@RunWith(SpringRunner.class)@SpringBootTestpublic 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 可以使代码更优雅,还能避免出错,其实还有很多复杂的、个性化用法,一篇难以写完,栈长后面有时间会整理出来,陆续给大家分享。

    感兴趣的也可以参考官方文档:

    https://mapstruct.org/documentation/reference-guide/

    本文实战源代码完整版已经上传:

    https://github.com/javastacks/spring-boot-best-practice

    欢迎 Star 学习,后面 Spring Boot 示例都会在这上面提供!

    好了,今天的分享就到这了,后面我还会陆续解读更多的好玩的 Java 技术,关注公众号Java技术栈第一时间推送。另外,我也将 Spring Boot 系列主流面试题和参考答案都整理好了,关注公众号Java技术栈回复关键字 "面试" 进行刷题。

    最后,觉得我的文章对你用收获的话,动动小手,给个在看、转发,原创不易,栈长需要你的鼓励。

    版权申明:本文系公众号 "Java技术栈" 原创,原创实属不易,转载、引用本文内容请注明出处,禁止抄袭、洗稿,请自重,尊重大家的劳动成果和知识产权,抄袭必究。

    近期热文推荐:

    1.1,000+ 道 Java面试题及答案整理(2021最新版)

    2.别在再满屏的 if/ else 了,试试策略模式,真香!!

    3.卧槽!Java 中的 xx ≠ null 是什么新语法?

    4.Spring Boot 2.5 重磅发布,黑暗模式太炸了!

    5.《Java开发手册(嵩山版)》最新发布,速速下载!

    觉得不错,别忘了随手点赞+转发哦!

  • 相关阅读:
    CSS之旅——第二站 如何更深入的理解各种选择器
    CSS之旅——第一站 为什么要用CSS
    记录一些在用wcf的过程中走过的泥巴路 【第一篇】
    asp.net mvc 之旅—— 第二站 窥探Controller下的各种Result
    asp.net mvc 之旅—— 第一站 从简单的razor入手
    Sql Server之旅——终点站 nolock引发的三级事件的一些思考
    Sql Server之旅——第十四站 深入的探讨锁机制
    Sql Server之旅——第十三站 对锁的初步认识
    Sql Server之旅——第十二站 sqltext的参数化处理
    Sql Server之旅——第十一站 简单说说sqlserver的执行计划
  • 原文地址:https://www.cnblogs.com/javastack/p/15233867.html
Copyright © 2011-2022 走看看