关于mybatis:mybatis之反射优化

45次阅读

共计 7080 个字符,预计需要花费 18 分钟才能阅读完成。

本篇次要介绍一下 mybatis 的反射优化相干内容,让咱们理解一下 mybatis 的反射优化是怎么做的以及为什么须要做反射优化,基于 3.4.6 版本

知识点

  • 什么是反射
  • mybatis 为什么要优化
  • mybatis 是怎么优化的

什么是反射

说到反射,我置信大家有思考过持续走技术路线的必定都据说过。咱们平时在创立一个对象并调用对应办法的时候,都是通过以下形式来做的

A a = new A();
a.do();

然而如果咱们应用反射的形式,则是这样实现的

A a = A.class.newInstance();

或者

Constructor<A> constructor = A.class.getDeclaredConstructor();
if (!constructor.isAccessible()) {constructor.setAccessible(true);
    }
A obj =constructor.newInstance();

为什么这么麻烦,这么做有什么益处呢?反射最大的益处就是灵便。这里先解释一个概念,对于 new 形式创立的对象,是在编译期就确定的,而对于反射创立的对象,是在运行期创立的。灵便就体现在它是运行期(.class 文件)确定创立的,试想一下,如果咱们有一个需要须要在 oracle 数据库和 mysql 数据库之间动静切换,那么咱们在创建对象实例的时候是不是依据传入的数据库类型来动态创建会比拟好,这样的话代码里只有写好加载对应类的逻辑,至于有没有这个类咱们齐全能够在内部来定(是否有.class 文件),当然,如果是为了精简代码,也是一种十分好的抉择。举个例子,咱们来定义一个依据类型获取对象的办法

不必反射:

    public <T> T getInstance(Class<T> tClass){if (tClass == A.class){return (T)new A();}
        if (tClass == B.class){return (T)new B();}
        throw new IllegalArgumentException("tClass unknow");
    }

应用反射:

    public <T> T getInstance(Class<T> 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 反射优化,肯定要了解这个类,咱们临时称之为反射器,我间接用一张图来阐明,关键点都在注解里

这里根本就晓得了,在反射器结构的时候,间接把要反射的类给拆解掉了,而后存入对应的成员变量中去。这里要留神的是 setMethodsgetMethods,这两个是优化的外围,他们将属性封装成了调用类进行缓存,后续遇到同样的属性名称,就不去从新通过反射获取办法对象了,间接用调用类来发动调用,这样就省去了反射获取办法这步损耗,具体调用类在这里

当然,对于反射器自身也是有缓存的,在这里

也就是说对于同一个类型,就不须要再去解析它的类信息了(这里就须要同时思考一个问题:如果类产生热更新,可能会呈现不可预知的状况,比方新增的某个字段 mybatis 始终取不到,这种状况就要思考敞开缓存),这里应用的也是简略工厂模式,然而和 ObjectFactory 不一样的是,ReflectorFactory不反对配置扩大。另外反射器尽管是反射优化的实现外围,然而他只是包内应用,不对外用,对外用的是 MetaClassMetaObjectMetaClass其实就是对反射器的一个代理,这里用的就是代理模式

另外我感觉 reflectorFactory 成员变量其实是能够不必定义的,保护一个 reflector 就够了。MetaObject其实也没多少货色,就是一个 objectWrapper 的代理

能够看到它反对的 wrapper 就这些,大多数状况下咱们用的是 BeanWrapper,当然这里提到一个 ObjectWrapperFactory,这个从代码上看原先应该是反对配置扩大的

遗憾的是官网文档里没有该项配置阐明,通过该扩大,咱们就可能定义本人的 ObjectWrapper
最初来看字段获取问题,这点的优化其实和第二点一样

能够看到实质上都是加到 setMethodsgetMethods中去了,不再赘述。

最初咱们来看下 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 的反射这块介绍得差不多了,也阐明了优化的起因和点,后续咱们本人开发就要留神反射了,也晓得如何进行优化。

正文完
 0