Writing mybatis from scratch (2) detailed explanation of mybatis interceptor plug-in mechanism

Time:2020-12-2

Prospect review

In the MVP version, we have implemented a basic running mybatis.

As the saying goes, everything is difficult at the beginning, then difficult in the middle.

The plug-in mechanism of mybatis is the second soul besides dynamic proxy.

Let’s experience the pain and happiness of this interesting soul~

The role of plug-ins

In the actual development process, the mybaits plug-in we often use is the paging plug-in. Through the pagination plug-in, we can get the paginated data without writing the count statement and limit, which brings us great development

The convenience of. In addition to paging, plug-in usage scenarios mainly include updating common fields of database, sub database and sub table, encryption and decryption, etc.

This blog mainly talks about the principle of the mybatis plug-in. The next blog will design a mybatis plug-in to realize the function of generating snowflake ID as the primary key of each data instead of automatically increasing ID of database when adding data.

Writing mybatis from scratch (2) detailed explanation of mybatis interceptor plug-in mechanism

JDK dynamic agent + responsibility chain design pattern

Mybatis’s plug-in is actually an interceptor function. It uses the comprehensive application of JDK dynamic agent and responsibility chain design pattern. Adopt the chain of responsibility model, organize multiple interceptors through dynamic agents, through which you can do something you want to do.

So before we talk about the mybatis interceptor, let’s talk about the JDK dynamic proxy + chain of responsibility design pattern.

JDK dynamic proxy case

package com.github.houbb.mybatis.plugin;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JdkDynamicProxy {

    /**
     *One interface
     */
    public interface HelloService{
        void sayHello();
    }

    /**
     *The target class implements the interface
     */
    static class HelloServiceImpl implements HelloService{

        @Override
        public void sayHello() {
            System.out.println("sayHello......");
        }

    }

    /**
     *Custom proxy classes need to implement the invocationhandler interface
     */
    static  class HelloInvocationHandler implements InvocationHandler {

        private Object target;

        public HelloInvocationHandler(Object target){
            this.target = target;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println ("--- insert pre notification code --------";
            //Implement the corresponding target method
            Object rs = method.invoke(target,args);
            System.out.println ("--- insert post processing code --------";
            return rs;
        }

        public static Object wrap(Object target) {
            return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                    target.getClass().getInterfaces(),new HelloInvocationHandler(target));
        }
    }

    public static void main(String[] args)  {
        HelloService proxyService = (HelloService) HelloInvocationHandler.wrap(new HelloServiceImpl());
        proxyService.sayHello();
    }

}
  • output
------Insert pre notification code-------------
sayHello......
------Insert post processing code-------------

Optimization 1: object oriented

The above proxy function is implemented, but there is an obvious defect. Helloinvocationhandler is a dynamic proxy class, which can also be understood as a tool class. It is impossible for us to write business code to the invoke method,

Do not conform to the idea of object-oriented, can be abstracted to deal with.

Defining interfaces

You can design an interceptor interface, and you need to intercept and implement the interface.

public interface Interceptor {

    /**
     *Specific interception processing
     */
    void intercept();

}

Implementation interface

public class LogInterceptor implements Interceptor{

    @Override
    public void intercept() {
        System.out.println ("--- insert pre notification code --------";
    }

}

and

public class TransactionInterceptor implements Interceptor{

    @Override
    public void intercept() {
        System.out.println ("--- insert post processing code --------";
    }

}

Implement proxy

public class InterfaceProxy implements InvocationHandler {

    private Object target;

    private List<Interceptor> interceptorList = new ArrayList<>();

    public InterfaceProxy(Object target, List<Interceptor> interceptorList) {
        this.target = target;
        this.interceptorList = interceptorList;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //Processing multiple interceptors
        for (Interceptor interceptor : interceptorList) {
            interceptor.intercept();
        }
        return method.invoke(target, args);
    }

    public static Object wrap(Object target, List<Interceptor> interceptorList) {
        InterfaceProxy targetProxy = new InterfaceProxy(target, interceptorList);
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(), targetProxy);
    }

    

}

Test verification

public static void main(String[] args) {
    List<Interceptor> interceptorList = new ArrayList<>();
    interceptorList.add(new LogInterceptor());
    interceptorList.add(new TransactionInterceptor());

    HelloService target = new HelloServiceImpl();
    HelloService targetProxy = (HelloService) InterfaceProxy.wrap(target, interceptorList);
    targetProxy.sayHello();
}
  • journal
------Insert pre notification code-------------
------Insert post processing code-------------
sayHello......

There is an obvious problem here, all intercepts are handled before the method is executed.

Optimization 2: before and after flexible designation

The above dynamic proxy can indeed extract the business logic from the proxy class. However, we notice that only the front-end agent can not achieve the front-end and back-end proxy, so it needs to be optimized.

So we need to do a little more abstraction,

The information of intercepted object is encapsulated as the parameter of interceptor interception method, and the real execution method of intercepting target object is put into interceptor to complete. In this way, interception before and after can be realized, and parameters of intercepted object can be modified.

Realization ideas

Proxy class context

Design an invocation object.

public class Invocation {

    /**
     *Target object
     */
    private Object target;
    /**
     *Methods of implementation
     */
    private Method method;
    /**
     *Parameters of the method
     */
    private Object[] args;

    public Invocation(Object target, Method method, Object[] args) {
        this.target = target;
        this.method = method;
        this.args = args;
    }

    /**
     *Method of executing target object
     */
    public Object process() throws Exception{
        return method.invoke(target,args);
    }

    //Omit getter / setter

}

Adjust the interface

  • Interceptor.java
public interface Interceptor {

    /**
     *Specific interception processing
     */
    Object intercept(Invocation invocation) throws Exception;

}
  • Log implementation
public class MyLogInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Exception {
        System.out.println ("--- insert pre notification code --------";
        Object result = invocation.process();
        System.out.println ("--- insert post processing code --------";
        return result;
    }

}

Re implement the proxy class

public class MyInvocationHandler implements InvocationHandler {

    private Object target;

    private Interceptor interceptor;

    public MyInvocationHandler(Object target, Interceptor interceptor) {
        this.target = target;
        this.interceptor = interceptor;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Invocation invocation = new Invocation(target, method, args);
        //It still returns the result of the proxy class
        return interceptor.intercept(invocation);
    }

    public static Object wrap(Object target, Interceptor interceptor) {
        MyInvocationHandler targetProxy = new MyInvocationHandler(target, interceptor);
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                targetProxy);
    }

}

The most important thing is to construct the invocation and then execute the corresponding method.

test

  • code
public static void main(String[] args) {
    HelloService target = new HelloServiceImpl();
    Interceptor interceptor = new MyLogInterceptor();
    HelloService targetProxy = (HelloService) MyInvocationHandler.wrap(target, interceptor);
    targetProxy.sayHello();
}
  • journal
------Insert pre notification code-------------
sayHello......
------Insert post processing code-------------

Optimization 3: clear boundaries

In this way, the front and back interception can be realized, and the interceptor can obtain the intercepted object information.

However, the call of the test code looks very awkward. For the target class, you only need to know what interception is inserted to it.

Again, add a method to insert the target class in the interceptor.

realization

Interface adjustment

public interface Interceptor {

    /**
     *Specific interception processing
     *
     *The result of @ return method execution
     * @since 0.0.2
     */
    Object intercept(Invocation invocation) throws Exception;

    /**
     *Insert target class
     *
     *@ return agent
     * @since 0.0.2
     */
    Object plugin(Object target);

}

Realize the adjustment

It can be understood as adjusting static methods to object methods.

public class MyLogInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Exception {
        System.out.println ("--- insert pre notification code --------";
        Object result = invocation.process();
        System.out.println ("--- insert post processing code --------";
        return result;
    }

    @Override
    public Object plugin(Object target) {
        return MyInvocationHandler.wrap(target, this);
    }

}

test

  • code
public static void main(String[] args) {
    HelloService target = new HelloServiceImpl();
    Interceptor interceptor = new MyLogInterceptor();
    HelloService targetProxy = (HelloService) interceptor.plugin(target);
    targetProxy.sayHello();
}
  • journal
------Insert pre notification code-------------
sayHello......
------Insert post processing code-------------

Responsibility chain model

How to deal with multiple interceptors?

Test code

public static void main(String[] args) {
    HelloService target = new HelloServiceImpl();
    //1. Interceptor 1
    Interceptor interceptor = new MyLogInterceptor();
    target = (HelloService) interceptor.plugin(target);
    //2. Interceptor 2
    Interceptor interceptor2 = new MyTransactionInterceptor();
    target = (HelloService) interceptor2.plugin(target);
    //Call
    target.sayHello();
}

The implementation of mytransactioninterceptor is as follows:

public class MyTransactionInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Exception {
        System.out.println("------tx start-------------");
        Object result = invocation.process();
        System.out.println("------tx end-------------");
        return result;
    }

    @Override
    public Object plugin(Object target) {
        return MyInvocationHandler.wrap(target, this);
    }

}

The log is as follows:

------tx start-------------
------Insert pre notification code-------------
sayHello......
------Insert post processing code-------------
------tx end-------------

Of course, many partners have already thought of using the chain of responsibility model. Let’s take a look at the chain of responsibility model.

Responsibility chain model

Responsibility chain model

public class InterceptorChain {

    private List<Interceptor> interceptorList = new ArrayList<>();

    /**
     *Insert all interceptors
     */
    public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptorList) {
            target = interceptor.plugin(target);
        }
        return target;
    }

    public void addInterceptor(Interceptor interceptor) {
        interceptorList.add(interceptor);
    }
    /**
     *Returns a non modifiable collection that can only be added through the addinterceptor method
     *It's in your own hands
     */
    public List<Interceptor> getInterceptorList() {
        return Collections.unmodifiableList(interceptorList);
    }
}

test

public static void main(String[] args) {
    HelloService target = new HelloServiceImpl();

    Interceptor interceptor = new MyLogInterceptor();
    Interceptor interceptor2 = new MyTransactionInterceptor();
    InterceptorChain chain = new InterceptorChain();
    chain.addInterceptor(interceptor);
    chain.addInterceptor(interceptor2);

    target = (HelloService) chain.pluginAll(target);
    //Call
    target.sayHello();
}
  • journal
------tx start-------------
------Insert pre notification code-------------
sayHello......
------Insert post processing code-------------
------tx end-------------

Personal thinking

Can interceptors be improved?

In fact, I feel that we can change the angle here. For example, when defining the interceptor interface, it can be changed to:

In this way, the code can be implemented without writing the execution part, which is easier to implement and will not be forgotten.

public interface Interceptor {

    /**
     *Specific interception processing
     */
    void before(Invocation invacation);

    /**
     *Specific interception processing
     */
    void after(Invocation invacation);

}

However, there is also a disadvantage, that is, it is invisible to the part of process execution, which loses some flexibility.

Abstract implementation

For the plugin () method, the implementation is actually very fixed.

It should be invisible to the interface and can be directly put into the chain for unified processing.

Introduction of handwritten mybatis plug-in

Having said so much, if you understand, then the next plug-in implementation is a piece of cake.

It’s just a simple implementation of the above ideas.

Quick experience

config.xml

Plug in is introduced, and other parts are omitted.

<plugins>
    <plugin interceptor="com.github.houbb.mybatis.plugin.SimpleLogInterceptor"/>
</plugins>

SimpleLogInterceptor.java

We simply output input and output parameters.

public class SimpleLogInterceptor implements Interceptor{
    @Override
    public void before(Invocation invocation) {
        System.out.println("----param: " + Arrays.toString(invocation.getArgs()));
    }

    @Override
    public void after(Invocation invocation, Object result) {
        System.out.println("----result: " + result);
    }

}

Execution test method

The output log is as follows.

----param: [[email protected], MapperMethod{type='select', sql='select * from user where id = ?', methodName='selectById', resultType=class com.github.houbb.mybatis.domain.User, paramType=class java.lang.Long}, [Ljava.lang.Object;@67011281]
----result: User{id=1, name='luna', password='123456'}
User{id=1, name='luna', password='123456'}

Is it simple, then how to achieve it?

Core implementation

Interface definition

public interface Interceptor {

    /**
     *Front intercept
     *@ param invocation context
     * @since 0.0.2
     */
    void before(Invocation invocation);

    /**
     *Post intercept
     *@ param invocation context
     *@ param result
     * @since 0.0.2
     */
    void after(Invocation invocation, Object result);

}

Start plug-in

When opensession(), we start the plug-in:

public SqlSession openSession() {
    Executor executor = new SimpleExecutor();
    //1. Plug in
    InterceptorChain interceptorChain = new InterceptorChain();
    List<Interceptor> interceptors = config.getInterceptorList();
    interceptorChain.add(interceptors);
    executor = (Executor) interceptorChain.pluginAll(executor);

    //2. Create
    return new DefaultSqlSession(config, executor);
}

Here we see a chain of responsibility, which is implemented as follows.

Chain of responsibility

public class InterceptorChain {

    /**
     *Interceptor list
     * @since 0.0.2
     */
    private final List<Interceptor> interceptorList = new ArrayList<>();

    /**
     *Add interceptor
     *@ param interceptor
     * @return this
     * @since 0.0.2
     */
    public synchronized InterceptorChain add(Interceptor interceptor) {
        interceptorList.add(interceptor);

        return this;
    }

    /**
     *Add interceptor
     *Interceptor list @ interceptor list
     * @return this
     * @since 0.0.2
     */
    public synchronized InterceptorChain add(List<Interceptor> interceptorList) {
        for(Interceptor interceptor : interceptorList) {
            this.add(interceptor);
        }

        return this;
    }

    /**
     *Agent
     *@ param target target class
     *@ return result
     * @since 0.0.2
     */
    public Object pluginAll(Object target) {
        for(Interceptor interceptor : interceptorList) {
            target = DefaultInvocationHandler.proxy(target, interceptor);
        }

        return target;
    }

}

The implementation of defaultinvocationhandler is as follows:

/**
 *Default proxy implementation
 * @since 0.0.2
 */
public class DefaultInvocationHandler implements InvocationHandler {

    /**
     *Proxy class
     * @since 0.0.2
     */
    private final Object target;

    /**
     *Interceptor
     * @since 0.0.2
     */
    private final Interceptor interceptor;

    public DefaultInvocationHandler(Object target, Interceptor interceptor) {
        this.target = target;
        this.interceptor = interceptor;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Invocation invocation = new Invocation(target, method, args);

        interceptor.before(invocation);

        // invoke
        Object result = method.invoke(target, args);

        interceptor.after(invocation, result);

        return result;
    }

    /**
     *Build agent
     *@ param target target object
     *@ param interceptor
     *@ return agent
     * @since 0.0.2
     */
    public static Object proxy(Object target, Interceptor interceptor) {
        DefaultInvocationHandler targetProxy = new DefaultInvocationHandler(target, interceptor);
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                targetProxy);
    }

}

Summary

The implementation of this section is not difficult. It is difficult to understand the overall design concept of mybatis for plug-ins. The technical level is still dynamic agent, which combines the design mode of responsibility chain.

After learning this routine, in fact, many similar frameworks can be used for reference in our own implementation.

Extended reading

Handwritten mybatis from scratch (1) MVP version

Writing mybatis from scratch (2) detailed explanation of mybatis interceptor plug-in mechanism

reference material

Mybatis framework (8) — principle of mybatis plug-in (agent + chain of responsibility)