1.前言
在进行分层和微服务模式下的开发时,我们经常回遇到DTO、DO、VO等之间的转换,如果手动对各个属性进行set的话容易漏掉,同时碰到字段特别多的情况会让人写到吐,当我们对字段进行增减的时候还需要手动处理。为了避免出现这些臃肿又容易出错的代码,我们一般都会使用Bean的拷贝工具。下面来介绍常见的几种。
2.Apache BeanUtils
org.apache.commons.beanutils.BeanUtils.copyProperties
这个Apache下最早使用的Bean拷贝工具类,将我们从一坨Set中解放了出来,代码非常优雅。在Maven中引入
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
将user的代码赋值给userDTO:
UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties(userDTO,user);
不过它有个致命的缺点:慢。
3.Spring BeanUtils
org.springframework.beans.BeanUtils.copyProperties
这个工具类在spring-beans
包中,只要使用的Spring开发,直接使用即可。使用方式跟上面类似,只不过source和target位置正好相反,也就是如下,将user的代码赋值给userDTO:
UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties(user,userDTO);
Spring的BeanUtils与Apache的BeanUtils原理都是用的反射来进行赋值,只不过,在内部实现的时候,Spring少了很多检查,所以速度要比Apache的快很多。
4.BeanCopier
net.sf.cglib.beans.BeanCopier
BeanCopier基于CGLib动态代理,在运行期间会生成代理对象(此处是有反射的),但在对属性进行Copy的时候没有使用反射,正式因为这一点使其速度比纯使用反射快了一些。CGLib包在Spring内部已经集成了,不需要额外的添加。当然,如果没用Spring,可以添加如下引用:
<dependency>
<groupId>asm</groupId>
<artifactId>asm</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>asm</groupId>
<artifactId>asm-commons</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>asm</groupId>
<artifactId>asm-util</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>2.2.2</version>
</dependency>
使用的时候,为了使用的便捷性,同时为了防止每次都生成代理类,将第一次生成的BeanCopier缓存起来,我们需要编写一个工具类:
public class BeanCopierUtils {
private static Logger logger = LoggerFactory.getLogger(BeanCopierUtils.class);
/**
* BeanCopier缓存,flyweight模式的运用,将BeanCopier缓存起来。
*/
private static Map<String, BeanCopier> beanCopierCacheMap = new HashMap<String, BeanCopier>();
/**
* 将source对象的属性拷贝到target对象中去
* @param source source对象
* @param target target对象
*/
public static void copyProperties(Object source, Object target){
String cacheKey = source.getClass().toString() +
target.getClass().toString();
BeanCopier beanCopier = null;
if (!beanCopierCacheMap.containsKey(cacheKey)) {
synchronized(BeanCopierUtils.class) {
if (!beanCopierCacheMap.containsKey(cacheKey)) {
beanCopier = BeanCopier.create(source.getClass(), target.getClass(), false);
beanCopierCacheMap.put(cacheKey, beanCopier);
}
}
} else {
beanCopier = beanCopierCacheMap.get(cacheKey);
}
beanCopier.copy(source, target, null);
}
/**
* 更优雅的属性复制方法
* @param source 数据源对象
* @param clazz 目标Class
* @return 克隆后的对象
*/
public static <T> T copyProperties(Object source,Class<T> clazz) {
T target = null;
try {
target = clazz.newInstance();
BeanCopierUtils.copyProperties(source, target);
} catch (Exception e) {
logger.error("error", e);
}
return target;
}
}
将user的数据赋值给UserDTO,使用方式如下:
BeanCopierUtils.copyProperties(user,UserDTO.class)
5.MapStruct
上面的几种工具都是基于Java的运行期进行处理的,MapStruct另辟蹊径,在编译期动起了心思。官方文档:https://mapstruct.org/documentation/stable/reference/html/ 。
添加引用:
<properties>
<org.mapstruct.version>1.3.1.Final</org.mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
在maven的编译插件下需要添加如下配置:
<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>
</annotationProcessorPaths>
</configuration>
</plugin>
由于MapStruct是依赖的Getter、Setter方法的,如果项目中使用了Lombok,还需要在上面的annotationProcessorPaths标签下添加配置:
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
创建一个映射工具类:
@Mapper
public interface PoToDtoMapping {
/**
* 转换器实例
*/
PoToDtoMapping INSTANCE = Mappers.getMapper(PoToDtoMapping.class);
/**
* 将User转换为UserDTO
* @param user
* @return
*/
UserDTO copyUserToUserDTO(User user);
}
当我们编译完项目之后,可以在 target → generated-sources → annotations
目录下找到MapStruct帮我们生成了一个上面的接口实现类:PoToDtoMappingImpl.java
,里面的内容如下:
public class PoToDtoMappingImpl implements PoToDtoMapping {
@Override
public UserDTO copyUserToUserDTO(User user) {
if ( user == null ) {
return null;
}
UserDTO userDTO = new UserDTO();
userDTO.setId( user.getId() );
userDTO.setName( user.getName() );
userDTO.setSex( user.getSex() );
userDTO.setAddress( user.getAddress() );
return userDTO;
}
}
看,实际上就是我们手动set的方式,只不过代码不用我们自己写了。当然这只是MapStruct最为基本的使用方法,它还有很多自定义的映射方式,具体可以阅读官方文档。
将user属性的数据赋值给userDTO,使用方式如下:
PoToDtoMapping.INSTANCE.copyUserToUserDTO(user)
6.对比
6.1 代码样例
我是使用的SpringBoot进行的测试,直接写在了Controller里,示例代码如下:
public class TestController {
private static List<User> users = new ArrayList<>();
static {
for (int i = 0; i < 1000000; i++) {
User user = new User();
user.setId(i+1);
user.setName("wlc"+i);
user.setAddress("爪哇"+i);
users.add(user);
}
}
@GetMapping("/bean-copy")
public void beanCopyTest(){
//结论:从性能上来说:
// mapStruct >> BeanCopier > Spring BeanUtils >> Apache BeanUtils
// 从使用编辑性上来说,BeanCopier比较优雅
StopWatch stopWatch = new StopWatch();
stopWatch.start();
List<UserDTO> userDTOS1 = new ArrayList<>();
for (User user : users) {
userDTOS1.add(BeanCopierUtils.copyProperties(user,UserDTO.class));
}
stopWatch.stop();
log.info("BeanCopier花费时间:{}ms",stopWatch.getLastTaskTimeMillis());
stopWatch.start();
List<UserDTO> userDTOS2 = new ArrayList<>();
for (User user : users) {
UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties(user,userDTO);
userDTOS2.add(userDTO);
}
stopWatch.stop();
log.info("Spring BeanUtils花费时间:{}ms",stopWatch.getLastTaskTimeMillis());
stopWatch.start();
List<UserDTO> userDTOS3 = new ArrayList<>();
for (User user : users) {
userDTOS3.add(PoToDtoMapping.INSTANCE.copyUserToUserDTO(user));
}
stopWatch.stop();
log.info("MapStruct 拷贝花费时间:{}ms",stopWatch.getLastTaskTimeMillis());
stopWatch.start();
List<UserDTO> userDTOS4 = new ArrayList<>();
try{
for (User user : users) {
UserDTO userDTO = new UserDTO();
org.apache.commons.beanutils.BeanUtils.copyProperties(userDTO,user);
userDTOS4.add(userDTO);
}
}catch (Exception e){
log.error("拷贝失败:{}",e);
}
stopWatch.stop();
log.info("Apache BeanUtils花费时间:{}ms",stopWatch.getLastTaskTimeMillis());
log.info("总共花费时间:{}ms",stopWatch.getTotalTimeMillis());
}
}
结果如下:
BeanCopier花费时间:715ms
Spring BeanUtils花费时间:1473ms
MapStruct 拷贝花费时间:34ms
Apache BeanUtils花费时间:10313ms
总共花费时间:12536ms
BeanCopier花费时间:637ms
Spring BeanUtils花费时间:1553ms
MapStruct 拷贝花费时间:33ms
Apache BeanUtils花费时间:10136ms
总共花费时间:12361ms
BeanCopier花费时间:624ms
Spring BeanUtils花费时间:1485ms
MapStruct 拷贝花费时间:85ms
Apache BeanUtils花费时间:9857ms
总共花费时间:12053ms
6.2 结论
从上面的结果可以看出,速度对比
:最快的工具MapStruct当仁不让,是最慢的Apache BeanUtils速度的100倍以上,BeanCopier速度次之,大约是Spring BeanUtils的2倍。
使用便捷性对比
: MapStruct使用相对来说更不方便,但由于其功能比较灵活,可以定制很多Bean的拷贝方式,基本上能杜绝Set;其他三种使用更为便捷。