MapStruct是一款很是实用Java工具,主要用于解决对象之间的拷贝问题,好比PO/DTO/VO/QueryParam之间的转换问题。区别于BeanUtils这种经过反射,它经过编译器编译生成常规方法,将能够很大程度上提高效率。@pdaihtml
首先看下这类工具出现的背景。@pdaijava
在开发的时候常常会有业务代码之间有不少的 JavaBean 之间的相互转化,好比PO/DTO/VO/QueryParam之间的转换问题。以前咱们的作法是:git
拷贝技术github
纯get/setspring
MapSturct 是一个生成类型安全, 高性能且无依赖的 JavaBean 映射代码的注解处理器(annotation processor)。apache
工具能够帮咱们实现 JavaBean 之间的转换, 经过注解的方式。json
同时, 做为一个工具类,相比于手写, 其应该具备便捷, 不容易出错的特色。api
这里展现最基本的PO转VO的例子,使用的是IDEA + Lombok + MapStruct缓存
注意:基于当前IDEA设置并不须要
mapstruct-processor
的依赖安全
通常来讲会加载两个包:
org.mapstruct:mapstruct
: 包含Mapstruct核心,好比注解等;若是是mapstruct-jdk8
会引入一些jdk8的语言特性;org.mapstruct:mapstruct-processor
: 处理注解用的,能够根据注解自动生成mapstruct的mapperImpl类以下示例基于IDEA实现,能够在build阶段的annotationProcessorPaths
中配置mapstruct-processor
的path。
<packaging>jar</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <org.mapstruct.version>1.4.0.Beta3</org.mapstruct.version> <org.projectlombok.version>1.18.12</org.projectlombok.version> </properties> <dependencies> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> <!-- lombok dependencies should not end up on classpath --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${org.projectlombok.version}</version> <scope>provided</scope> </dependency> <!-- fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.71</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies> <build> <pluginManagement> <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> <!-- See https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html --> <!-- Classpath elements to supply as annotation processor path. If specified, the compiler --> <!-- will detect annotation processors only in those classpath elements. If omitted, the --> <!-- default classpath is used to detect annotation processors. The detection itself depends --> <!-- on the configuration of annotationProcessors. --> <!-- --> <!-- According to this documentation, the provided dependency processor is not considered! --> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${org.projectlombok.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </pluginManagement> </build>
这里面假设基于一些业务需求采用的是MySQL,且将一些扩展的数据放在了config字段中,并以JSON转String存储。
@Data @Accessors(chain = true) public class User { private Long id; private String username; private String password; // 密码 private Integer sex; // 性别 private LocalDate birthday; // 生日 private LocalDateTime createTime; // 建立时间 private String config; // 其余扩展信息,以JSON格式存储 }
最后真正展现的应该:
@Data @Accessors(chain = true) public class UserVo { private Long id; private String username; private String password; private Integer gender; private LocalDate birthday; private String createTime; private List<UserConfig> config; @Data public static class UserConfig { private String field1; private Integer field2; } }
注意:
@Mapper public interface UserConverter { UserConverter INSTANCE = Mappers.getMapper(UserConverter.class); @Mapping(target = "gender", source = "sex") @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss") UserVo do2vo(User var1); @Mapping(target = "sex", source = "gender") @Mapping(target = "password", ignore = true) @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss") User vo2Do(UserVo var1); List<UserVo> do2voList(List<User> userList); default List<UserVo.UserConfig> strConfigToListUserConfig(String config) { return JSON.parseArray(config, UserVo.UserConfig.class); } default String listUserConfigToStrConfig(List<UserVo.UserConfig> list) { return JSON.toJSONString(list); } }
@Test public void do2VoTest() { User user = new User() .setId(1L) .setUsername("zhangsan") .setSex(1) .setPassword("abc123") .setCreateTime(LocalDateTime.now()) .setBirthday(LocalDate.of(1999, 9, 27)) .setConfig("[{\"field1\":\"Test Field1\",\"field2\":500}]"); UserVo userVo = UserConverter.INSTANCE.do2vo(user); // asset assertNotNull(userVo); assertEquals(userVo.getId(), user.getId()); // print System.out.println(user); System.out.println(userVo); // User(id=1, username=zhangsan, password=abc123, sex=1, birthday=1999-09-27, createTime=2020-08-17T14:54:01.528, config=[{"field1":"Test Field1","field2":500}]) // UserVo(id=1, username=zhangsan, password=abc123, gender=1, birthday=1999-09-27, createTime=2020-08-17 14:54:01, config=[UserVo.UserConfig(field1=Test Field1, field2=500)]) } @Test public void vo2DoTest() { UserVo.UserConfig userConfig = new UserVo.UserConfig(); userConfig.setField1("Test Field1"); userConfig.setField2(500); UserVo userVo = new UserVo() .setId(1L) .setUsername("zhangsan") .setGender(2) .setCreateTime("2020-01-18 15:32:54") .setBirthday(LocalDate.of(1999, 9, 27)) .setConfig(Collections.singletonList(userConfig)); User user = UserConverter.INSTANCE.vo2Do(userVo); // asset assertNotNull(userVo); assertEquals(userVo.getId(), user.getId()); // print System.out.println(user); System.out.println(userVo); }
MapStruct 来生成的代码, 其相似于人手写。 速度上能够获得保证。
前面例子中生成的代码能够在编译后看到, 在 target/generated-sources/annotations 里能够看到; 同时真正在代码包执行的能够在target/classes包中看到。
public class UserConverterImpl implements UserConverter { @Override public UserVo do2vo(User var1) { if ( var1 == null ) { return null; } UserVo userVo = new UserVo(); userVo.setGender( var1.getSex() ); if ( var1.getCreateTime() != null ) { userVo.setCreateTime( DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss" ).format( var1.getCreateTime() ) ); } userVo.setId( var1.getId() ); userVo.setUsername( var1.getUsername() ); userVo.setPassword( var1.getPassword() ); userVo.setBirthday( var1.getBirthday() ); userVo.setConfig( strConfigToListUserConfig( var1.getConfig() ) ); return userVo; } @Override public User vo2Do(UserVo var1) { if ( var1 == null ) { return null; } User user = new User(); user.setSex( var1.getGender() ); if ( var1.getCreateTime() != null ) { user.setCreateTime( LocalDateTime.parse( var1.getCreateTime(), DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss" ) ) ); } user.setId( var1.getId() ); user.setUsername( var1.getUsername() ); user.setBirthday( var1.getBirthday() ); user.setConfig( listUserConfigToStrConfig( var1.getConfig() ) ); return user; } @Override public List<UserVo> do2voList(List<User> userList) { if ( userList == null ) { return null; } List<UserVo> list = new ArrayList<UserVo>( userList.size() ); for ( User user : userList ) { list.add( do2vo( user ) ); } return list; } }
这和Lombok实现机制一致。
核心之处就是对于注解的解析上。JDK5引入了注解的同时,也提供了两种解析方式。
运行时可以解析的注解,必须将@Retention设置为RUNTIME, 好比@Retention(RetentionPolicy.RUNTIME)
,这样就能够经过反射拿到该注解。java.lang,reflect反射包中提供了一个接口AnnotatedElement,该接口定义了获取注解信息的几个方法,Class、Constructor、Field、Method、Package等都实现了该接口,对反射熟悉的朋友应该都会很熟悉这种解析方式。
编译时解析有两种机制,分别简单描述下:
1)Annotation Processing Tool
apt自JDK5产生,JDK7已标记为过时,不推荐使用,JDK8中已完全删除,自JDK6开始,可使用Pluggable Annotation Processing API来替换它,apt被替换主要有2点缘由:
2)Pluggable Annotation Processing API
JSR 269: Pluggable Annotation Processing API自JDK6加入,做为apt的替代方案,它解决了apt的两个问题,javac在执行的时候会调用实现了该API的程序,这样咱们就能够对编译器作一些加强,这时javac执行的过程以下:
Lombok本质上就是一个实现了“JSR 269 API”的程序。在使用javac的过程当中,它产生做用的具体流程以下:
从上面的Lombok执行的流程图中能够看出,在Javac 解析成AST抽象语法树以后, Lombok 根据本身编写的注解处理器,动态地修改 AST,增长新的节点(即Lombok自定义注解所须要生成的代码),最终经过分析生成JVM可执行的字节码Class文件。使用Annotation Processing自定义注解是在编译阶段进行修改,而JDK的反射技术是在运行时动态修改,二者相比,反射虽然更加灵活一些可是带来的性能损耗更加大。
:::tip
通常特性和例子最好直接参考官网例子, 这里会差别化的体现一些常见的用法。@pdai
:::
注意在不一样的JDK版本中作法不太同样。@pdai
通常经常使用的类型字段转换 MapStruct都能替咱们完成,可是有一些是咱们自定义的对象类型,MapStruct就不能进行字段转换,这就须要咱们编写对应的类型转换方法,笔者使用的是JDK8,支持接口中的默认方法,能够直接在转换器中添加自定义类型转换方法。
上述例子中User对象的config属性是一个JSON字符串,UserVo对象中是List类型的,这须要实现JSON字符串与对象的互转。
default List<UserConfig> strConfigToListUserConfig(String config) { return JSON.parseArray(config, UserConfig.class); } default String listUserConfigToStrConfig(List<UserConfig> list) { return JSON.toJSONString(list); }
若是是 JDK8如下的,不支持默认方法,能够另外定义一个 转换器,而后再当前转换器的 @Mapper 中经过 uses = XXX.class 进行引用。
定义好方法以后,MapStruct当匹配到合适类型的字段时,会调用咱们自定义的转换方法进行转换。
好比上面例子中User能够转为UserQueryParam, 业务功能上好比经过UserQueryParam里面的参数进行查找用户的。
@Data @Accessors(chain = true) public class UserQueryParam { private Long id; private String username; }
添加转换方法
UserQueryParam vo2QueryParam(User var1);
除了UserConverter.INSTANCE这种方式还能够注入Spring容器中使用。
当添加componentModel="spring"
时,它会在实现类上自动添加@Component
注解,这样就能被Spring记性component scan,从而加载到springContext中,进而被@Autowird
注入使用。(其它还有jsr330
和cdi
标准,基本上使用componentModel="spring"
就够了)。
@Mapper(componentModel="spring") public interface UserConverter { }
@Slf4j @RunWith(SpringRunner.class) @SpringBootTest public class UserConverterTest { @Resource private UserConverter userConverter; // test methods }
好比上述例子中User购买了东西,须要邮寄到他的地址Address,这时须要展现UserWithAddress的信息:
@Data public class Address { private String street; private Integer zipCode; private Integer houseNo; private String description; }
@Data public class UserWithAddressVo { private String username; private Integer sex; private String street; private Integer zipCode; private Integer houseNumber; private String description; }
@Mapping(source = "person.description", target = "description") @Mapping(source = "address.houseNo", target = "houseNumber") UserWithAddressVo userAndAddress2Vo(User user, Address address);
注意:在多对一转换时, 遵循如下几个原则
属性也能够直接从传入的参数来赋值。
@Mapping(source = "person.description", target = "description") @Mapping(source = "hn", target = "houseNumber") UserWithAddressVo userAndAddressHn2Vo(User user, Integer hn);
:::tip
在了解基本的MapStruct使用以后,咱们将从多个角度来深刻理解MapStruct这个工具。@pdai
:::
一般来讲IDE对于MapStruct这类工具的支持体如今两方面,一个是Maven的集成,另外一个是编辑时的提示(Hit); 相关的支持能够参考官网。@pdai
artifactId
还须要加jdk版本,好比mapstruct-jdk8
;<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </dependency>
mapstruct-processor
的<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <org.mapstruct.version>1.4.0.Beta3</org.mapstruct.version> </properties> <dependencies> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> </dependencies> <build> <pluginManagement> <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> <!-- See https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html --> <!-- Classpath elements to supply as annotation processor path. If specified, the compiler --> <!-- will detect annotation processors only in those classpath elements. If omitted, the --> <!-- default classpath is used to detect annotation processors. The detection itself depends --> <!-- on the configuration of annotationProcessors. --> <!-- --> <!-- According to this documentation, the provided dependency processor is not considered! --> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </pluginManagement> </build>
必须保证你使用的Eclipse中包含
m2e-apt
插件,且尽量的升级这个插件到最新的版本,这个插件主要用于自动应用annotation processor
相关的配置。
同时在pom.xml中推荐你加入以下配置, 缘由请看官方给的以下注释:
<properties> <!-- automatically run annotation processors within the incremental compilation --> <m2e.apt.activation>jdt_apt</m2e.apt.activation> </properties>
基于咱们对它原理的理解,咱们知道mapstrcut最后执行时依然是get/set,因此性能是比较高的。同时咱们也知道反射优化是能够解决一部分性能问题的,那么经过反射方式进行的属性拷贝和get/set这种性能相差多少呢?
综合咱们前面的文章,经常使用的util包中有以下属性拷贝类:
更多测试对比能够参考这里
咱们再看下是否有其它相似的框架呢?这里主要来源这篇文章
Dozer 是一个映射框架,它使用递归将数据从一个对象复制到另外一个对象。框架不只可以在 bean 之间复制属性,还可以在不一样类型之间自动转换。
更多关于 Dozer 的内容能够在官方文档中找到: http://dozer.sourceforge.net/documentation/gettingstarted.html ,或者你也能够阅读这篇文章:https://www.baeldung.com/dozer 。
Orika 是一个 bean 到 bean 的映射框架,它递归地将数据从一个对象复制到另外一个对象。
Orika 的工做原理与 Dozer 类似。二者之间的主要区别是 Orika 使用字节码生成。这容许以最小的开销生成更快的映射器。
更多关于 Orika 的内容能够在官方文档中找到:https://orika-mapper.github.io/orika-docs/,或者你也能够阅读这篇文章:https://www.baeldung.com/orika-mapping。
ModelMapper 是一个旨在简化对象映射的框架,它根据约定肯定对象之间的映射方式。它提供了类型安全的和重构安全的 API。
更多关于 ModelMapper 的内容能够在官方文档中找到:http://modelmapper.org/ 。
JMapper 是一个映射框架,旨在提供易于使用的、高性能的 Java bean 之间的映射。该框架旨在使用注释和关系映射应用 DRY 原则。该框架容许不一样的配置方式:基于注释、XML 或基于 api。
更多关于 JMapper 的内容能够在官方文档中找到:https://github.com/jmapper-framework/jmapper-core/wiki。
对于性能测试,咱们可使用 Java Microbenchmark Harness,关于如何使用它的更多信息能够在 这篇文章:https://www.baeldung.com/java-microbenchmark-harness 中找到。
测试结果(某一种)
全部的基准测试都代表,根据场景的不一样,MapStruct 和 JMapper 都是不错的选择,尽管 MapStruct 对 SingleShotTime 给出的结果要差得多。
当两个对象属性不一致时,好比User对象中某个字段不存在与UserVo当中时,在编译时会有警告提示,能够在@Mapping中配置 ignore = true,当字段较多时,能够直接在@Mapper中设置unmappedTargetPolicy属性或者unmappedSourcePolicy属性为 ReportingPolicy.IGNORE便可。
若是项目中也同时使用到了 Lombok,必定要注意 Lombok的版本要等于或者高于1.18.10,不然会有编译不经过的状况发生。
更多文章请参考 Java 全栈知识体系