属性拷贝你还在用BeanUtils?
从PO, DTO到Domain Driven Design这篇文章提到各种实体类, 工作中我们往往因为领域的问题要在DO,BO,VO,DTO之间来回转换.
最初
年轻时候的我是这样做的.
可以看出我这套块编辑的操作还是挺骚的. 但还是感觉麻烦.
于是我找了几个常用的三方工具
- org.apache.commons.beanutils.BeanUtils.copyProperties
- org.apache.commons.beanutils.PropertyUtils.copyProperties
- org.springframework.beans.BeanUtils.copyProperties
- org.springframework.cglib.beans.BeanCopier.copy
- org.mapstruct
本文主推使用mapstruct做属性复制,请看下文
通常选择一个工具我们要从性能和实用性方面两个角度去考量.
性能检测
为了足够硬核,我们实践检验效果.通过一组测试来检查5个方法的性能表现.
测试代码如下:
@Slf4j
public class CopyDemoTest {
public UserMainBO bo;
public static int count = 1000000;
@Before
public void init(){
bo = new UserMainBO();
bo.setId(1L);
}
@Test
public void mapstruct() {
UserMainVOMapping INSTANCE = Mappers.getMapper( UserMainVOMapping.class );
log.info("star------------");
for (int i = 1; i <=count; i++) {
// log.debug(i+"");
UserMainVO vo = INSTANCE.toVO(bo);
}
log.info("end------------");
}
@Test
public void beanCopier() {
log.info("star------------");
BeanCopier copier = BeanCopier.create(UserMainBO.class, UserMainVO.class, false);
for (int i = 1; i <=count; i++) {
// log.debug(i+"");
UserMainVO vo = new UserMainVO();
copier.copy(bo, vo, null);
}
log.info("end------------");
}
@Test
public void springBeanUtils(){
log.info("star------------");
for (int i = 1; i <=count; i++) {
// log.debug(i+"");
UserMainVO vo = new UserMainVO();
BeanUtils.copyProperties(bo, vo);
}
log.info("end------------");
}
@Test
public void apacheBeanUtils() throws InvocationTargetException, IllegalAccessException {
for (int i = 1; i <=count; i++) {
// log.debug(i+"");
UserMainVO vo = new UserMainVO();
org.apache.commons.beanutils.BeanUtils.copyProperties(bo, vo);
}
log.info("end------------");
}
@Test
public void apachePropertyUtils() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
log.info("star------------");
for (int i = 1; i <=count; i++) {
// log.debug(i+"");
UserMainVO vo = new UserMainVO();
PropertyUtils.copyProperties(bo, vo);
}
log.info("end------------");
}
}
tools/count | 1000/次 | 10000/次 | 100000/次 | 1000000/次 |
---|---|---|---|---|
apache.BeanUtils | 550ms | 1085ms | 4287ms | 32088ms |
apache.PropertyUtils | 232ms | 330ms | 2080ms | 20681ms |
cglib.BeanCopier | 73ms | 106ms | 102ms | 99ms |
mapstruct | 91ms | 5ms | 7ms | 12ms |
spring.BeanUtils | 5ms | 188ms | 336ms | 844ms |
如图所示, 可看出其性能一次是:
mapstruct > BeanCopier > spring.BeanUtils > apache.PropertyUtils > apache.BeanUtils
mapstruct 相当的顶, 性能遥遥领先.
分析
我们看下源码
apache.BeanUtils
while(var13.hasNext()) {
Entry<String, Object> entry = (Entry)var13.next();
String name = (String)entry.getKey();
if (this.getPropertyUtils().isWriteable(dest, name)) {
// 核心逻辑
this.copyProperty(dest, name, entry.getValue());
}
}
apache.PropertyUtils
value = this.getSimpleProperty(orig, name);
if (dest instanceof DynaBean) {
((DynaBean)dest).set(name, value);
} else {
// 核心逻辑
this.setSimpleProperty(dest, name, value);
}
Apache 主要集中了各种丰富的功能(日志、转换、解析等等),导致性能变差。
而Spring BeanUtils则是直接通过反射来读取和写入
for(int var9 = 0; var9 < var8; ++var9) {
PropertyDescriptor targetPd = var7[var9];
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
} catch (Throwable var15) {
throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var15);
}
}
}
}
}
BeanCopier
public abstract void copy(Object var1, Object var2, Converter var3);
BeanCopier动态生成一个要代理类的子类,其实就是通过字节码方式转换成性能最好的get和set方式,只需考虑创建BeanCopier的开销.
mapstruct
这里我们打开UserMainVOMapping的实现类可以看出,其相当硬核.直接用maven 在编译期间生产对应的实现类.
看到这里有的观众老爷就会说了:"那这个性能差的也不多啊, 我为什么要用mapsturct?" 请接着往下看
实用性
mapstruct的接口
@Mapper(componentModel = "spring")
public interface UserMainVOMapping {
@Mappings({
@Mapping(source = "testB", target = "testV")
})
UserMainVO toVO(UserMainBO userMainBO);
List<UserMainVO> toVOList(List<UserMainBO> list);
PageInfo<UserMainBO> toVOPage(PageInfo<UserMainBO> page);
}
这里我们可以看出一下几个特征
- 直接定义接口就可以实现属性复制
- 属性对应可以 n source -> 1tager
- 集合形式也可以转
- 特殊实体类只要属性相同也可以转
- 可以通过@Mapping 指定属性复制路径
足见其功能的强大
mapstruct的使用
maven 配置
这里只介绍 maven形式配置, gradle 和 ant 请参考官网
<properties>
<org.mapstruct.version>1.3.1.Final</org.mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
注意这里配置一下lombok, 否则启动会冲突
Mapping 接口编写
@Mapper(componentModel = "spring")
public interface UserMainVOMapping {
@Mappings({
@Mapping(source = "testB", target = "testV")
})
UserMainVO toVO(UserMainBO userMainBO);
List<UserMainVO> toVOList(List<UserMainBO> list);
PageInfo<UserMainBO> toVOPage(PageInfo<UserMainBO> page);
}
使用类示意
@RestController
public class UserController {
@Resource
private UserMainVOMapping userMainVOMapping;
@Resource
private UserMainDOService userMainDOService;
@GetMapping("/userMain/page")
public ResultVO page(int page, int pageSize, String nickname) {
PageInfo<UserMainBO> userPage = userMainDOService.page(page, pageSize, nickname, false);
UserMainVOMapping INSTANCE = Mappers.getMapper( UserMainVOMapping.class);
return new ResultVO(ResultEnum.SUCCESS, INSTANCE.toVOPage(userPage));
}
@GetMapping("/userMain/page2")
public ResultVO page2(int page, int pageSize, String nickname) {
PageInfo<UserMainBO> userPage = userMainDOService.page(page, pageSize, nickname, false);
return new ResultVO(ResultEnum.SUCCESS, userMainVOMapping.toVOPage(userPage));
}
@GetMapping("moreToOne")
public UserMainVO moreToOne(){
UserMainBO userMainBO = new UserMainBO();
userMainBO.setId(1L);
userMainBO.setTestB("test");
SubBO subBO = new SubBO();
subBO.setA("A");
userMainBO.setSub(subBO);
return userMainVOMapping.toVO(userMainBO);
}
}
注意
可以在Mapping类上加@Mapper(componentModel = "spring"), 通过注入的方式引入Mapper接口
可以通过Mappers.getMapper( UserMainVOMapping.class) 的方式获取Mapper接口
总结
- 众多属性拷贝工具中mapstruct相对比较好
- mapstruct书写性能极高
- mapstruct书写非常方便
- mapstruct非常灵活可用来定义各种类型的属性copy
这期差不多就这些了.如果对你有用的话,欢迎评论,交流,关注,点赞.
谢谢大家啦~