前阵子有个用户调用劵列表一直超时,后面经过性能排查:发现这个用户下面有8000多张劵,db查询花了10多毫秒。但是对象从entity到dto的复制却花了几百毫秒,后面定位到性能瓶颈是出现在了ApacheBeanUtils的org.apache.commons.beanutils.BeanUtils#copyProperties方法上面,复制8000多个对象花了几百毫秒。
通过阅读源码发现了org.apache.commons.beanutils.BeanUtils#copyProperties的时间主要花在两个地方:1、反射创建对象 2、值复制的类型转换。 原来ApacheBeanUtils提供的copyProperties除了支持相同类型相同名称的字段复制以外,还支持基本类型到包装类型,包装类型到基本类型, Date转string,long 这些功能,只要字段名称相同ApacheBeanUtils会尽可能进行类型转换然后复制。ApacheBeanUtils的值复制是通过conveter进行的。有兴趣的同学可以中央仓库下载相应的源码去了解。
beanutils --> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.9.1</version></dependency>
具体包路径是org/apache/commons/beanutils/converters下面可以看到各种各样的converters。

后面也查明了性能瓶颈主要卡在了类型转换这里。
上面提到了org.apache.commons.beanutils.BeanUtils#copyProperties的时间主要花在两个地方:1、反射创建对象 2、值复制的类型转换。那反射对性能的影响到底有多大呢,下面我们来探讨一下。
反射是java语言的一个很重要的特性,它可以遍历对象的属性,方法。甚至动态去修改对象的值和行为,突破私有字段的保护机制,访问并修改对象的私有字段。很多底层的框架的都利用了反射的特性, 比如spring的IOC就是利用了反射。
下面来分析一下的反射的方法调用:
这一段代码,通过打印异常的堆栈,得到反射的方法调用链
public class ReflectTest { public void target(int i ){ new Exception("#" + i).printStackTrace(); } public static void main(String[] args) throws Exception { ReflectTest instance = ReflectTest.class.newInstance(); Method method = ReflectTest.class.getMethod("target", int.class); method.invoke(instance,1); } }
java.lang.Exception: #1
at ReflectTest.target(ReflectTest.java:7)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at ReflectTest.main(ReflectTest.java:13)
其中NativeMethodAccessorImpl表示调用的是本地的C++方法。所以我们知道了反射的方法调用是会经过java -> c++ ->java 的转换,所以这个过程看上去的确是比较耗时。。。。这里简称这个为本地实现
既然本地实现的方法可能会比较耗时那么有没有不用调用本地方法实现呢?其实是有的。在默认的JAVA反射机制里面如果反射的方法的调用次数超过一个阀值15,则会用有动态生成的字节码去代替本地的C++方法调用,即动态实现。

