Apache BeanUtils性能缺陷分析

常见Bean工具类性能比较

Updated on 2019-12-02 21:28 (Created on: 2019-11-20 09:42)

前言

最近开发的时候, 发现有段结果VO转换成DO的代码显示错误, 细看发现是阿里的代码规范报错:

报错

看到这个提示, 有点摸不着头脑, 只看到建议, 未知晓原因. 翻查了一番资料之后发现, 阿里规范强制要求不能使用Apache BeanUtils, 建议使用Spring BeanUtils, Cglib BeanCopier取而代之, 因为使用Apache BeanUtils可能会导致性能下降. 关于Apache BeanUtils会导致性能下降多少? 其他竞品的性能优势又有多大? 对于这样的问题, 阿里规范并没有给出详细解释. 所谓纸上得来终觉浅, 绝知此事要躬行, 没有好奇心的程序员和咸鱼又有什么差别, 心存疑问, 我决定自己动手研究下.

Benchmark 准备工作

参与测试的是社区比较流行的的Bean类库:

  1. Apache BeansUtils, 对应包版本: 1.9.4
  2. Spring BeanUtils, 对应包版本: 5.2.1.RELEASE
  3. Cglib BeanCopier, 对应包版本: 3.3.0

使用的测试框架是JMH, 即Java Microbenchmark Harness,这是专门用于进行代码的微基准测试的一套工具API, Java9之后添加到JDK标准库中. (为什么不使用手写多次循环的方式进行性能测试? 是因为多次循环作性能测试的方式基本是不准确的, 存在过多的干扰因素)

用于测试的类, 是一个拥有60个非原始类型字段的类, 成员变量类型包含有String, List, BigDecimal, 具体字段参考AlphaNum.java

测试机器, CPU: 2.2 GHz Intel Core i7; Memory: 16 GB; OS: Mac OSX

Java版本: Java8

Benchmark 测试详情

测试代码如下, 关于JMH参数的用法我不就展开了, 关于JMH的具体说明可以参考这篇文章: Java微基准测试框架JMH


/**
 * @author RamsayLeung/ramsayleung@gmail.com
 * @version : BeanUtilsBenchmark.java, v 0.1 2019年11月18日 14:23 ramsay Exp $
 */
@Warmup(iterations = 5)
@Measurement(iterations = 10, time = 5)
@Fork(2)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
public class BeanUtilsBenchmark {
    private AlphaNum   source;
    private AlphaNum   destination;
    private BeanCopier copier;

    @Setup
    public void init() {
        source = new AlphaNum();
        destination = new AlphaNum();
        copier = BeanCopier.create(AlphaNum.class, AlphaNum.class, false);
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public void testApacheBeanUtils() throws InvocationTargetException, IllegalAccessException {
        org.apache.commons.beanutils.BeanUtils.copyProperties(destination, source);
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public void testSpringBeanUtils() {
        org.springframework.beans.BeanUtils.copyProperties(source, destination);
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public void testCglibBeanCopier() {
        copier.copy(source, destination, null);
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public void testNativeCopy() {
        destination.set_A(source.get_A());
        destination.set_B(source.get_B());
        // 此处省略60行
    }

    public static void main(String[] args) throws RunnerException {
        Options options = new OptionsBuilder()
                .include(BeanUtilsBenchmark.class.getSimpleName())
                .build();
        new Runner(options).run();
    }
}

基准测试类型是Throughput模式指的是, "1秒内最多可以执行多少次调用", 下面是执行结果, 具体的执行日志可以查看 benchmark log:

Benchmark                                    Mode  Cnt      Score             Error    Units
BeanUtilsBenchmark.testApacheBeanUtils      thrpt   20     15.972 ±    0.490           ops/ms
BeanUtilsBenchmark.testSpringBeanUtils      thrpt   20    273.154 ±   10.125           ops/ms
BeanUtilsBenchmark.testNativeCopy           thrpt   20  46414.463 ± 1093.329           ops/ms
BeanUtilsBenchmark.testCglibBeanCopier      thrpt   20  47896.804 ±  535.622           ops/ms

Cglib BeanCopier开挂了么, 怎么比其他两个类库快了4个数量级, 堪比直接调用get&&set方法, 每毫秒执行了近200百万次?

cglib: 我cglib 没有开挂, 我就是那么强.

原因分析

Apache BeanUtils

先来分析下Apache BeanUtils为什么性能有问题, 是什么问题导致的? Apache BeanUtilsSpring BeanUtilscopy性能也相差了近一个数量级. 先来看下Apache BeanUtils copy操作最耗时的操作是什么? 循环10000000次的Apache BeanUtils.copyProperties方法(循环次数少了, 没法取样, 也可能是我姿势不对?), 然后通过jvisualvm查找最费时的函数

jvisualvm

最耗时的函数分别是:

  1. getSimpleProperty
  2. isReadable
  3. isWriteable
  4. copyProperty
  5. setSimpleProperty

让我们来看一下, 这几个函数的功能究竟是什么? 竟然会如此费时:

public void copyProperties(final Object dest, final Object orig) throws IllegalAccessException, InvocationTargetException {
    // 省略几十行判断代码
    final PropertyDescriptor[] origDescriptors =
        getPropertyUtils().getPropertyDescriptors(orig);
    for (PropertyDescriptor origDescriptor : origDescriptors) {
        final String name = origDescriptor.getName();
        if ("class".equals(name)) {
            continue; // No point in trying to set an object's class
        }
        // 判断source对象是否可读, 近100行的判断逻辑
        if (getPropertyUtils().isReadable(orig, name) &&
        // 判断destination对象是否可写, 又是近100行的判断逻辑
            getPropertyUtils().isWriteable(dest, name)) {
            try {
                final Object value =
                    // 调用Apache Beans包内的包装的invoke函数, 获取对应字段的属性值, 又校验几十行
                    getPropertyUtils().getSimpleProperty(orig, name);
                // 调用底层的属性copy函数
                copyProperty(dest, name, value);
            } catch (final NoSuchMethodException e) {
                // Should not happen
            }
        }
    }
}

public void copyProperty(final Object bean, String name, Object value) throws IllegalAccessException, InvocationTargetException {
    // 省略前面一百多行判断代码
    try {
        // 调用Apache Beans包内的包装的invoke函数, 设置字段的属性值, 又校验几十行
        getPropertyUtils().setSimpleProperty(target, propName, value);
    } catch (final NoSuchMethodException e) {
        throw new InvocationTargetException
            (e, "Cannot set " + propName);
    }
}

看过Apache BeanUtils的源码, 就能发现问题所在: Apache BeanUtils力求做得完美, 增加了非常多的校验, 兼容, 日志代码, 过度的包装导致性能下降严重, 着实是过犹不及. 本来属性copy的基本原理就是获取需要copy的属性值, 判断下读写权限, 然后设置成目标对象属性, 没想到被Apache BeanUtils搞得这么复杂.

Spring BeanUtils

既然Spring BeanUtilsApache BeanUtils都是使用反射来实现属性copy功能的, 那么Spring BeanUtilsApache BeanUtils快的原因是什么呢? 先来看看Spring BeanUtils属性copy的代码:

private static void copyProperties(Object source, Object target, @Nullable Class<?> editable, @Nullable String... ignoreProperties) 
    throws BeansException {

    // 省略几行的校验代码
    for (PropertyDescriptor targetPd : targetPds) {
        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 {
                        // 判断source对象是否可读
                        if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                            readMethod.setAccessible(true);
                        }
                        // 获取要copy对应字段的属性值
                        Object value = readMethod.invoke(source);
                        // 判断destination对象是否可写
                        if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                            writeMethod.setAccessible(true);
                        }
                        // copy属性值到destination对象
                        writeMethod.invoke(target, value);
                    }
                    catch (Throwable ex) {
                        throw new FatalBeanException(
                                "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                    }
                }
            }
        }
    }
}

Spring BeanUtils的实现原理还是和Apache BeanUtils一样, 但是却简单得多, 直接用反射进行读写; Apache BeanUtils用几十行代码做的事, Spring BeanUtils顶多两行就搞定了, 难怪Spring BeanUtils会比Apache BeanUtils快了一个数量级(我就喜欢像Spring BeanUtils这样的耿直孩子, 不喜"繁文缛节").

猜想验证

为了验证Spring BeanUtilsApache BeanUtils快的原因是减少大量的校验/日志代码, 我要将Apache BeanUtils属性copy的函数自行实现一遍, 将无甚用处的辅助代码去掉, 直接调用反射进行属性copy, 然后再和Spring BeanUtils比较性能, 以此验证猜想:

public void copyProperties(final Object dest, final Object orig) {
    final PropertyDescriptor[] origDescriptors = getPropertyUtils().getPropertyDescriptors(orig);
    for (PropertyDescriptor origDescriptor : origDescriptors) {
        final String name = origDescriptor.getName();
        Method readMethod = origDescriptor.getReadMethod();
        try {

            // 为了对比效果, 我直接将校验读写权限的代码去掉
            final Object value = readMethod.invoke(orig);

            PropertyDescriptor descriptor = getPropertyUtils().getPropertyDescriptor(dest, name);

            Method writeMethod = descriptor.getWriteMethod();

            writeMethod.invoke(dest, value);

        } catch (final Exception e) {
            throw new RuntimeException("Could not copy property from source to target", e);
        }
    }
}

自己实现的copy属性函数与其他copy函数性能比较:

Benchmark                                    Mode  Cnt      Score             Error    Units
BeanUtilsBenchmark.testApacheBeanUtils      thrpt   20     16.118 ±    0.805           ops/ms
BeanUtilsBenchmark.testCustomizedBeanUtils  thrpt   20     75.338 ±    0.558           ops/ms(我实现的版本)
BeanUtilsBenchmark.testSpringBeanUtils      thrpt   20    280.316 ±    8.999           ops/ms
BeanUtilsBenchmark.testNativeCopy           thrpt   20  47523.015 ± 1146.777           ops/ms
BeanUtilsBenchmark.testCglibBeanCopier      thrpt   20  48416.499 ±  260.375           ops/ms

自行实现属性copy的版本与Spring BeanUtils的性能主要差距从原来的1个数量级下降到了4倍差距, 而这4倍差距猜测是getPropertyDescriptor的不同实现的性能导致的, 也成功验证了是过多的辅助功能(如日志, 解析, 转换)导致Apache BeanUtils性能下降.

Cglib BeanCopier

为什么Cglib BeanCopier的速度堪比原生的get&&set方法呢? 因为Cglib BeanCopier核心功能就是通过操作字节码生成类, 来实现原本通过反射或者一堆代码才能实现的逻辑. 下面的代码中, Cglib就在背后悄悄替我们生成了两个类:

BeanCopier copier = BeanCopier.create(AlphaNum.class, AlphaNum.class, false);
copier.copy(source, destination, null);

生成的其中一个类, 它通过拷贝属性代码, 来实现我们需要的拷贝逻辑, 这个生成类就是BeanCopier.create(AlphaNum.class, AlphaNum.class, false);对应的代码:

public class Object$$BeanCopierByCGLIB$$2949144c extends BeanCopier {
    public Object$$BeanCopierByCGLIB$$2949144c() {
    }

    public void copy(Object var1, Object var2, Converter var3) {
        AlphaNum var10000 = (AlphaNum)var2;
        AlphaNum var10001 = (AlphaNum)var1;
        var10000.set_1(((AlphaNum)var1).get_1());
        var10000.set_A(var10001.get_A());
        var10000.set_AList(var10001.get_AList());
        // 此外省略近60行
    }
}

既然是生成get&&set方法, 这样就难怪Cglib BeanCopier的速度那么快了.

总结

Apache BeanUtils的性能最差, 难怪阿里的Java规范会强制不允许使用呢; 而Spring BeanUtilsCglib BeanCopier性能都是挺好的. 因为实现机制的差异, Cglib BeanCopier性能比Spring BeanUtils高四个数量级, 如果对性能有特别要求, 可使用BeanCopier, 其性能可以与直接调用get&&set方法相提并论, 否则可以使用Spring BeanUtils