屬性拷貝你還在用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<UserMainVO> toVOPage(PageInfo<UserMainBO> page);
}
這里我們可以看出一下幾個特征
- 直接定義接口就可以實現屬性復制
- 屬性對應可以 n source -> 1target
- 集合形式也可以轉
- 特殊實體類只要屬性相同也可以轉
- 可以通過@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</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
這期差不多就這些了.如果對你有用的話,歡迎評論,交流,關注,點贊.
謝謝大家啦~