1. 程式人生 > >Mybatis攔截器原始碼深度解析

Mybatis攔截器原始碼深度解析

目錄:

一. 建立攔截器鏈
1. 建立物件
2. 建立配置檔案
3. 載入攔截器鏈
二. 方法呼叫解析
1. 對請求物件進行攔截器包裝
2. 執行呼叫
三. 小結

Mybatis攔截器 可以幫助我們在執行sql語句過程中增加外掛以實現一些通用的邏輯,比如對查詢sql分頁、資料許可權處理等。
允許使用外掛攔截的方法呼叫包括:

- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)

各方法的具體作用可以通過 Mybatis之sqlSession呼叫鏈分析 進行了解。

方法呼叫時載入攔截器鏈的總體時序圖如下:

一. 建立攔截器鏈

1. 建立物件

建立攔截器物件,對Executor介面的實現類上的update方法呼叫進行攔截:

// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  public Object intercept(Invocation invocation) throws Throwable {
    return invocation.proceed();
  }
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  public void setProperties(Properties properties) {
  }
}

Mybaits中通過註解Intercepts來設定當前攔截器是否對被攔截的請求進行處理,例子中的註解指定對Executor的所有update方法進行攔截處理。

  • Intercepts
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
  Signature[] value();
}
  • Signature
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  Class<?> type();

  String method();

  Class<?>[] args();
}

對註解的解析在Plugin類中。

2. 建立配置檔案

可以通過XMl和註解兩種方式進行配置,通過解析配置建立攔截器鏈。

  • xml配置:
<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>
  • 註解配置:
@Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }

3. 載入攔截器鏈

以XML配置好攔截器好,從XML解析原始碼中,可以看到通過xml標籤屬性 interceptor 把攔截器加入 Configuration的 InterceptorChain中,

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

二. 方法呼叫解析

之前我們配置了一個對Executor的update方法進行攔截的外掛,那麼看下具體的執行過程。在呼叫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進行包裝,因為使用了JDK的動態代理,所以返回物件必須為介面 
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

1. 對請求物件進行攔截器包裝

通過InterceptorChain的pluginAll方法,對interceptors集合迴圈,依次對target(也就是上面傳入的executor物件)進行代理:

public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      // target通過攔截器鏈,迴圈迭代代理
      target = interceptor.plugin(target);
    }
    return target;
  }

自定義的攔截器需要實現Interceptor實現的plugin方法,該方法用來給target增加代理,推薦直接呼叫Plugin.wrap(target, this)方法(這裡將每一個代理物件的建立放在Plugin類中的靜態方法,但是每新增一個外掛都需要寫這個方法)

  @Override
  public Object plugin(Object target) {
    // 注意第二個引數 為this,通過回撥自己,將引數傳遞給Plugin物件,plugin代理物件執行時,如果符合條件,將回調target的intercept,參見本文2小節
    return Plugin.wrap(target, this);
  }

Plugin實現了InvocationHandler介面,其類圖如下:

Plugin的wrap方法用來建立target的代理物件:

// 引數interceptor為 ExamplePlugin類物件 
public static Object wrap(Object target, Interceptor interceptor) {
    // 1. 獲取註解中的配置,class對應Signature中的type屬性,Set<Method>對應Signature中的method屬性
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // 2. 獲取被代理物件的class類物件,這裡為Executor的介面實現類
    Class<?> type = target.getClass();
    // 3. 獲取符合物件target介面的攔截器(這裡為Executor.class)
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 如果註解包含目標物件介面,對Plugin物件進行代理,返回的Plugin代理物件,呼叫目標方法時會進行Pluing的invoke()方法
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          // 代理物件傳入引數包括 目標類、該攔截器物件、註解配置解析Map
          new Plugin(target, interceptor, signatureMap));
    }
    // 如果該外掛註解中TYPE值不包含目標物件介面,則不處理,直接返回目標物件
    return target;
  }

其中getAllInterFaces方法用來判斷判斷註解中是否包含目標物件介面:

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<>();
    while (type != null) {
      // 如果傳入Executor的實現類,那麼這裡為Executor介面
      for (Class<?> c : type.getInterfaces()) {
        // 1. 判斷註解中是否包含目標物件介面  
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      // 2. 如果繼承父介面,繼續迴圈判斷
      type = type.getSuperclass();
    }
    // 返回該外掛註解與被代理物件匹配的所有的介面類物件陣列 
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }

2. 執行呼叫

當呼叫被代理物件Execute的所有方法,都會進入Plugin的invoke方法:

  // 代理類只用來做流程判斷,不增加具體的業務邏輯,業務邏輯統一在實現Invocation介面的外掛類中增加
  // 對該方法進行遞迴呼叫(包括所有已載入進攔截器鏈中的攔截器,首先判斷當前攔截器中的註解條件是否滿足,滿足的話執行當前攔截器,否則呼叫target的方法,進入下一個攔截器)
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      // 判斷Signature引數method是否包含當前呼叫方法,如果包含,進入攔截器intercept方法;否則,跳過該攔截器繼續執行
      if (methods != null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

自定義攔截器中intercept實現如下:

public Object intercept(Invocation invocation) throws Throwable {

    // TODO 增加自己的業務需求

    return invocation.proceed();
  }

Invocation相當於責任鏈中的請求類,其封裝反射引數,包含target(target通過InterceptorChain的pluginAll迴圈代理,因此可以是攔截器的代理類,當經過最後一個攔截器時,為實際呼叫物件)、method、args,該類主要用來減少intercept方法呼叫時傳入的引數數量

其中proceed方法如下:

public Object proceed() throws InvocationTargetException, IllegalAccessException {
    // 通過反射執行target的方法
    // 如果target依舊為Plugin代理類,則繼續進行代理類Plugin中的invoke方法中
    return method.invoke(target, args);
  }

三. 小結

Mybatis攔截器主要實現以下幾個點:

  • 通過註解判斷被攔截的請求是否符合當前的攔截器。
  • 支援橫向擴充套件,可以自定義攔截器並加入到攔截器鏈中。
  • 支援請求在攔截器鏈中依次傳遞(Invocation類)。

    其結合責任鏈模式使請求和處理解耦,但是每一次請求都要通過責任鏈上的所有攔截器,也就是一次呼叫需要所有攔截器進行判斷,因此也有一些侷限性。

相關文章:

一文讀懂JDBC
mybaits動態代理之最小demo實現
Mybatis之sqlSession呼叫鏈分析

瞭解更多請關注微信公眾號: