相信很多小伙伴有这样的困扰!!!在开发中,很多时候需要VO和BO或者BO和DO等实体对象之前的赋值转换,以便于后续service层或者dao层实体对象的使用。
正如下图所示,不知道各位看到如何繁多的set赋值第一反应是啥,反正我是挺头疼的。而且这还是只是对象属性赋值的一半不到😂
从理论上讲直接set赋值应该是性能最快的,但是难道就没有什么办法能帮助我们,既能减少如此繁琐且容易出错,甚至遗漏的赋值方式,还能保证性能的方法么?答案当然是肯定有。
可能大家最直观的想法就是使用spring为我们提供的BeanUtils工具类的copyProperties()方法。通过读源码我们知道,该方法包装的Java反射,而且只支持同属性类型和同属性名的对象转换,一旦遇到不同属性类型的对象将自动忽略,导致无法自动赋值。而且如果一个对象的属性字段太多,反射带来的消耗势必将影响性能。
如果之前有频繁使用该工具类的小伙伴,估计遇到过类似如下的情况:
- source对象中的类型为包装类型(ex:Integer)
- target对象中的类型为基本类型 (ex:int)
此时如果还是直接使用BeanUtils的copyProperties方法,将会报异常java.lang.IllegalArgumentException。
当然这可能是最常见的一个例子之一。再比如空格问题,如果在source对象中,属性值中包含空格,在copyProperties后空格会保留,如果最后持久化到数据库中,极大可能会对后续操作造成影响。
另外一个类似的工具类的那就是Apache的commons.beanUtils,这个工具类功能和spring的BeanUtils类似。据说是有不少问题,而且性能也拉胯,何况阿里巴巴开发规范明确规定不允许使用Apache的该工具类进行属性的copy。有兴趣的小伙伴可以研究一下。
当然还有诸如Cglib BeanCpoier以及Apache的PropertyUtils,均不是本文的重点,故不做具体介绍。有兴趣的可以自行研究。
有了上面的问题,那么就要引出我们今天的主角:MapStruct
我们先看一下官网是如何介绍MapStruct的
乍一看认为无非就是和那几个工具类一样么,省去写大量的set方法,简化代码等。但是,朋友们注意这一句:与其他映射框架相比,MapStrut在编译时生成bean映射,以确保高性能,允许快速的开发人员反馈和彻底的错误检查
这是什么意思呢?话不多说我们直接 show code。
- 第一步:创建Maven项目后,我们在pom中引入mapstruct的依赖
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>
- 第二步:在pom的build标签下添加mapstruct的processor,用于在编译时由mapstruct帮助我们生成对象映射
<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>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
- 第三步:添加好依赖后,我们开始创建对象。搞一个BO和DO,然后将BO的属性值复制给DO。
public class UserBO {
private String name;
private Integer id;
private String address;
private String email;
private Integer age;
private String gender;
//省略get/set方法
}
public class UserDO {
private String name;
private Integer id;
private String address;
private String email;
private Integer age;
private String gender;
//省略get/set方法
}
- 第四步:关键步骤,创建一个接口,用于让MapStruct将BO对象转换为DO对象,并正确赋值。其中@Mapper注解标识这是一个MapStruct对象属性转换接口,在编译时MapStruct会自动生成该接口的实现类。Mappers.getMapper()方法用于自动生成实现类的实例。
@Mapper
public interface MapStructConvertUtil {
MapStructConvertUtil INSTANCE = Mappers.getMapper(MapStructConvertUtil.class);
UserDO convert(UserBO userBO);
}
- 第五步:测试看效果:
Ohhhhhhh!!!竟然真的做到了。
什么原因呢?看一下build后的文件,惊奇的发现居然自动生成了一个接口的实现类。
打开后,不觉明历,原来如此。MapStruct不仅自动的生成了接口的实现类,还将我们convert方法的target对象和source对象做了属性匹配。这不恰恰就是我们自己不想写的繁琐的set赋值么。
以此我们也可以推断,相较于直接set方法赋值,使用MapStruct对性能会稍稍有一点影响,就在于生成接口的实现类上,但是理论上该影响会很小,几乎可以保持和直接set赋值一样的性能。后续有时间可以测试一下性能,看看实际差距有多大。
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2021-06-15T23:10:21+0800",
comments = "version: 1.4.2.Final, compiler: javac, environment: Java 1.8.0_292 (Amazon.com Inc.)"
)
public class MapStructConvertUtilImpl implements MapStructConvertUtil {
@Override
public UserDO convert(UserBO userBO) {
if ( userBO == null ) {
return null;
}
UserDO userDO = new UserDO();
userDO.setName( userBO.getName() );
userDO.setId( userBO.getId() );
userDO.setAddress( userBO.getAddress() );
userDO.setEmail( userBO.getEmail() );
userDO.setAge( userBO.getAge() );
userDO.setGender( userBO.getGender() );
return userDO;
}
}
下面我们再做进一步的扩展研究。实际项目中有部分小伙伴项目中可能会用到Lombok,更进一步的简化代码。那么Lombok和MapStruct怎么一起使用呢,仅仅是引入依赖,实体加上@Data注解就OK了么?答案是:NO!!!
我们直接看官网,官网的FAQ下有对应的问题解决方案,并且给出了示例
打开示例的pom文件,可以看到不仅需要引入Lombok依赖,还需要在build中添加Lombok,以及lombok-mapstruct-binding。
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright MapStruct Authors.
Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.mapstruct.examples.lombok</groupId>
<artifactId>mapstruct-examples-lombok</artifactId>
<version>1.0-SNAPSHOT</version>
<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.2.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.20</org.projectlombok.version>
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.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>
<!-- IntelliJ pre 2018.1.1 requires the mapstruct processor to be present as provided dependency -->
<!-- <dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
<scope>provided</scope>
</dependency>-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
<version>4.13.1</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>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
使用官网的demo,我们测试一下整合了lombok的mapstruct的效果。依旧使用UserBO和UserDO这两个实体,稍稍做一些改动。
@Data
public class UserBO {
private String name;
private Integer id;
private String address;
private String email;
private Integer age;
private String gender;
}
@Data
public class UserDO {
private String name;
private Integer id;
private String address;
private String email;
private Integer age;
private String gender;
}
转换接口和测试方法都不变,跑一遍code,看效果。依旧可以实现对象属性的复制。
简单总结一下:
- 使用高版本的idea集成mapstruct,只需要引入mapstruct的依赖,增加mapstruct的转换接口即可。低版本的idea(2018.1.1)则需要将依赖mapstruct-processor也引入
- 集成lombok和mapStruct,不仅需要引入lombok依赖,也需要在build中加入lombok和lombok-mapstruct-binding,用于在builder时自动处理get/set,以及处理lombok和mapstruct的绑定顺序。有兴趣的小伙伴可以试试build中不加ombok-mapstruct-binding依赖,在自动生成的接口实现类型中,就不会有2个对象的属性赋值,只会new一个空的target对象,直接返回。原因就是没有做绑定关系,先生成了接口实现类,而后lombok才处理了get/set。所以测试对象的值都没有赋值成功。
@Generated( value = "org.mapstruct.ap.MappingProcessor", date = "2021-06-16T09:26:01+0800", comments = "version: 1.4.2.Final, compiler: javac, environment: Java 1.8.0_292 (Amazon.com Inc.)" ) public class MapStructConvertUtilImpl implements MapStructConvertUtil { @Override public UserDO convert(UserBO userBO) { if ( userBO == null ) { return null; } UserDO userDO = new UserDO(); return userDO; } }
- 本文只做了入门的基本使用,对于复杂的对象属性,如何使用mapstruct处理,将在《MapStruct进阶使用(二)》中介绍,欢迎围观。
最后,如果觉得本文对你有用,请不要吝啬你的素质三连,你的鼓励就是我创作的最大动力,感谢阅读。