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

2020-09-04 老馬嘯西風

前景回顧

第一節 從零開始手寫 mybatis(一)MVP 版本 中我們實現了一個最基本的可以運行的 mybatis。

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

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

下面我們一起來體驗一下這有趣的靈魂帶來的痛苦與快樂~

插件的作用

在實際開發過程中,我們經常使用的Mybaits插件就是分頁插件了,通過分頁插件我們可以在不用寫count語句和limit的情況下就可以獲取分頁後的數據,給我們開發帶來很大

的便利。除了分頁,插件使用場景主要還有更新資料庫的通用欄位,分庫分表,加解密等的處理。

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



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

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

所以在講Mybatis攔截器之前我們先說說JDK動態代理+責任鏈設計模式。

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(&34;); } } /** * 自定義代理類需要實現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(&34;); //執行相應的目標方法 Object rs = method.invoke(target,args); System.out.println(&34;); 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(&34;); }}

public class TransactionInterceptor implements Interceptor{ @Override public void intercept() { System.out.println(&34;); }}

實現代理

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(&34;); Object result = invocation.process(); System.out.println(&34;); 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(&34;); Object result = invocation.process(); System.out.println(&34;); 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(&34;); Object result = invocation.process(); System.out.println(&34;); 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=&34;/></plugins>

SimpleLogInterceptor.java

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

public class SimpleLogInterceptor implements Interceptor{ @Override public void before(Invocation invocation) { System.out.println(&34; + Arrays.toString(invocation.getArgs())); } @Override public void after(Invocation invocation, Object result) { System.out.println(&34; + result); }}

執行測試方法

輸出日誌如下。

----param: [com.github.houbb.mybatis.config.impl.XmlConfig@3b76982e, MapperMethod{type=&39;, sql=&39;, methodName=&39;, resultType=class com.github.houbb.mybatis.domain.User, paramType=class java.lang.Long}, [Ljava.lang.Object;@67011281]----result: User{id=1, name=&39;, password=&39;}User{id=1, name=&39;, password=&39;}

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

核心實現

接口定義

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(四)- mybatis 事務管理機制詳解
    前景回顧第一節 從零開始手寫 mybatis(一)MVP 版本 中我們實現了一個最基本的可以運行的 mybatis。第二節 從零開始手寫 mybatis(二)mybatis interceptor 插件機制詳解第三節 從零開始手寫 mybatis(三)jdbc pool 從零實現資料庫連接池本節我們一起來學習一下 mybatis 中的事務管理。
  • 從零開始手寫 mybatis (三)jdbc pool 從零實現資料庫連接池
    前景回顧第一節 從零開始手寫 mybatis(一)MVP 版本 中我們實現了一個最基本的可以運行的 mybatis。第二節 從零開始手寫 mybatis(二)mybatis interceptor 插件機制詳解本節我們一起來看一下如何實現一個資料庫連接池。為什麼需要連接池?資料庫連接的創建是非常耗時的一個操作,在高並發的場景,如果每次對於資料庫的訪問都重新創建的話,成本太高。
  • mybatis:自定義實現攔截器插件Interceptor
    @Override public Object plugin(Object target) { //判斷是否攔截這個類型對象(根據@Intercepts註解決定),然後決定是返回一個代理對象還是返回原對象。
  • Mybatis第四講 插件開發
    = (Interceptor) resolveClass(interceptor).newInstance(); interceptorInstance.setProperties(properties);configuration.addInterceptor(interceptorInstance);配置對象Configuration的添加插件方法
  • 建議收藏,mybatis插件原理詳解
    上次發文說到了如何集成分頁插件MyBatis插件原理分析,看完感覺自己better了,今天我們接著來聊mybatis插件的原理。(interceptors); }}上面的for循環代表了只要是插件,都會以責任鏈的方式逐一執行(別指望它能跳過某個節點),所謂插件,其實就類似於攔截器。
  • 從零開始手寫 mybatis(一)MVP 版本
    (這是官網解釋)MyBatis 運行原理手寫 mybatis其實整體流程就是這麼簡單,我們來一起實現一個簡單版本的 mybatis。創作目的(1)深入學習 mybatis 的原理一千個讀者就有一千個哈姆雷特,一千個作者就有一千個莎士比亞。
  • 開發MyBatis自定義Interceptor攔截器的使用
    二、攔截器註解 mybatis實現自定義攔截器實現過程: (1)實現org.apache.ibatis.plugin.Interceptor接口; (2)添加攔截器註解:org.apache.ibatis.plugin.Intercepts; (3) 配置文件中添加攔截器。
  • springBoot中兩種配置mybatis分頁插件方式
    pageHelper}); return bean; } @Bean public PageHelper pageHelper() { PageHelper pageHelper = new PageHelper(); Properties properties = new Properties(); properties.setProperty("dialect", env.getProperty("mybatis.dialect
  • mybatis 分頁插件 mybatis-plus-page
    mybatis-plus-page 詳細介紹mybatis分頁插件MicroPageInterceptor 特點:1, 支持mysql和oracle分頁2, 不必在xml編寫統計count的sql3, 使用RowBounds子類PageInfo
  • 不可不知的MyBatis面試點
    selectList();} ProductMapper在spring中的實例化這主要得益於Spring的擴展機制(如果大家對spring的擴展機制還不太了解的話,可以先看一下),在mybatis中MapperScannerConfigurer這個類實現了BeanDefinitionRegistryPostProcessor的postProcessBeanDefinitionRegistry,在postProcessBeanDefinitionRegistry方法中掃描指定路徑下的Mapper接口文件,從而將這些接口轉換為
  • 詳解mybatis和Mybatis-Plus區別
    Mybatis-Plus(以下簡稱MP)是為簡化開發、提高開發效率而生,但它也提供了一些很有意思的插件,比如SQL性能監控、樂觀鎖、執行分析等。-- 配置 Mybatis 配置文件(可無) --> <property name=&34; value=&34; /> <!-- 配置包別名,支持通配符 * 或者 ; 分割 --> <property name=&34; value=&34; /> <!
  • Mybatis中別名、插件與數據源配置
    設置別名別名也是mybatis中最重要的配置之一,可以通過很簡單的一個字符串來代替一個Class,它可以減少很多的重複代碼,別名類的源碼如下圖:在mybatis的配置文件中也可以設置別名,別名的配置及解析源碼如下圖:
  • Mybatis框架如何使用分頁插件呢?
    分頁插件使用的方式修改 pom 文件,添加分頁 jar 包依賴修改 mybatis.xml 文件UserDao 接口,UserMapper.xml 添加對應方法與實現 sql對應 UserService 接口添加分頁查詢方法測試分頁效果
  • Mybatis 逆向工程使用姿勢不對文檔全被清空,一怒之下寫了個插件……
    這鍋只能自己背了你用過 Mybatis 逆向工程(mybatis-generator-maven-plugin)生成相關文件嗎?就像這樣的:可以看到逆向工程幫我們生成了實體類、Mapper 接口和 Mapper.xml。用起來真的很方便,我用了好幾年了,但是前段時間翻車了。
  • 通關 MyBatis 實戰 (下篇)
    各位志同道合的朋友們大家好,我是一個一直在一線網際網路踩坑十餘年的編碼愛好者,現在將我們的各種經驗以及架構實戰分享出來,如果大家喜歡,就關注我,一起將技術學深學透,我會每一篇分享結束都會預告下一專題MyBatis 最初的設計是基於 XML 配置文件的,但隨著 Java 的發展(Java
  • 徹底搞懂MyBatis插件原理及PageHelper原理
    package com.lonelyWolf.mybatis.plugin;import org.apache.ibatis.executor.Executor;import org.apache.ibatis.mapping.MappedStatement;import org.apache.ibatis.plugin.
  • 如何讓 Mybatis 自動生成代碼,提高開發效率
    來自:cnblogs.com/homejim/p/9782403.html | 責編:樂樂  圖 / 圖蟲  往日回顧:  正文  在使用mybatis過程中, 當手寫JavaBean和XML\src\main\java">javaClientGenerator>  context>generatorConfiguration>  需要改一些內容:  本地資料庫驅動程序jar包的全路徑(必須要改)。
  • 從零開始學SpringBoot之MyBatis-註解
    本節概述:(1) 關於mybatis(2) 註解思想(3) 新建項目並添加依賴項包(4) 創建啟動類App.java(5) 編寫實體類演示(6) 寫映射接口DemoMapper(7) 編寫服務類DemoService(8) 寫控制類DemoController(9) 配置資料庫連接池(10) 測試讓我們來看看這個部分:(1) 關於mybatis
  • 徹底搞懂MyBatis插件+PageHelper原理
    method:被攔截的方法名args:標註方法的參數類型2、我們還需要在mybatis-config中配置好插件。<plugins> <plugin interceptor="com.lonelyWolf.mybatis.plugin.MyPlugin"> <property name="userName" value="張三"/> &
  • mybatis-plus思維導圖,讓mybatis-plus不再難懂
    但mybatis有個讓我比較頭疼的一個問題是sql工作量很大,尤其是欄位多的時候。雖然說單表的增刪改查操作可以通過mybatis generator工具來生成(或者自己寫模板工具生成),但項目開發的過程中總免不了要新添加新欄位,這些工具就幫不了我了,我得把新欄位寫到原來的所有增刪改查的sql中。這是個痛苦的過程,特別是當你重複了很多次之後。