簡介
在實際項目中,考慮到不同的數據使用者,我們經常要處理 VO、DTO、Entity、DO 等對象的轉換,如果手動編寫 setter/getter 方法一個個賦值,將非常繁瑣且難維護。通常情況下,這類轉換都是同名屬性的轉換(類型可以不同),我們更多地會使用 bean copy 工具,例如 Apache Commons BeanUtils、Cglib BeanCopier 等。
在使用 bean copy 工具時,我們更多地會考慮性能,有時也需要考慮深淺復制的問題。本文將對比幾款常用的 bean copy 工具的性能,並介紹它們的原理、區別和使用注意事項。
項目環境
本文使用 jmh 作為測試工具。
os:win 10
jdk:1.8.0_231
jmh:1.25
選擇的 bean copy 工具及對應的版本如下:
apache commons beanUtils:1.9.4
spring beanUtils:5.2.10.RELEASE
cglib beanCopier:3.3.0
orika mapper:1.5.4
測試代碼
本文使用的 java bean 如下,這個是之前測試序列化工具時用過的。一個用戶對象,一對一關聯部門對象和崗位對象,其中部門對象又存在自關聯。
public class User implements Serializable {
private static final long serialVersionUID = 1L;
// 普通屬性--129個
private String id;
private String account;
private String password;
private Integer status;
// ······
/**
* 所屬部門
*/
private Department department;
/**
* 崗位
*/
private Position position;
// 以下省略setter/getter方法
}
public class Department implements Serializable {
private static final long serialVersionUID = 1L;
// 普通屬性--7個
private String id;
private String parentId;
// ······
/**
* 子部門
*/
private List<Department> children;
// 以下省略setter/getter方法
}
public class Position implements Serializable {
private static final long serialVersionUID = 1L;
// 普通屬性--6個
private String id;
private String name;
// ······
// 以下省略setter/getter方法
}
下面展示部分測試代碼,完整代碼見末尾鏈接。
apache commons beanUtils
apache commons beanUtils 的 API 非常簡單,通常只要一句代碼就可以了。它支持自定義轉換器(這個轉換器是全局的,將替代默認的轉換器)。
@Benchmark
public UserVO testApacheBeanUtils(CommonState commonState) throws Exception {
/*ConvertUtils.register(new Converter() {
@Override
public <T> T convert(Class<T> type, Object value) {
if (Boolean.class.equals(type) || boolean.class.equals(type)) {
final String stringValue = value.toString().toLowerCase();
for (String trueString : trueStrings) {
if (trueString.equals(stringValue)) {
return type.cast(Boolean.TRUE);
}
}
// ······
}
return null;
}
}, Boolean.class);*/
UserVO userVO = new UserVO();
org.apache.commons.beanutils.BeanUtils.copyProperties(userVO, commonState.user);
assert "zzs0".equals(userVO.getName());
return userVO;
}
apache commons beanUtils 的原理比較簡單,濃縮起來就是下面的幾行代碼。可以看到,源對象屬性值的獲取、目標對象屬性值的設置,都是使用反射實現,所以,apache commons beanUtils 的性能稍差。還有一點需要注意,它的復制只是淺度復制。
// 獲取目標類的BeanInfo對象(這個會緩存起來,不用每次都重新創建)
BeanInfo targetBeanInfo = Introspector.getBeanInfo(target.getClass());
// 獲取目標類的PropertyDescriptor數組(這個會緩存起來,不用每次都重新創建)
PropertyDescriptor[] targetPds = targetBeanInfo.getPropertyDescriptors();
// 遍歷PropertyDescriptor數組,並給同名屬性賦值
for(PropertyDescriptor targetPd : targetPds) {
// 獲取源對象中同名屬性的PropertyDescriptor對象,當然,這個也是通過Introspector獲取的
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
// 讀取源對象中該屬性的值
Method readMethod = sourcePd.getReadMethod();
Object value = readMethod.invoke(source);
// 設置目標對象中該屬性的值
Method writeMethod = targetPd.getWriteMethod();
writeMethod.invoke(target, value);
}
spring beanUtils
spring beanUtils 的 API 和 apache commons beanUtils 差不多,也是簡單的一句代碼。但是,前者只支持同類型屬性的轉換,且不支持自定義轉換器。
@Benchmark
public UserVO testSpringBeanUtils(CommonState commonState) throws Exception {
UserVO userVO = new UserVO();
org.springframework.beans.BeanUtils.copyProperties(commonState.user, userVO);
assert "zzs0".equals(userVO.getName());
return userVO;
}
看過 spring beanUtils 源碼就會發現,它只是一個簡單的工具類,只有短短幾行代碼。原理的話,和 apache commons beanUtils 一樣的,所以,它的復制也是淺度復制。
cglib beanCopier
cglib beanCopier 需要先創建一個BeanCopier
,然后再執行 copy 操作。它也支持設置自定義轉換器,但是,這種轉換器僅限當前調用有效,而且,我們需要在同一個轉換器里處理所有類型的轉換。使用 cglib beanCopier 需要注意,BeanCopier
對象可復用,不需要重復創建。
@Benchmark
public UserVO testCglibBeanCopier(CommonState commonState, CglibBeanCopierState cglibBeanCopierState) throws Exception {
BeanCopier copier = cglibBeanCopierState.copier;
UserVO userVO = new UserVO();
copier.copy(commonState.user, userVO, null);
assert "zzs0".equals(userVO.getName());
return userVO;
}
@State(Scope.Benchmark)
public static class CglibBeanCopierState {
BeanCopier copier;
@Setup(Level.Trial)
public void prepare() {
copier = BeanCopier.create(User.class, UserVO.class, false);
}
}
cglib beanCopier 的原理也不復雜,它是使用了 asm 生成一個包含所有 setter/getter 代碼的代理類,通過設置以下系統屬性可以在指定路徑輸出生成的代理類:
cglib.debugLocation=D:/growUp/test
打開上面例子生成的代理類,可以看到,源對象屬性值的獲取、目標對象屬性值的設置,都是直接調用對應方法,而不是使用反射,通過后面的測試會發現它的速度接近我們手動 setter/getter。另外,cglib beanCopier 也是淺度復制。
public class Object$$BeanCopierByCGLIB$$6bc9202f extends BeanCopier
{
public void copy(final Object o, final Object o2, final Converter converter) {
final UserVO userVO = (UserVO)o2;
final User user = (User)o;
userVO.setAccount(user.getAccount());
userVO.setAddress(user.getAddress());
userVO.setAge(user.getAge());
userVO.setBirthday(user.getBirthday());
userVO.setDepartment(user.getDepartment());
userVO.setDiploma(user.getDiploma());
// ······
}
}
orika mapper
相比其他 bean copy 工具,orika mapper 的 API 要復雜一些,相對地,它的功能也更強大,不僅支持注冊自定義轉換器,還支持注冊對象工廠、過濾器等。使用 orika mapper 需要注意,MapperFactory
對象可復用,不需要重復創建。
@Benchmark
public UserVO testOrikaBeanCopy(CommonState commonState, OrikaState orikaState) throws Exception {
MapperFacade mapperFacade = orikaState.mapperFactory.getMapperFacade();// MapperFacade對象始終是同一個
UserVO userVO = mapperFacade.map(commonState.user, UserVO.class);
assert "zzs0".equals(userVO.getName());
return userVO;
}
@State(Scope.Benchmark)
public static class OrikaState {
MapperFactory mapperFactory;
@Setup(Level.Trial)
public void prepare() {
mapperFactory = new DefaultMapperFactory.Builder().build();
/*mapperFactory.getConverterFactory().registerConverter(new CustomConverter<Boolean, Integer>() {
@Override
public Integer convert(Boolean source, Type<? extends Integer> destinationType, MappingContext mappingContext) {
if(source == null) {
return null;
}
return source ? 1 : 0;
}
});*/
}
}
orika mapper 和 cglib beanCopier 有點類似,也會生成包含所有 setter/getter 代碼的代理類,不同的是 orika mapper 使用的是 javassist,而 cglib beanCopier 使用的是 asm。
通過設置以下系統屬性可以在指定路徑輸出生成的代理類(本文選擇直接輸出java文件):
# 輸出java文件
ma.glasnost.orika.GeneratedSourceCode.writeSourceFiles=true
ma.glasnost.orika.writeSourceFilesToPath=D:/growUp/test
# 輸出class文件
# ma.glasnost.orika.GeneratedSourceCode.writeClassFiles=true
# ma.glasnost.orika.writeClassFilesToPath=D:/growUp/test
和 cglib beanCopier 不同,orika mapper 生成了三個文件。根本原因在於 orika mapper 是深度復制,用戶對象中的部門對象和崗位對象也會生成新的實例對象並拷貝屬性。
打開其中一個文件,可以看到,普通屬性直接賦值,像部門對象這種,會調用BoundMapperFacade
繼續拷貝。
public class Orika_UserVO_User_Mapper166522553009000$0 extends ma.glasnost.orika.impl.GeneratedMapperBase {
public void mapAtoB(java.lang.Object a, java.lang.Object b, ma.glasnost.orika.MappingContext mappingContext) {
super.mapAtoB(a, b, mappingContext);
// sourceType: User
cn.zzs.bean.copy.other.User source = ((cn.zzs.bean.copy.other.User)a);
// destinationType: UserVO
cn.zzs.bean.copy.other.UserVO destination = ((cn.zzs.bean.copy.other.UserVO)b);
destination.setAccount(((java.lang.String)source.getAccount()));
destination.setAddress(((java.lang.String)source.getAddress()));
destination.setAge(((java.lang.Integer)source.getAge()));
if(!(((cn.zzs.bean.copy.other.Department)source.getDepartment()) == null)) {
if(((cn.zzs.bean.copy.other.Department)destination.getDepartment()) == null) {
destination.setDepartment((cn.zzs.bean.copy.other.Department)((ma.glasnost.orika.BoundMapperFacade)usedMapperFacades[0]).map(((cn.zzs.bean.copy.other.Department)source.getDepartment()), mappingContext));
} else {
destination.setDepartment((cn.zzs.bean.copy.other.Department)((ma.glasnost.orika.BoundMapperFacade)usedMapperFacades[0]).map(((cn.zzs.bean.copy.other.Department)source.getDepartment()), ((cn.zzs.bean.copy.other.Department)destination.getDepartment()), mappingContext));
}
} else {
{
destination.setDepartment(null);
}
}
// ······
if(customMapper != null) {
customMapper.mapAtoB(source, destination, mappingContext);
}
}
public void mapBtoA(java.lang.Object a, java.lang.Object b, ma.glasnost.orika.MappingContext mappingContext) {
// ······
}
}
測試結果
以下以吞吐量作為指標,相同條件下,吞吐量越大越好。
cmd 指令如下:
mvn clean package
java -ea -jar target/benchmarks.jar -f 1 -t 1 -wi 10 -i 10
測試結果如下:
# JMH version: 1.25
# VM version: JDK 1.8.0_231, Java HotSpot(TM) 64-Bit Server VM, 25.231-b11
# VM invoker: D:\growUp\installation\jdk1.8.0_231\jre\bin\java.exe
# VM options: -ea
# Warmup: 10 iterations, 10 s each
# Measurement: 10 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
Benchmark Mode Cnt Score Error Units
BeanCopyTest.testApacheBeanUtils thrpt 10 4.077 ± 0.046 ops/ms
BeanCopyTest.testCglibBeanCopier thrpt 10 12158.830 ± 112.239 ops/ms
BeanCopyTest.testDeadCode thrpt 10 12393.230 ± 219.693 ops/ms
BeanCopyTest.testOrikaBeanCopy thrpt 10 1424.492 ± 16.948 ops/ms
BeanCopyTest.testSpringBeanUtils thrpt 10 88.815 ± 1.235 ops/ms
根據測試結果,對象拷貝速度方面:
手動拷貝 > cglib beanCopier > orika mapper > spring beanUtils > apache commons beanUtils
由於 apache commons beanUtils 和 spring beanUtils 使用了大量反射,所以速度較慢;
cglib beanCopier 和 orika mapper 使用動態代理生成包含 setter/getter 的代碼的代理類,不需要調用反射來賦值,所以,速度較快。cglib beanCopier 的速度和手動拷貝不相上下。
orika mapper 是深度復制,需要額外處理對象類型的屬性轉換,也增加了部分開銷。
以上數據僅供參考。感謝閱讀。
2021-05-28 更改
相關源碼請移步: beanCopy-tool-demo
本文為原創文章,轉載請附上原文出處鏈接:https://www.cnblogs.com/ZhangZiSheng001/p/14108080.html