mybatis框架的插件機制

01java

什麼是mybatis插件機制spring


mybatis框架經過提供攔截器(interceptor)的方式,支持用戶擴展或者改變原有框架的功能,就是mybatis框架中的插件機制。數據庫


02apache

支持攔截的方法設計模式


Executor(update、query、commit、rollback等方法):Executor爲SQL執行器。session


StatementHandler(prepare、parameterize、batch、update、query等方法):StatementHandler負責操做 Statement 對象與數據庫進行交流,調用 ParameterHandler 和 ResultSetHandler對參數進行映射,對結果進行實體類的綁定。mybatis


ParameterHandler(getParameterObject、setParameters方法):ParameterHandler用於處理SQL語句中的參數。app


ResultSetHandler(handleResultSets、handleOutputParameters等方法):ResultSetHandler用於處理SQL執行完成之後返回的結果集映射。框架


03ide

插件機制應用場景


性能監控

對SQL語句執行的性能監控,能夠經過攔截Executor類的update、query等方法,用日誌記錄每一個方法執行的時間。真實生產環境能夠設置性能監控開關,以避免性能監控拖慢正常業務響應速度。


黑白名單功能

有些業務系統,生產環境單表數據量巨大,有些SQL語句是不容許在生產環境執行的。能夠經過攔截Executor類的update、 query等方法,對SQL語句進行攔截與黑白名單中的SQL語句或者關鍵詞進行對比,從而決定是否繼續執行SQL語句。


公共字段統一賦值

通常業務系統都會有建立者、建立時間、修改者、修改時間四個字段,對於這四個字段的賦值,實際上能夠在DAO層統一攔截處理。能夠用mybatis插件攔截Executor類的update方法,對相關參數進行統一賦值便可。


其它

mybatis擴展性仍是很強的,基於插件機制,基本上能夠控制SQL執行的各個階段,如執行階段、參數處理階段、語法構建階段、結果集處理階段,具體能夠根據項目來靈活運用。


04

SQL執行時長統計demo


首先,自定義SQLProcessTimeInterceptor,在Executor層面進行攔截,用於統計SQL執行時長。用戶自定義Interceptor除了繼承Interceptor接口外,還須要使用@Intercepts和@Signature兩個註解進行標識。@Intercepts註解指定了一個@Signature註解列表,每一個@Signature註解中都標識了須要攔截的方法信息,其中@Signature註解中的type屬性用於指定須要攔截的類型,method屬性用於指定須要攔截的方法,args屬性指定了被攔截方法的參數列表。因爲java有重載的概念,經過type、method、args三個屬性能夠標識出惟一的方法。



import lombok.extern.slf4j.Slf4j;import org.apache.ibatis.executor.Executor;import org.apache.ibatis.mapping.MappedStatement;import org.apache.ibatis.plugin.*;import org.apache.ibatis.session.ResultHandler;import org.apache.ibatis.session.RowBounds;@Slf4j@Intercepts({        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,                RowBounds.class, ResultHandler.class})})public class SqlProcessTimeInterceptor implements Interceptor {    @Override    public Object intercept(Invocation invocation) throws Throwable {        long start = System.currentTimeMillis();        log.info("start time :" + System.currentTimeMillis());
       Object result = invocation.proceed();        long end = System.currentTimeMillis();        log.info("end time :" + end);        log.info("process time is :" + (end - start) + "ms");        return result;    }
   @Override    public Object plugin(Object target) {        return Plugin.wrap(target, this);    }
   @Override    public void setProperties(Properties properties) {
   }}


Object intercept(Invocation invocation)是實現攔截邏輯的地方,內部要經過invocation.proceed()顯式地推動責任鏈前進,也就是調用下一個攔截器攔截目標方法。
Object plugin(Objecttarget)就是用當前這個攔截器生成對目標target的代理,實際是經過Plugin.wrap(target,this)來完成的,把目標target和攔截器this傳給了包裝函數。


setProperties(Properties properties)用於設置額外的參數,參數配置在攔截器的Properties節點裏,也能夠經過代碼的方式配置properties屬性。


而後,將自定義SQLProcessTimeInterceptor加入到配置中,供mybatis框架初始化的時候拉取。



import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;
@Configurationpublic class MybatisConfiguration {    @Bean    ConfigurationCustomizer mybatisConfigurationCustomizer() {        return new ConfigurationCustomizer() {            @Override            public void customize(org.apache.ibatis.session.Configuration configuration) {                configuration.addInterceptor(new SqlProcessTimeInterceptor());            }        };    }}


最後,執行單元測試查看是否攔截成功。








//單元測試@Testpublic void selectUserById() {   User user = userMapper.selectUserById(1L);   Assert.assertNotNull(user);}





//運行結果start time :1611127784286end time :1611127784703process time is :417ms


05

mybatis插件機制原理


因爲以上demo攔截的是Executor,如下原理分析基於Executor攔截。ParameterHandler、ResultHandler、ResultSetHandler、StatementHandler攔截過程原理相似。


一、將自定義的Interceptor配置進mybaits框架中,以便mybatis框架在初始化的時候將自定義Interceptor加入到Configuration.interceptorChain中。


配置分爲兩種方式:


一種爲xml配置文件的方式:Mybatis初始化的時候,會經過XMLConfigBuilder.pluginElement(XNode parent)對xml進行解析,而後將Interceptor加入到interceptorChain中。配置方式以下:








<!-- mybatis-config.xml --><plugins>  <plugin interceptor="自定義Interceptor類的全路徑">    <property name="propertyKey" value="propertyValue"/>  </plugin></plugins>


另一種方式是用@Configuration註解定義配置類:可替換xml配置文件,被註解的類內部包含有一個或多個被@Bean註解的方法,這些方法將會被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext類進行掃描,並用於構建bean定義,初始化Spring容器。本文demo採起的配置方式就是這種。


兩種方式最終都會調用到InterceptorChain.addInterceptor(Interceptor interceptor)方法。






//Configuration類 public void addInterceptor(Interceptor interceptor) {    interceptorChain.addInterceptor(interceptor); }





 //InterceptorChain類  public void addInterceptor(Interceptor interceptor) {    interceptors.add(interceptor);  }


二、下圖爲mybatis框架執行SQL操做的請求流轉過程圖。executor執行器會把SQL操做委託給statementHandler處理。statementHandler會調用ParamenterHander進行對SQL參數的一些處理,而後再調用statement執行SQL。執行完成之後會返回ResultSet結果集,而後ResultSetHandler會對結果集合進行處理(例如:返回結果和實體類的映射)。


圖片


三、Mybatis框架中會經過Configuration.newExecutor()生成executor對象,生成過程當中會經過pluinAll()方法生成Executor的代理對象,以達到將Interceptor中自定義功能織入到Executor中的目的。參見代碼:



//Configuration類  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {    executorType = executorType == null ? defaultExecutorType : executorType;    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;    Executor executor;    if (ExecutorType.BATCH == executorType) {      executor = new BatchExecutor(this, transaction);    } else if (ExecutorType.REUSE == executorType) {      executor = new ReuseExecutor(this, transaction);    } else {      executor = new SimpleExecutor(this, transaction);    }    if (cacheEnabled) {      executor = new CachingExecutor(executor);    }    //生成executor的代理對象    executor = (Executor) interceptorChain.pluginAll(executor);    return executor;  }


進入pluginAll方法會發現該方法會遍歷InterceptorChain的interceptor集合,並調用interceptor的plugin方法。注意生成代理對象從新賦值給target,若是有多個攔截器的話,生成的代理對象會被另外一個代理對象代理,從而造成一個代理鏈條。因此插件不宜定義過多,以避免嵌套層級太多影響程序性能。



//InterceptorChain類  public Object pluginAll(Object target) {  //遍歷interceptor集合    for (Interceptor interceptor : interceptors) {    //調用自定義Interceptor的plugin方法      target = interceptor.plugin(target);    }    return target;  }


自定義插件的interceptor.plugin方法通常考慮調用mybatis提供的工具方法:Plugin.wrap(),該類實現了InvocationHandler接口。當代理executor對象被調用時,會觸發plugin.invoke()方法,該方法是真正的Interceptor.intercept()被執行的地方。



//Plugin類 public static Object wrap(Object target, Interceptor interceptor) { //獲取自定義Interceptor的@Signature註解信息    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);    //獲取被代理對象類型    Class<?> type = target.getClass();    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);    if (interfaces.length > 0) {    //經過jdk動態代理生成代理對象,將自定義功能織入代理對象      return Proxy.newProxyInstance(          type.getClassLoader(),          interfaces,          new Plugin(target, interceptor, signatureMap));    }    return target;  }


getSignatureMap方法:首先會拿到攔截器這個類的 @Interceptors註解,而後拿到這個註解的屬性 @Signature註解集合,而後遍歷這個集合,遍歷的時候拿出 @Signature註解的type屬性(Class類型),而後根據這個type獲得帶有method屬性和args屬性的Method。因爲 @Interceptors註解的 @Signature屬性是一個屬性,因此最終會返回一個以type爲key,value爲Set的Map結構。



private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);    // issue #251    if (interceptsAnnotation == null) {      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());          }    Signature[] sigs = interceptsAnnotation.value();    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();    for (Signature sig : sigs) {      Set<Method> methods = signatureMap.get(sig.type());      if (methods == null) {        methods = new HashSet<Method>();        signatureMap.put(sig.type(), methods);      }      try {        Method method = sig.type().getMethod(sig.method(), sig.args());        methods.add(method);      } catch (NoSuchMethodException e) {        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);      }    }    return signatureMap;  }


Plugin.invoke()方法是真正自定義插件邏輯被調起的地方。



//Plugin類  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {    try {      Set<Method> methods = signatureMap.get(method.getDeclaringClass());      //判斷當前method方法是否在自定義類@Intercepts註解的@Signature列表中,若是當前方法須要被攔截,則執行Interceptor.intercept方法      if (methods != null && methods.contains(method)) {      //自定義Interceptor中附加功能被執行的地方        return interceptor.intercept(new Invocation(target, method, args));      }      //若是當前方法不須要被攔截,則直接被調用並返回      return method.invoke(target, args);    } catch (Exception e) {      throw ExceptionUtil.unwrapThrowable(e);    }  }


interceptor.intercept(new Invocation(target, method, args))中的參數Invocation對象對目標類、目標方法和方法參數進行了封裝。須要注意的是,自定義Interceptor中的intercept方法中不要忘記執行invocation.process()方法,不然整個責任鏈會中斷掉。



public class Invocation {
 private final Object target;  private final Method method;  private final Object[] args;  ......  .  .  ......  //processed方法,觸發被代理類目標方法的執行  public Object proceed() throws InvocationTargetException, IllegalAccessException {    return method.invoke(target, args);  }
}


至此mybatis插件機制的使用方式和運行機理介紹完成。


06

總結


本篇文章介紹了mybatis框架插件機制的應用場景和運行原理。插件模塊的實現思想以設計模式中的責任鏈模式和代理模式爲主,下降了對象之間的耦合,加強了系統的可擴展性,知足了開放封閉原則。可將插件思想靈活運用於平常研發工做中。

相關文章
相關標籤/搜索