Reflection optimization of mybatis

Posted by BRUUUCE on Wed, 05 Jan 2022 09:56:44 +0100

This article mainly introduces the related contents of reflection optimization of mybatis. Let's understand how reflection optimization of mybatis is done and why reflection optimization is needed. It is based on version 3.4.6

Knowledge points

  • What is reflection
  • Why should mybatis be optimized
  • How is mybatis optimized

What is reflection

When it comes to reflection, I believe everyone has considered continuing to take the technical route. I'm sure they've all heard of it. We usually create an object and call the corresponding method in the following ways

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

But if we use reflection, this is how we do it

A a = A.class.newInstance();

perhaps

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

Why is it so troublesome and what are the benefits of doing so? The biggest advantage of reflection is flexibility. Here we will explain a concept first. Objects created in the new mode are determined at compile time, while objects created in reflection are created at run time. The flexibility is reflected in the fact that it is created by the runtime (. Class file). Imagine that if we have a requirement to dynamically switch between oracle database and mysql database, it would be better if we dynamically create the object instance according to the incoming database type. In this case, as long as the logic of loading the corresponding class is written in the code, We can decide whether there is this class externally (whether there is a. Class file). Of course, it is also a very good choice if it is to simplify the code. For example, let's define a method to get objects by type

No reflection:

    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");
    }

Use reflection:

    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);
        }

Why should mybatis be optimized

After we understand when reflection is used and the benefits of using reflection, let's take a look at the problems of reflection. Directly to the code, define an object first

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;
    }
}

Generate object problem

Let's generate the object first

        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++){
            ReflectionObject reflectionObject = new ReflectionObject();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Total unreflected time:" + (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("Total reflection time:" + (endTime - startTime) + "ms");
        }
        catch (Exception ex){

        }

This code is very simple, which is to execute reflection and non reflection 10000 times to generate objects. Let's see the results

It doesn't seem obvious. 100000 times

There's still a small gap. One million times

Start to reflect the gap (10 times) and add an order of magnitude to 10 million times

The gap has widened to 20 times. Here we can conclude that when the execution times reach a certain level (generally more than 1 million times), the performance of generating objects with reflection will begin to decline sharply.

Method call problem

Next, let's try the impact of method calls. Reflecting method calls requires two steps. The first step is to obtain the method object, and the second step is to initiate method calls. Let's take these two steps apart and start with 10000 times

        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("Total unreflected time:" + (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("Reflection acquisition method takes time:" + (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("Reflection call time after getting method:" + (endTime - startTime) + "ms");

Let's see the results

The gap has been reflected. It has been added to 100000 times

The gap is becoming more and more obvious. Add it to 1 million times

The gap is very obvious. It can be concluded here that it is very slow to use reflection to call methods 10000 times, which mainly takes time to obtain reflection methods.

Field acquisition problem

Finally, let's try the impact of field calls. Reflecting field calls also requires two steps. The first step is to obtain the field object, and the second step is to initiate field calls. Let's take these two steps apart and start with 10000 times

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("Total unreflected time:" + (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("Reflection field acquisition time:" + (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("Reflection call time after getting field:" + (endTime - startTime) + "ms");

Look at the results

It can be seen that it is a little faster than the method. Add it to 100000 times

The gap is not obvious enough, added to 1 million times

It can be seen that the reflection acquisition field is obviously the slowest, and it is added to 10 million times

The gap is obvious, and the conclusion is basically determined. The field reflection acquisition is faster than the method acquisition, mainly slow. In the process of acquiring the field, it takes twice as long as the field call.

Optimization reasons

From the above test results, it can be seen that the more execution times, the greater the performance gap. Do you think there are a lot of 1 million times? Imagine how many times a day we have to execute sql when using mybatis? Is it true that 1 million times will soon disappear, and even 10 million times will be run out at once. As an excellent persistence framework, mybatis naturally uses reflection mechanisms (such as parameter mapping and result set mapping). Otherwise, how can so many custom types be supported gracefully, and you can't lose performance for elegance? Weighing the pros and cons, mybatis began to optimize the reflection.

How is mybatis optimized

After talking about so much that has nothing to do with mybatis, we finally want to talk about mybatis. Let's see how mybatis optimizes reflection. First, let's look at the code related to reflection optimization

Under the above package are the code logic related to reflection optimization. We look at the optimization of mybatis based on the problems in the previous section, which makes it smoother. In the previous section, we mentioned three reflection problems: object generation, method invocation and field acquisition. Let's analyze it one by one to see what mybatis has done.

The first is the problem of generating objects. As long as reflection is used, it is inevitable, so mybatis does not optimize it. mybtais uses the simple factory mode and provides the ObjectFactory interface. The default implementation class is DefaultObjectFactory

You can see that reflection is directly used to generate objects here, but mybatis provides extensions. We can use custom ObjectFactory in configuration and refer to the official configuration

In the custom logic, we can partially optimize it, such as using object pool. Of course, the premise is that you need to understand the impact of all relevant logic of mybatis (you have to consider the problem of multithreading).
Look at the second problem, method invocation. From the analysis in the previous section, it can be seen that the performance problem of method call mainly lies in obtaining method objects. As for method call, it must not be optimized (unless you don't use the method), so mybatis optimizes the reflection of obtaining method objects. Look directly at the optimized core class org apache. ibatis. reflection. Reflector, this class is very important. To understand mybatis reflection optimization, we must understand this class. We temporarily call it reflector. I will directly illustrate it with a diagram, and the key points are in the annotation

It is basically known here that when the reflector is constructed, the class to be reflected is directly disassembled and stored in the corresponding member variable. It should be noted here that setMethods and getMethods are the core of optimization. They encapsulate the attributes into calling classes for caching. If they encounter the same attribute name later, they will not re obtain the method object through reflection, but directly use the calling class to initiate the call. In this way, the loss of reflection acquisition method is omitted. The specific calling class is here

Of course, there is a cache for the reflector itself, here

In other words, for the same type, there is no need to parse its class information (here, we need to consider a problem at the same time: if the class is hot updated, unpredictable situations may occur, such as closing the cache when a new field mybatis is is not available). The simple factory mode is also used here, Unlike ObjectFactory, however, ReflectorFactory does not support configuration extensions. In addition, although the reflector is the core of the implementation of reflection optimization, it is only used in the package, not for external use. It uses MetaClass and MetaObject for external use. MetaClass is actually a proxy for the reflector. The proxy mode is used here

In addition, I think the member variables of reflectorFactory need not be defined. Maintaining a reflector is enough. In fact, MetaObject doesn't have many things. It is a proxy of objectWrapper

You can see that these wrappers are supported. In most cases, we use BeanWrapper. Of course, an ObjectWrapperFactory is mentioned here. From the code point of view, it should have supported configuration extension

Unfortunately, there is no configuration description in the official document. Through this extension, we can define our own ObjectWrapper.
Finally, let's look at the field acquisition problem. The optimization of this point is actually the same as the second point

You can see that they are essentially added to setMethods and getMethods. I won't repeat them.

Finally, let's take a look at whether the optimization of mybatis works (compare the field acquisition with fast performance). Here, we don't stack it slowly. We directly add the code 1 million times

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("Total unreflected time:" + (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("Reflection field acquisition time:" + (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("Reflection call time after getting field:" + (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 Total time after reflection Optimization:" + (endTime - startTime) + "ms");
        }
        catch (Exception ex){

        }

Look at the results again

Yo Ho, you've really optimized a lot, more than half. If you compare method calls, there will be more. You must ask a question here: doesn't mybatis optimize the acquisition time? Why does the result take 50% more time than the reflection call after obtaining the field? That's because new MetaObject, which has a lot of losses.

summary

This article is full of dry goods. It basically introduces the reflection of mybatis and explains the reasons and points of optimization. We should pay attention to reflection and know how to optimize our own development in the future.

Topics: Mybatis reflection