开发背景你遇到过这样的开发场景吗?服务通过接口提供数据,或者说服务之间的数据交互,首先查询数据库,映射成一个数据对象(XxxDO)。一般情况下,接口不允许直接以数据库数据对象XxxDO的形式提供数据,必须重新打包成数据传输对象(XxxDTO)提供。为什么不能直接提供DO?1)根据单一设计原则,DO只能对应数据实体对象,不能承担其他职责;2)DO可能包含表中的所有字段数据,不符合接口的参数定义。数据过大会影响传输速度,不符合数据安全原则;3)根据《阿里 Java 开发手册》分层领域模型规范,一个对象不能走遍全球,需要定义为POJO/DO/BO/DTO/VO/Query等数据对象,完整定义可参考到阿里开发手册,关注公众号:Java技术栈,后台回复:手册,即可获取最新高清完整版。传统的DO->DTO方式XxxDTO可能包含了XxxDO的大部分数据,或者结合了其他DO的部分数据。传统的方法如下:get/setconstructorBeanUtilstoolclassBuildermode相信大部分人都是这样做的,虽然很直接,但是一般真的很low,耦合性强,经常丢参数,或者参数价值观是错误的。在这种开发场景下,我个人认为这些都不是最好的方法。这种开发场景实在是太常见了。有Javabean自动映射工具吗?没错——就是MapStruct!!MapStruct简介官网地址:https://mapstruct.org/开源地址:https://github.com/mapstruct/mapstructJavabeanmappings,theeasyway!Javabean映射的简单方法。MapStruct是一个代码生成器。与SpringBoot和Maven一样,它也是基于约定优于配置的理念,大大简化了Javabean之间数据映射的实现。MapStruct的优点:1.MapStruct使用简单的方法调用来生成映射代码,所以速度非常快***;2.类型安全,避免错误,只映射相互映射的对象和属性,所以不会出现错误用户实体被错误映射到订单DTO;3、只需要JDK1.8+,没有任何其他依赖,所有代码都是自包含的;4.调试方便;5、通俗易懂;支持方式:MapStruct支持命令行编译,如:纯javac命令、Maven、Gradle、Ant等,也支持Eclipse、IntelliJIDEA等IDE。MapStruct实战本文栈长演示基于IntelliJIDEA、SpringBoot和Maven。基本上准备添加两个数据库DO类:一个用户主类和一个用户扩展类。/***微信公众号:Java技术栈*@作者栈长*/@DatapublicclassUserDO{privateStringname;:Java技术栈*@authorstacklength*/@DatapublicclassUserExtDO{privateStringregSource;privateStringfavorite;privateStringschool;privateintkids;privateStringmemo;}新增一个数据传输DTO类:用户展示类,包括用户主类和用户的部分数据扩展类。/***微信公众号:Java技术栈*@作者栈长*/@DatapublicclassUserShowDTO{privateStringname;没有BeanUtils,如何将两个用户对象的数据封装成DTO对象呢?本文不会介绍SpringBoot的基础知识。一系列基础教程和示例源码可以在这里找到:https://github.com/javastacks/spring-boot-best-practice引入MapStruct依赖:org.mapstructmapstruct${org.mapstruct.version}Maven插件相关配置:MapStruct和Lombok一起使用会有版本冲突,注意以下配置。org.apache.maven.pluginsmaven-compiler-plugin3.8.11.81.8org.mapstructmapstruct处理器${org.mapstruct.version}org.projectlomboklombok${org.projectlombok.version}org.projectlomboklombok-mapstruct-binding<版本>${lombok-mapstruct-binding.version}</configuration>添加MapStruct映射:/***微信公众号:Java技术栈*@author栈长*/@MapperpublicinterfaceUserStruct{UserStructINSTANCE=Mappers.getMapper(UserStruct.class);@Mappings({@Mapping(source="生日",target="生日",dateFormat="yyyy-MM-dd")@Mapping(target="regDate",expression="java(org.apache.commons.lang3.time.DateFormatUtils.format(userDO.getRegDate(),\"yyyy-MM-ddHH:mm:ss\"))")@Mapping(source="userExtDO.regSource",target="registerSource")@Mapping(source="userExtDO.favorite",target="favorite")@Mapping(target="memo",ignore=true)})UserShowDTOtoUserShowDTO(UserDOuserDO);ListtoUserShowDTOs(ListuserDOs);}要点:1)增加一个interface接口,使用MapStruct的@Mapper注解进行修改。这里的名字是XxxStruct,避免和MyBatis的Mapper混淆;2)使用Mappers添加一个INSTANCE实例,也可以使用Spring进行注入。会谈;3)添加两个映射方法,返回单个对象,对象列表;4)使用@Mappings+@Mapping组合映射。如果两个字段名相同,则不用写。可以指定映射的日期格式、数字格式、表达式等。ignore表示忽略字段映射;5)List方法的映射会调用Single方法映射,没有单独映射,后面看源码就好了;另外,Java8+及以上版本不需要@Mappings注解,直接使用@Mapping注解即可:Java8修改后:/***微信公众号:Java技术栈*@作者栈长*/@MapperpublicinterfaceUserStruct{UserStructINSTANCE=Mappers.getMapper(UserStruct.class);@Mapping(source="生日",target="生日",dateFormat="yyyy-MM-dd")@Mapping(target="regDate",expression="java(org.apache.commons.lang3.time.DateFormatUtils.format(userDO.getRegDate(),\"yyyy-MM-ddHH:mm:ss\"))")@Mapping(source="userExtDO.regSource",target="registerSource")@Mapping(source="userExtDO.favorite",target="favorite")@Mapping(target="memo",ignore=true)UserShowDTOtoUserShowDTO(UserDOuserDO);ListtoUserShowDTOs(ListuserDOs);}测试一下:/***微信公众号:Java技术栈*@作者栈长*/publicclassUserStructTest{@Testpublicvoidtest1(){UserExtDOuserExtDO=newUserExtDO();userExtDO.setRegSource("公众号:Java技术栈");userExtDO.setFavorite("写代码");userExtDO.setSchool("社会大学");UserDOuserDO=newUserDO();userDO.setName("堆栈长度");userDO.setSex(1);userDO.setAge(18);userDO.setBirthday(newDate());userDO.setPhone("18888888888");userDO.setMarried(true);userDO.setRegDate(newDate());userDO.setMemo("666");userDO.setUserExtDO(userExtDO);UserShowDTOuserShowDTO=UserStruct.INSTANCE.toUserShowDTO(userDO);System.out.println("=====单个对象映射=====");System.out.println(userShowDTO);ListuserDOs=newArrayList<>();UserDOuserDO2=newUserDO();BeanUtils.copyProperties(userDO,userDO2);userDO2.setName("堆叠长度2");userDOs.add(userDO);userDOs.add(userDO2);ListuserShowDTOs=UserStruct.INSTANCE.toUserShowDTOs(userDOs);System.out.println("=====对象列表映射=====");userShowDTOs.forEach(System.out::println);}}输出结果:见那么,数据转换结果成功的原理是什么?上面我们知道,可以通过注解修改接口来完成。原理是什么?查看编译后的目录:原理是在编译时生成接口的一个实现类。打开看下其源代码:publicclassUserStructImplimplementsUserStruct{publicUserStructImpl(){}publicUserShowDTOtoUserShowDTO(UserDOuserDO){if(userDO==null){returnnull;}else{UserShowDTOuserShowDTO=newUserShowDTO();if(userDO.getBirthday()!=null){userShowDTO.setBirthday((newSimpleDateFormat("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-ddHH:mm:ss"));returnuserShowDTO;}}publicListtoUserShowDTOs(ListuserDOs){if(userDOs==null){returnnull;}else{Listlist=newArrayList(userDOs.size());Iteratorvar3=userDOs.iterator();while(var3.hasNext()){UserDOuserDO=(UserDO)var3.next();list.add(this.toUserShowDTO(userDO));}returnlist;}}privateStringuserDOUserExtDORegSource(UserDOuserDO){if(userDO==null){returnnull;}else{UserExtDOuserExtDO=userDO.getUserExtDO();if(userExtDO==null){returnnull;}else{StringregSource=userExtDO.getRegSource();returnregSource==null?null:regSource;}}}privateStringuserDOUserExtDOFavorite(UserDOuserDO){if(userDO==null){returnnull;}else{UserExtDOuserExtDO=userDO.getUserExtDO();if(userExtDO==null){returnnull;}else{Stringfavorite=userExtDO.getFavorite();returnfavorite==null?null:favorite;}}}}其实实现类就是调用对象的get/set等其他常规操作,而List是一个循环调用的对象的单映射方法就清楚了!上面Spring注入方式的例子创建了一个UserStruct实例:UserStructINSTANCE=Mappers.getMapper(UserStruct.class);如@Mapper注解的源码所示:参数componentModeldefaults的值为default,即手动创建实例,或者通过Spring注入Spring。修改后的版本如下:killINSTANCE,@Mapper注解添加componentModel="spring"值。/***微信公众号:Java技术栈*@作者栈长*/@Mapper(componentModel="spring")publicinterfaceUserSpringStruct{@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-ddHH:mm:ss\"))")@Mapping(source="userExtDO.regSource",target="registerSource")@Mapping(source="userExtDO.favorite",target="favorite")@Mapping(target="memo",ignore=true)UserShowDTOtoUserShowDTO(UserDOuserDO);ListtoUserShowDTOs(ListuserDOS);}测试一下:本文使用的是SpringBoot,所以这里使用了SpringBoot的单元测试方式。不懂SpringBoot单元测试的可以关注公众号:Java技术栈,后台回复:boot,系列教程都整理好了。/***微信公众号:Java技术栈*@作者栈长*/@RunWith(SpringRunner.class)@SpringBootTestpublicclassUserSpringStructTest{@AutowiredprivateUserSpringStructuserSpringStruct;@Testpublicvoidtest1(){UserExtDOuserExtDO=newUserExtDO();userExtDO.setRegSource("公众号:Java技术栈");userExtDO.setFavorite("写代码");userExtDO.setSchool("社会大学");UserDOuserDO=newUserDO();userDO.setName("StacklengthSpring");userDO.setSex(1);userDO.setAge(18);userDO.setBirthday(newDate());userDO.setPhone("18888888888");userDO.setMarried(true);userDO.setRegDate(newDate());userDO.setMemo("666");userDO.setUserExtDO(userExtDO);UserShowDTOuserShowDTO=userSpringStruct.toUserShowDTO(userDO);System.out.println("=====单对象映射=====");System.out.println(userShowDTO);ListuserDOs=newArrayList<>();UserDOuserDO2=newUserDO();BeanUtils.copyProperties(userDO,userDO2);userDO2.setName("StacklengthSpring2");userDOs.add(userDO);userDOs.add(userDO2);istuserShowDTOs=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学习,这??里会提供以下SpringBoot实例!本文转载自微信公众号《Java技术栈》,可通过以下二维码关注。转载本文请联系Java技术栈公众号。