1. 程式人生 > >cglib、orika、spring等bean copy工具效能測試和原理分析

cglib、orika、spring等bean copy工具效能測試和原理分析

# 簡介 在實際專案中,考慮到不同的資料使用者,我們經常要處理 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 如下,這個是之前測試序列化工具時用過的。一個使用者物件,一對一關聯部門物件和崗位物件,其中部門物件又存在自關聯。 ```java 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 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 非常簡單,通常只要一句程式碼就可以了。它支援自定義轉換器(這個轉換器是全域性的,將替代預設的轉換器)。 ```java @Benchmark public UserVO testApacheBeanUtils(CommonState commonState) throws Exception { /*ConvertUtils.register(new Converter() { @Override public T convert(Class 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 的效能稍差。還有一點需要注意,**它的複製只是淺度複製**。 ```java // 獲取目標類的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 差不多,也是簡單的一句程式碼。但是,**前者只支援同類型屬性的轉換,且不支援自定義轉換器**。 ```java @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 操作。它也支援設定自定義轉換器,需要注意的是,**這種轉換器僅限當前呼叫有效,而且,我們需要在同一個轉換器裡處理所有型別的轉換**。 ```java @Benchmark public UserVO testCglibBeanCopier(CommonState commonState) throws Exception { BeanCopier copier = BeanCopier.create(commonState.user.getClass(), UserVO.class, false); UserVO userVO = new UserVO(); copier.copy(commonState.user, userVO, null); assert "zzs0".equals(userVO.getName()); return userVO; // 設定自定義轉換器 /**BeanCopier copier = BeanCopier.create(commonState.user.getClass(), UserVO.class, true); UserVO userVO = new UserVO(); copier.copy(commonState.user, userVO, new Converter() { @Override public Object convert(Object value, Class target, Object context) { if(Integer.class.isInstance(value)) { System.err.println("賦值Integer屬性"); } return value; } }); assert "zzs0".equals(userVO.getName()); return userVO;**/ } ``` cglib beanCopier 的原理也不復雜,它是使用了 asm 生成一個包含所有 setter/getter 程式碼的代理類,通過設定以下系統屬性可以在指定路徑輸出生成的代理類: ```properties cglib.debugLocation=D:/growUp/test ``` 開啟上面例子生成的代理類,可以看到,**源物件屬性值的獲取、目標物件屬性值的設定,都是直接呼叫對應方法,而不是使用反射**,通過後面的測試會發現它的速度接近我們手動 setter/getter。另外,**cglib beanCopier 也是淺度複製**。 ```java 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`物件可複用,不需要重複建立**。 ```java @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() { @Override public Integer convert(Boolean source, Type 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檔案): ```properties # 輸出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 是深度複製**,使用者物件中的部門物件和崗位物件也會生成新的例項物件並拷貝屬性。 ![orika_class](https://img2020.cnblogs.com/blog/1731892/202012/1731892-20201209134026787-321701029.png) 開啟其中一個檔案,可以看到,普通屬性直接賦值,像部門物件這種,會呼叫`BoundMapperFacade`繼續拷貝。 ```java 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 指令如下: ```shell mvn clean package java -ea -jar target/benchmarks.jar -f 1 -t 1 -wi 10 -i 10 ``` 測試結果如下: ```shell # 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.181 ± 0.035 ops/ms BeanCopyTest.testCglibBeanCopier thrpt 10 7640.876 ± 36.674 ops/ms BeanCopyTest.testDeadCode thrpt 10 12419.576 ± 195.084 ops/ms BeanCopyTest.testOrikaBeanCopy thrpt 10 1458.256 ± 25.725 ops/ms BeanCopyTest.testSpringBeanUtils thrpt 10 87.586 ± 6.582 ops/ms ``` 根據測試結果,物件拷貝速度方面: **手動拷貝 > cglib beanCopier > orika mapper > spring beanUtils > apache commons beanUtils** 由於 apache commons beanUtils 和 spring beanUtils 使用了大量反射,所以速度較慢; cglib beanCopier 和 orika mapper 使用動態代理生成包含 setter/getter 的程式碼的代理類,不需要呼叫反射來賦值,所以,速度較快。orika mapper 是深度複製,需要額外處理物件型別的屬性轉換,也增加了部分開銷。 以上資料僅供參考。感謝閱讀。 > 相關原始碼請移步:[ beanCopy-tool-demo](https://github.com/ZhangZiSheng001/beanCopy-tool-demo) > 本文為原創文章,轉載請附上原文出處連結:[https://www.cnblogs.com/ZhangZiSheng001/p/14108080.html ](https://www.cnblogs.com/ZhangZiSheng001/p/14108080.html )