本篇主要介绍一下 mybatis 的反射优化相关内容,让我们了解一下 mybatis 的反射优化是怎么做的以及为什么需要做反射优化,基于3.4.6版本
知识点
- 什么是反射
- mybatis为什么要优化
- mybatis是怎么优化的
什么是反射
说到反射,我相信大家有考虑过继续走技术路线的肯定都听说过。我们平时在创建一个对象并调用对应方法的时候,都是通过以下方式来做的
A a = new A();
a.do();
但是如果我们使用反射的方式,则是这样实现的
A a = A.class.newInstance();
或者
Constructor constructor = A.class.getDeclaredConstructor();
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
A obj =constructor.newInstance();
为什么这么麻烦,这么做有什么好处呢?反射最大的好处就是灵活。这里先解释一个概念,对于new
方式创建的对象,是在编译期就确定的,而对于反射创建的对象,是在运行期创建的。灵活就体现在它是运行期(.class文件)确定创建的,试想一下,如果我们有一个需求需要在 oracle 数据库和 mysql 数据库之间动态切换,那么我们在创建对象实例的时候是不是根据传入的数据库类型来动态创建会比较好,这样的话代码里只要写好加载对应类的逻辑,至于有没有这个类我们完全可以在外部来定(是否有.class文件),当然,如果是为了精简代码,也是一种非常好的选择。举个例子,我们来定义一个根据类型获取对象的方法
不用反射:
public T getInstance(Class tClass){
if (tClass == A.class){
return (T)new A();
}
if (tClass == B.class){
return (T)new B();
}
throw new IllegalArgumentException("tClass unknow");
}
使用反射:
public T getInstance(Class tClass){
Assert.isTrue(tClass == ReflectionObject.class || tClass == UserInfo.class, "tClass unknow");
try{
return tClass.newInstance();
}
catch (Exception ex){
//log
throw new RuntimeException(ex);
}
mybatis为什么要优化
在我们了解了什么时候反射以及使用反射有什么好处之后,接着来看下反射有哪些问题。直接上代码,先定义一个对象
public class ReflectionObject {
private String a;
private int b;
public String getA() {
return a;
}
public void setA(String a) {
this.a = a;
}
public int getB() {
return b;
}
public void setB(int b) {
this.b = b;
}
}
生成对象问题
先来生成一下该对象
long startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++){
ReflectionObject reflectionObject = new ReflectionObject();
}
long endTime = System.currentTimeMillis();
System.out.println("未反射总耗时:" + (endTime - startTime) + "ms");
try{
startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++){
ReflectionObject reflectionObject1 = ReflectionObject.class.newInstance();
}
endTime = System.currentTimeMillis();
System.out.println("反射总耗时:" + (endTime - startTime) + "ms");
}
catch (Exception ex){
}
这个代码很简单,就是执行一万次反射和非反射来生成对象,来看下结果
好像不是很明显,那执行10万次
还是差距不大,来个100万次
开始体现差距了(10倍)再加一个数量级到1000万次
差距拉开到20倍了,这里我们可以得出结论,当执行次数达到一定程度之后(一般100万次以上),用反射来生成对象的性能就会开始急剧下降。
方法调用问题
接着来试一下方法调用的影响,反射方法调用需要两步,第一步是获取方法对象,第二步是发起方法调用,我们把这两步拆出来测,还是从1万次开始
ReflectionObject reflectionObject = new ReflectionObject();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++){
reflectionObject.setA("abc");
String a = reflectionObject.getA();
}
long endTime = System.currentTimeMillis();
System.out.println("未反射总耗时:" + (endTime - startTime) + "ms");
try{
startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++){
Method setMethod = ReflectionObject.class.getDeclaredMethod("setA", String.class);
Method getMethod =ReflectionObject.class.getDeclaredMethod("getA");
}
endTime = System.currentTimeMillis();
System.out.println("反射获取方法耗时:" + (endTime - startTime) + "ms");
Method setMethod = ReflectionObject.class.getDeclaredMethod("setA", String.class);
Method getMethod =ReflectionObject.class.getDeclaredMethod("getA");
startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++){
setMethod.invoke(reflectionObject, "cba");
getMethod.invoke(reflectionObject);
}
endTime = System.currentTimeMillis();
System.out.println("获取方法后反射调用耗时:" + (endTime - startTime) + "ms");
来看下结果
已经体现差距了,加到10万次
差距越来越明显了,再加到100万次
差距非常明显了,这里可以得出结论了,1万次开始使用反射来调用方法就很慢了,其中主要耗时在反射获取方法。
字段获取问题
最后来试一下字段调用的影响,反射字段调用也需要两步,第一步是获取字段对象,第二步是发起字段调用,我们把这两步拆出来测,还是从1万次开始
ReflectionObject reflectionObject = new ReflectionObject();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++){
reflectionObject.setA("abc");
String a = reflectionObject.getA();
}
long endTime = System.currentTimeMillis();
System.out.println("未反射总耗时:" + (endTime - startTime) + "ms");
try{
startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++){
Field a = ReflectionObject.class.getDeclaredField("a");
}
endTime = System.currentTimeMillis();
System.out.println("反射获取字段耗时:" + (endTime - startTime) + "ms");
Field a = ReflectionObject.class.getDeclaredField("a");
a.setAccessible(true);
startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++){
a.set(reflectionObject, "cba");
a.get(reflectionObject);
}
endTime = System.currentTimeMillis();
System.out.println("获取字段后反射调用耗时:" + (endTime - startTime) + "ms");
看下结果
可以看到比方法快一点,加到10万次
差距不够明显,加到100万次
可以看到反射获取字段明显最慢了,再加到1000万次
差距很明显了,结论基本上也确定了,字段反射获取比方法获取快,主要也是慢在获取字段的过程,比字段调用耗时多了一倍。
优化原因
从以上测试结果可以看出,执行次数越多,则性能差距越大,你以为100万次很多吗,试想一下我们平时在使用 mybatis 的时候,一天得执行多少次sql ?是不是100万次很快就没了,甚至1000万次也是一下执行光了。作为一款优秀的持久化框架,mybatis 自然用到了反射机制(比如参数映射、结果集映射),不然这么多自定义类型怎么优雅支持,那也不能为了优雅损失性能吧?权衡利弊之下,mybatis 就开始对反射这块进行优化了。
mybatis是怎么优化的
讲了这么多和 mybatis 无关的,终于要聊到 mybatis 了,我们来看看 mybatis 是如何对反射做优化的,先看下反射优化相关的代码
上面这个包下都是反射优化相关的代码逻辑,我们基于上一节中的问题来看 mybatis 的优化,这样更顺畅一些。上一节中提到3个反射问题:生成对象问题、方法调用问题、字段获取问题。我们逐个分析看 mybatis 做了什么。
首先是生成对象问题,只要用到反射,这个是无可避免的,所以 mybatis 并没有对此进行优化,mybtais 用到了简单工厂模式,提供了ObjectFactory
接口,默认实现类是DefaultObjectFactory
可以看到这里直接用反射来生成对象,但是 mybatis 提供了扩展,我们在配置中可以使用自定义的 ObjectFactory
,参照官方配置
在自定义的逻辑中我们可以对其进行部分优化,比如使用对象池,当然前提是你要了解 mybatis 所有相关逻辑的影响(还得考虑多线程下的问题)。
再看第二个问题,方法调用问题。从上一节的分析中可知,方法调用的性能问题主要在获取方法对象这里,至于方法调用,这个肯定是无法优化了(除非你不用该方法了),所以 mybatis 就对获取方法对象这块反射进行了优化。直接看优化的核心类org.apache.ibatis.reflection.Reflector
,这个类非常重要,要理解 mybatis 反射优化,一定要理解这个类,我们暂时称之为反射器,我直接用一张图来说明,关键点都在注解里
这里基本就知道了,在反射器构造的时候,直接把要反射的类给拆解掉了,然后存入对应的成员变量中去。这里要注意的是setMethods
和getMethods
,这两个是优化的核心,他们将属性封装成了调用类进行缓存,后续遇到同样的属性名称,就不去重新通过反射获取方法对象了,直接用调用类来发起调用,这样就省去了反射获取方法这步损耗,具体调用类在这里
当然,对于反射器本身也是有缓存的,在这里
也就是说对于同一个类型,就不需要再去解析它的类信息了(这里就需要同时考虑一个问题:如果类发生热更新,可能会出现不可预知的情况,比如新增的某个字段 mybatis 一直取不到,这种情况就要考虑关闭缓存),这里使用的也是简单工厂模式,但是和ObjectFactory
不一样的是,ReflectorFactory
不支持配置扩展。另外反射器虽然是反射优化的实现核心,但是他只是包内使用,不对外用,对外用的是MetaClass
和MetaObject
。MetaClass
其实就是对反射器的一个代理,这里用的就是代理模式
另外我觉得reflectorFactory
成员变量其实是可以不用定义的,维护一个reflector
就够了。MetaObject
其实也没多少东西,就是一个objectWrapper
的代理
可以看到它支持的 wrapper就这些,大多数情况下我们用的是 BeanWrapper,当然这里提到一个ObjectWrapperFactory
,这个从代码上看原先应该是支持配置扩展的
遗憾的是官方文档里没有该项配置说明,通过该扩展,我们就能够定义自己的ObjectWrapper
。
最后来看字段获取问题,这点的优化其实和第二点一样
可以看到本质上都是加到setMethods
和getMethods
中去了,不再赘述。
最后我们来看下 mybatis 这个优化是否起作用了(拿性能快的字段获取来对比),这里就不慢慢叠加的了,直接100万次,上代码
ReflectionObject reflectionObject = new ReflectionObject();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++){
reflectionObject.setA("abc");
String a = reflectionObject.getA();
}
long endTime = System.currentTimeMillis();
System.out.println("未反射总耗时:" + (endTime - startTime) + "ms");
try{
startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++){
Field a = ReflectionObject.class.getDeclaredField("a");
}
endTime = System.currentTimeMillis();
System.out.println("反射获取字段耗时:" + (endTime - startTime) + "ms");
Field a = ReflectionObject.class.getDeclaredField("a");
a.setAccessible(true);
startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++){
a.set(reflectionObject, "cba");
a.get(reflectionObject);
}
endTime = System.currentTimeMillis();
System.out.println("获取字段后反射调用耗时:" + (endTime - startTime) + "ms");
DefaultSqlSessionFactory sqlSessionFactory = (DefaultSqlSessionFactory)applicationContext.getBean("sqlSessionFactory");
startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++){
MetaObject metaObject = sqlSessionFactory.getConfiguration().newMetaObject(reflectionObject);
Object aaa = metaObject.getValue("a");
metaObject.setValue("a", "bcd");
}
endTime = System.currentTimeMillis();
System.out.println("mybatis反射优化后总耗时:" + (endTime - startTime) + "ms");
}
catch (Exception ex){
}
再看下结果
呦吼,真的优化了不少了,将近一半以上,如果对比方法调用,就更多了。想必这里你们会提出一个问题:不是说 mybatis 优化了获取的耗时吗,为什么结果比获取字段后的反射调用耗时要多50%呢?那是因为new了MetaObject,这个是有不少损耗的。
总结
本篇总体干货满满,基本上将 mybatis 的反射这块介绍得差不多了,也说明了优化的原因和点,后续我们自己开发就要注意反射了,也知道如何进行优化。