首頁 > 軟體

在springboot中如何給mybatis加攔截器

2022-08-26 14:03:37

攔截器的作用就是我們可以攔截某些方法的呼叫,在目標方法前後加上我們自己邏輯

Mybatis攔截器設計的一個初衷是為了供使用者在某些時候可以實現自己的邏輯而不必去動Mybatis固有的邏輯。

mybatis 自定義攔截器

1、實現Interceptor 介面,並新增攔截註解 @Intercepts

2、組態檔中新增攔截器

1、實現Interceptor介面,並新增攔截註解 @Intercepts

mybatis 攔截器預設可攔截的型別只有四種

  • Executor:攔截執行器的方法。
  • ParameterHandler:攔截引數的處理。
  • ResultHandler:攔截結果集的處理。
  • StatementHandler:攔截Sql語法構建的處理。

對於我們的自定義攔截器必須使用 mybatis 提供的註解來指明我們要攔截的是四類中的哪一個類介面

具體規則如下:

a:Intercepts 攔截器:標識我的類是一個攔截器

b:Signature 署名:則是指明我們的攔截器需要攔截哪一個介面的哪一個方法

  • type 對應四類介面中的某一個,比如是 Executor
  • method 對應介面中的哪類方法,比如 Executor 的 update 方法
  • args 對應介面中的哪一個方法,比如 Executor 中 query 因為過載原因,方法有多個,args 就是指明引數型別,從而確定是哪一個方法
@Intercepts({
        @Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class}),
        @Signature(method = "query", type = StatementHandler.class, args = {Statement.class, ResultHandler.class})
})
public class MyInterceptor implements Interceptor {
    /**
     * 這個方法很好理解
     * 作用只有一個:我們不是攔截方法嗎,攔截之後我們要做什麼事情呢?
     *      這個方法裡面就是我們要做的事情
     *
     * 解釋這個方法前,我們一定要理解方法引數 {@link Invocation} 是個什麼鬼?
     * 1 我們知道,mybatis攔截器預設只能攔截四種型別 Executor、StatementHandler、ParameterHandler 和 ResultSetHandler
     * 2 不管是哪種代理,代理的目標物件就是我們要攔截物件,舉例說明:
     *      比如我們要攔截 {@link Executor#update(MappedStatement ms, Object parameter)} 方法,
     *      那麼 Invocation 就是這個物件,Invocation 裡面有三個引數 target method args
     *          target 就是 Executor
     *          method 就是 update
     *          args   就是 MappedStatement ms, Object parameter
     *
     *   如果還是不能理解,我再舉一個需求案例:看下面方法程式碼裡面的需求
     *
     *  該方法在執行時呼叫
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        /*
         * 需求:我們需要對所有更新操作前列印查詢語句的 sql 紀錄檔
         * 那我就可以讓我們的自定義攔截器 MyInterceptor 攔截 Executor 的 update 方法,在 update 執行前列印sql紀錄檔
         * 比如我們攔截點是 Executor 的 update 方法 :  int update(MappedStatement ms, Object parameter)
         *
         * 那當我們紀錄檔列印成功之後,我們是不是還需要呼叫這個query方法呢,如何如呼叫呢?
         * 所以就出現了 Invocation 物件,它這個時候其實就是一個 Executor,而且 method 對應的就是 query 方法,我們
         * 想要呼叫這個方法,只需要執行 invocation.proceed()
         */
        /* 因為我攔截的就是Executor,所以我可以強轉為 Executor,預設情況下,這個Executor 是個 SimpleExecutor */
        Executor executor = (Executor)invocation.getTarget();
        /*
         * Executor 的 update 方法裡面有一個引數 MappedStatement,它是包含了 sql 語句的,所以我獲取這個物件
         * 以下是虛擬碼,思路:
         * 1 通過反射從 Executor 物件中獲取 MappedStatement 物件
         * 2 從 MappedStatement 物件中獲取 SqlSource 物件
         * 3 然後從 SqlSource 物件中獲取獲取 BoundSql 物件
         * 4 最後通過 BoundSql#getSql 方法獲取 sql
         */
        MappedStatement mappedStatement = ReflectUtil.getMethodField(executor, MappedStatement.class);
        SqlSource sqlSource = ReflectUtil.getField(mappedStatement, SqlSource.class);
        BoundSql boundSql = sqlSource.getBoundSql(args);
        String sql = boundSql.getSql();
        logger.info(sql);
        /*
         * 現在紀錄檔已經列印,需要呼叫目標物件的方法完成 update 操作
         * 我們直接呼叫 invocation.proceed() 方法
         * 進入原始碼其實就是一個常見的反射呼叫 method.invoke(target, args)
         * target 對應 Executor物件
         * method 對應 Executor的update方法
         * args   對應 Executor的update方法的引數
         */
        return invocation.proceed();
    }
    /**
     * 這個方法也很好理解
     * 作用就只有一個:那就是Mybatis在建立攔截器代理時候會判斷一次,當前這個類 MyInterceptor 到底需不需要生成一個代理進行攔截,
     * 如果需要攔截,就生成一個代理物件,這個代理就是一個 {@link Plugin},它實現了jdk的動態代理介面 {@link InvocationHandler},
     * 如果不需要代理,則直接返回目標物件本身
     *
     * Mybatis為什麼會判斷一次是否需要代理呢?
     * 預設情況下,Mybatis只能攔截四種型別的介面:Executor、StatementHandler、ParameterHandler 和 ResultSetHandler
     * 通過 {@link Intercepts} 和 {@link Signature} 兩個註解共同完成
     * 試想一下,如果我們開發人員在自定義攔截器上沒有指明型別,或者隨便寫一個攔截點,比如Object,那Mybatis瘋了,難道所有物件都去攔截
     * 所以Mybatis會做一次判斷,攔截點看看是不是這四個介面裡面的方法,不是則不攔截,直接返回目標物件,如果是則需要生成一個代理
     *
     *  該方法在 mybatis 載入核心組態檔時被呼叫
     */
    @Override
    public Object plugin(Object target) {
        /*
         * 看了這個方法註釋,就應該理解,這裡的邏輯只有一個,就是讓mybatis判斷,要不要進行攔截,然後做出決定是否生成一個代理
         *
         * 下面程式碼什麼鬼,就這一句就搞定了?
         * Mybatis判斷依據是利用反射,獲取這個攔截器 MyInterceptor 的註解 Intercepts和Signature,然後解析裡面的值,
         * 1 先是判斷要攔截的物件是四個型別中 Executor、StatementHandler、ParameterHandler、 ResultSetHandler 的哪一個
         * 2 然後根據方法名稱和引數(因為有過載)判斷對哪一個方法進行攔截  Note:mybatis可以攔截這四個介面裡面的任一一個方法
         * 3 做出決定,是返回一個物件呢還是返回目標物件本身(目標物件本身就是四個介面的實現類,我們攔截的就是這四個型別)
         *
         * 好了,理解邏輯我們寫程式碼吧~~~  What !!! 要使用反射,然後解析註解,然後根據引數型別,最後還要生成一個代理物件
         * 我一個小白我怎麼會這麼高大上的程式碼嘛,怎麼辦?
         *
         * 那就是使用下面這句程式碼吧  哈哈
         * mybatis 早就考慮了這裡的複雜度,所以提供這個靜態方法來實現上面的邏輯
         */
        return Plugin.wrap(target, this);
    }
    /**
     * 這個方法最好理解,如果我們攔截器需要用到一些變數引數,而且這個引數是支援可設定的,
     *  類似Spring中的@Value("${}")從application.properties檔案獲取
     * 這個時候我們就可以使用這個方法
     *
     * 如何使用?
     * 只需要在 mybatis 組態檔中加入類似如下設定,然後 {@link Interceptor#setProperties(Properties)} 就可以獲取引數
     *      <plugin interceptor="liu.york.mybatis.study.plugin.MyInterceptor">
     *           <property name="username" value="LiuYork"/>
     *           <property name="password" value="123456"/>
     *      </plugin>
     *      方法中獲取引數:properties.getProperty("username");
     *
     * 問題:為什麼要存在這個方法呢,比如直接使用 @Value("${}") 獲取不就得了?
     * 原因是 mybatis 框架本身就是一個可以獨立使用的框架,沒有像 Spring 這種做了很多依賴注入的功能
     *
     *  該方法在 mybatis 載入核心組態檔時被呼叫 
     */
    @Override
    public void setProperties(Properties properties) {
        String username = properties.getProperty("username");
        String password = properties.getProperty("password");
        // TODO: 2019/2/28  業務邏輯處理...
    }
}

三個核心方法都加了詳細的註釋,而且結合案例需求說明問題

那麼多文字不想行看,沒關係有概括

總結:

1.在mybatis中可被攔截的型別有四種(按照攔截順序)

  • Executor:攔截執行器的方法。
  • ParameterHandler:攔截引數的處理。
  • ResultHandler:攔截結果集的處理。
  • StatementHandler:攔截Sql語法構建的處理。

2.各個引數的含義

  • @Intercepts:標識該類是一個攔截器;
  • @Signature:指明自定義攔截器需要攔截哪一個型別,哪一個方法;

2.1 type:對應四種型別中的一種;

2.2 method:對應介面中的哪個方法;

2.3 args:對應哪一個方法引數型別(因為可能存在過載方法);

接下來我們看看 Plugin 類

package org.apache.ibatis.plugin;
/**
 * Plugin 類其實就是一個代理類,因為它實現了jdk動態代理介面 InvocationHandler
 * 我們核心只需要關注兩個方法
 * wrap:
 *      如果看懂了程式碼案例1的例子,那麼這個方法很理解,這個方法就是 mybatis 提供給開發人員使用的一個工具類方法,
 *      目的就是幫助開發人員省略掉 反射解析註解 Intercepts 和 Signature,有興趣的可以去看看原始碼 Plugin#getSignatureMap 方法
 *
 * invoke:
 *      這個方法就是根據 wrap 方法的解析結果,判斷當前攔截器是否需要進行攔截,
 *      如果需要攔截:將 目標物件+目標方法+目標引數 封裝成一個 Invocation 物件,給我們自定義的攔截器 MyInterceptor 的 intercept 方法
 *                   這個時候就剛好對應上了上面案例1中對 intercept 方法的解釋了,它就是我們要處理自己邏輯的方法,
 *                   處理好了之後是否需要呼叫目標物件的方法,比如上面說的 列印了sql語句,是否還要查詢資料庫呢?答案是肯定的
 *      如果不需要攔截:則直接呼叫目標物件的方法
 *                   比如直接呼叫 Executor 的 update 方法進行更新資料庫
 *
 */
class Plugin implements InvocationHandler {
    public static Object wrap(Object target, Interceptor interceptor) {
        // 省略
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 省略
    }
}

貼一段網上的通用解釋吧:

  • Plugin的wrap方法,它根據當前的Interceptor上面的註解定義哪些介面需要攔截,然後判斷當前目標物件是否有實現對應需要攔截的介面,如果沒有則返回目標物件本身,如果有則返回一個代理物件。
  • 而這個代理物件的InvocationHandler正是一個Plugin。所以當目標物件在執行介面方法時,如果是通過代理物件執行的,則會呼叫對應InvocationHandler的invoke方法,也就是Plugin的invoke方法。
  • 所以接著我們來看一下該invoke方法的內容。
  • 這裡invoke方法的邏輯是:如果當前執行的方法是定義好的需要攔截的方法,則把目標物件、要執行的方法以及方法引數封裝成一個Invocation物件,再把封裝好的Invocation作為引數傳遞給當前攔截器的intercept方法。
  • 如果不需要攔截,則直接呼叫當前的方法。
  • Invocation中定義了定義了一個proceed方法,其邏輯就是呼叫當前方法,所以如果在intercept中需要繼續呼叫當前方法的話可以呼叫invocation的procced方法。

這就是Mybatis中實現Interceptor攔截的一個思想

2、在組態檔中新增攔截器

在springboot中要給mybatis加上這個攔截器,有三種方法,前兩種方法在啟動專案時不會自動呼叫自定義攔截器的setProperties方法。

攔截器順序

1、不同攔截器順序

Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler

2、對於同一個型別的攔截器的不同物件攔截順序:

在 mybatis 核心組態檔根據設定的位置,攔截順序是 從上往下

(1)第一種

直接給自定義攔截器新增一個 @Component註解,當呼叫sql時結果如下,可以看到攔截器生效了,但是啟動時候並沒有自動呼叫setProperties方法。

@Component
@Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }) })
public class MybatisInterceptor implements Interceptor {
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		//業務程式碼
	}
	@Override
	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}
	@Override
	public void setProperties(Properties properties) {
		// TODO Auto-generated method stub
	}
}

(2)第二種

在設定類裡新增攔截器,這種方法結果同上,也不會自動呼叫setProperties方法。

@Configuration
public class MybatisConfig {
    @Bean
    public ConfigurationCustomizer mybatisConfigurationCustomizer() {
        return new ConfigurationCustomizer() {
            @Override
            public void customize(Configuration configuration) {
                configuration.addInterceptor(new MybatisInterceptor());
            }
        };
    }
}

(3)第三種

這種方法就是跟以前的設定方法類似,在yml組態檔中指定mybatis的xml組態檔,

注意:config-location屬性和configuration屬性不能同時指定

mybatis:
  config-location: classpath:mybatis.xml
  type-aliases-package: me.zingon.pagehelper.model
  mapper-locations: classpath:mapper/*.xml

mybatis.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <typeAliases>
        <package name="me.zingon.pacargle.model"/>
    </typeAliases>
    <plugins>
        <plugin interceptor="me.zingon.pagehelper.interceptor.MyPageInterceptor"> 
            <property name="dialect" value="oracle"/>
        </plugin>
    </plugins>
</configuration>

可以看到,在啟動專案的時候setProperties被自動呼叫了

總結:前兩種方法可以在初始化自定義攔截器的時候通過 @Value 註解直接初始化需要的引數。

以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。 


IT145.com E-mail:sddin#qq.com