從零開始手寫 mybatis(二)mybatis interceptor 插件機制詳解

前景回顧

第一節 從零開始手寫 mybatis(一)MVP 版本 中咱們實現了一個最基本的能夠運行的 mybatis。html

常言道,萬事開頭難,而後中間難。java

mybatis 的插件機制是 mybatis 除卻動態代理以外的第二大靈魂。git

下面咱們一塊兒來體驗一下這有趣的靈魂帶來的痛苦與快樂~github

插件的做用

在實際開發過程當中,咱們常用的Mybaits插件就是分頁插件了,經過分頁插件咱們能夠在不用寫count語句和limit的狀況下就能夠獲取分頁後的數據,給咱們開發帶來很大sql

的便利。除了分頁,插件使用場景主要還有更新數據庫的通用字段,分庫分表,加解密等的處理。數據庫

這篇博客主要講Mybatis插件原理,下一篇博客會設計一個Mybatis插件實現的功能就是每當新增數據的時候不用數據庫自增ID而是經過該插件生成雪花ID,做爲每條數據的主鍵。設計模式

image

JDK動態代理+責任鏈設計模式

Mybatis的插件其實就是個攔截器功能。它利用JDK動態代理和責任鏈設計模式的綜合運用。採用責任鏈模式,經過動態代理組織多個攔截器,經過這些攔截器你能夠作一些你想作的事。mybatis

因此在講Mybatis攔截器以前咱們先說說JDK動態代理+責任鏈設計模式。app

JDK 動態代理案例

package com.github.houbb.mybatis.plugin;

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

public class JdkDynamicProxy {

    /**
     * 一個接口
     */
    public interface HelloService{
        void sayHello();
    }

    /**
     * 目標類實現接口
     */
    static class HelloServiceImpl implements HelloService{

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

    }

    /**
     * 自定義代理類須要實現InvocationHandler接口
     */
    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("------插入前置通知代碼-------------");
            //執行相應的目標方法
            Object rs = method.invoke(target,args);
            System.out.println("------插入後置處理代碼-------------");
            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();
    }

}
  • 輸出
------插入前置通知代碼-------------
sayHello......
------插入後置處理代碼-------------

優化1:面向對象

上面代理的功能是實現了,可是有個很明顯的缺陷,就是 HelloInvocationHandler 是動態代理類,也能夠理解成是個工具類,咱們不可能會把業務代碼寫到寫到到invoke方法裏,框架

不符合面向對象的思想,能夠抽象一下處理。

定義接口

能夠設計一個Interceptor接口,須要作什麼攔截處理實現接口就好了。

public interface Interceptor {

    /**
     * 具體攔截處理
     */
    void intercept();

}

實現接口

public class LogInterceptor implements Interceptor{

    @Override
    public void intercept() {
        System.out.println("------插入前置通知代碼-------------");
    }

}

public class TransactionInterceptor implements Interceptor{

    @Override
    public void intercept() {
        System.out.println("------插入後置處理代碼-------------");
    }

}

實現代理

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 {
        //處理多個攔截器
        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);
    }

}

測試驗證

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();
}
  • 日誌
------插入前置通知代碼-------------
------插入後置處理代碼-------------
sayHello......

這裏有一個很明顯的問題,全部的攔截都在方法執行前被處理了。

優化 2:靈活指定先後

上面的動態代理確實能夠把代理類中的業務邏輯抽離出來,可是咱們注意到,只有前置代理,沒法作到先後代理,因此還須要在優化下。

因此須要作更一步的抽象,

把攔截對象信息進行封裝,做爲攔截器攔截方法的參數,把攔截目標對象真正的執行方法放到Interceptor中完成,這樣就能夠實現先後攔截,而且還能對攔截對象的參數等作修改。

實現思路

代理類上下文

設計一個 Invocation 對象。

public class Invocation {

    /**
     * 目標對象
     */
    private Object target;
    /**
     * 執行的方法
     */
    private Method method;
    /**
     * 方法的參數
     */
    private Object[] args;

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

    /**
     * 執行目標對象的方法
     */
    public Object process() throws Exception{
        return method.invoke(target,args);
    }

    // 省略 Getter/Setter

}

調整接口

  • Interceptor.java
public interface Interceptor {

    /**
     * 具體攔截處理
     */
    Object intercept(Invocation invocation) throws Exception;

}
  • 日誌實現
public class MyLogInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Exception {
        System.out.println("------插入前置通知代碼-------------");
        Object result = invocation.process();
        System.out.println("------插入後置處理代碼-------------");
        return result;
    }

}

從新實現代理類

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);
        // 返回的依然是代理類的結果
        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);
    }

}

最核心的就在於構建了 invocation,而後執行對應的方法。

測試

  • 代碼
public static void main(String[] args) {
    HelloService target = new HelloServiceImpl();
    Interceptor interceptor = new MyLogInterceptor();
    HelloService targetProxy = (HelloService) MyInvocationHandler.wrap(target, interceptor);
    targetProxy.sayHello();
}
  • 日誌
------插入前置通知代碼-------------
sayHello......
------插入後置處理代碼-------------

優化 3:劃清界限

上面這樣就能實現先後攔截,而且攔截器能獲取攔截對象信息。

可是測試代碼的這樣調用看着很彆扭,對應目標類來講,只須要了解對他插入了什麼攔截就好。

再修改一下,在攔截器增長一個插入目標類的方法。

實現

接口調整

public interface Interceptor {

    /**
     * 具體攔截處理
     *
     * @return 方法執行的結果
     * @since 0.0.2
     */
    Object intercept(Invocation invocation) throws Exception;

    /**
     * 插入目標類
     *
     * @return 代理
     * @since 0.0.2
     */
    Object plugin(Object target);

}

實現調整

能夠理解爲把靜態方法調整爲對象方法。

public class MyLogInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Exception {
        System.out.println("------插入前置通知代碼-------------");
        Object result = invocation.process();
        System.out.println("------插入後置處理代碼-------------");
        return result;
    }

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

}

測試

  • 代碼
public static void main(String[] args) {
    HelloService target = new HelloServiceImpl();
    Interceptor interceptor = new MyLogInterceptor();
    HelloService targetProxy = (HelloService) interceptor.plugin(target);
    targetProxy.sayHello();
}
  • 日誌
------插入前置通知代碼-------------
sayHello......
------插入後置處理代碼-------------

責任鏈模式

多個攔截器如何處理?

測試代碼

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

其中 MyTransactionInterceptor 實現以下:

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

}

日誌以下:

------tx start-------------
------插入前置通知代碼-------------
sayHello......
------插入後置處理代碼-------------
------tx end-------------

固然不少小夥伴看到這裏其實已經想到使用責任鏈模式,下面咱們一塊兒來看一下責任鏈模式。

責任鏈模式

責任鏈模式

public class InterceptorChain {

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

    /**
     * 插入全部攔截器
     */
    public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptorList) {
            target = interceptor.plugin(target);
        }
        return target;
    }

    public void addInterceptor(Interceptor interceptor) {
        interceptorList.add(interceptor);
    }
    /**
     * 返回一個不可修改集合,只能經過addInterceptor方法添加
     * 這樣控制權就在本身手裏
     */
    public List<Interceptor> getInterceptorList() {
        return Collections.unmodifiableList(interceptorList);
    }
}

測試

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);
    // 調用
    target.sayHello();
}
  • 日誌
------tx start-------------
------插入前置通知代碼-------------
sayHello......
------插入後置處理代碼-------------
------tx end-------------

我的的思考

攔截器是否能夠改進?

實際上我的感受這裏能夠換一種角度,好比定義攔截器接口時,改成:

這樣能夠代碼中能夠不用寫執行的部分,實現起來更加簡單,也不會忘記。

public interface Interceptor {

    /**
     * 具體攔截處理
     */
    void before(Invocation invacation);

    /**
     * 具體攔截處理
     */
    void after(Invocation invacation);

}

不過這樣也有一個缺點,那就是對於 process 執行的部分不可見,喪失了一部分靈活性。

抽象實現

對於 plugin() 這個方法,實際上實現很是固定。

應該對於接口不可見,直接放在 chain 中統一處理便可。

手寫 mybatis 引入插件

說了這麼多,若是你理解以後,那麼接下來的插件實現部分就是小菜一碟。

只是將上面的思想作一個簡單的實現而已。

快速體驗

config.xml

引入插件,其餘部分省略。

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

SimpleLogInterceptor.java

咱們就是簡單的輸出一下入參和出參。

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

}

執行測試方法

輸出日誌以下。

----param: [com.github.houbb.mybatis.config.impl.XmlConfig@3b76982e, 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'}

是否是灰常的簡單,那麼是怎麼實現的呢?

核心實現

接口定義

public interface Interceptor {

    /**
     * 前置攔截
     * @param invocation 上下文
     * @since 0.0.2
     */
    void before(Invocation invocation);

    /**
     * 後置攔截
     * @param invocation 上下文
     * @param result 執行結果
     * @since 0.0.2
     */
    void after(Invocation invocation, Object result);

}

啓動插件

在 openSession() 的時候,咱們啓動插件:

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

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

這裏咱們就看到了一個責任鏈,實現以下。

責任鏈

public class InterceptorChain {

    /**
     * 攔截器列表
     * @since 0.0.2
     */
    private final List<Interceptor> interceptorList = new ArrayList<>();

    /**
     * 添加攔截器
     * @param interceptor 攔截器
     * @return this
     * @since 0.0.2
     */
    public synchronized InterceptorChain add(Interceptor interceptor) {
        interceptorList.add(interceptor);

        return this;
    }

    /**
     * 添加攔截器
     * @param interceptorList 攔截器列表
     * @return this
     * @since 0.0.2
     */
    public synchronized InterceptorChain add(List<Interceptor> interceptorList) {
        for(Interceptor interceptor : interceptorList) {
            this.add(interceptor);
        }

        return this;
    }

    /**
     * 代理全部
     * @param target 目標類
     * @return 結果
     * @since 0.0.2
     */
    public Object pluginAll(Object target) {
        for(Interceptor interceptor : interceptorList) {
            target = DefaultInvocationHandler.proxy(target, interceptor);
        }

        return target;
    }

}

其中的 DefaultInvocationHandler 實現以下:

/**
 * 默認的代理實現
 * @since 0.0.2
 */
public class DefaultInvocationHandler implements InvocationHandler {

    /**
     * 代理類
     * @since 0.0.2
     */
    private final Object target;

    /**
     * 攔截器
     * @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;
    }

    /**
     * 構建代理
     * @param target 目標對象
     * @param interceptor 攔截器
     * @return 代理
     * @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);
    }

}

小結

本節的實現並不難,難在要理解 mybatis 總體對於插件的設計理念,技術層面仍是動態代理,結合了責任鏈的設計模式。

這種套路學會以後,其實不少相似的框架,咱們本身在實現的時候均可以借鑑這種思想。

拓展閱讀

從零開始手寫 mybatis(一)MVP 版本

image

參考資料

Mybatis框架(8)---Mybatis插件原理(代理+責任鏈)

相關文章
相關標籤/搜索