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

子月生發表於2020-12-09

簡介

在實際專案中,考慮到不同的資料使用者,我們經常要處理 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 操作。它也支援設定自定義轉換器,需要注意的是,這種轉換器僅限當前呼叫有效,而且,我們需要在同一個轉換器裡處理所有型別的轉換

    @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 程式碼的代理類,通過設定以下系統屬性可以在指定路徑輸出生成的代理類:

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 是深度複製,使用者物件中的部門物件和崗位物件也會生成新的例項物件並拷貝屬性。

orika_class

開啟其中一個檔案,可以看到,普通屬性直接賦值,像部門物件這種,會呼叫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.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://www.cnblogs.com/ZhangZiSheng001/p/14108080.html

相關文章