Mybatis攔截器實現原理深度分析


1.攔截器簡介

 攔截器可以說使我們平時開發經常用到的技術了,Spring AOP、Mybatis自定義插件原理都是基於攔截器實現的,而攔截器又是以動態代理為基礎實現的,每個框架對攔截器的實現不完全相同,今天我們就來一起分析下Mybatis攔截器實現原理,其實也就是自定義插件的實現原理了。

2.Mybatis攔截器

2.1創建一個攔截器

在mybatis中提供了Interceptor接口,自己實現攔截器只需要實現Interceptor接口即可,下面來看一下接口定義:

public interface Interceptor {
  //攔截方法,執行攔截器邏輯
  Object intercept(Invocation invocation) throws Throwable;
  //為目標對象創建代理並返回,通過調用Plugin.wrap(target, this)實現
  Object plugin(Object target);
  //設置屬性
  void setProperties(Properties properties);

}

下面來看一下我們常用的分頁插件的實現:

//Intercepts注解表示這是一個攔截器,Signature注解描述具體攔截mybatis中四大對象中的哪一個,這里面只攔截Executor類型,
//只攔截Executor的query方法,因為query方法有多個,所以通過args標識攔截具體哪個query方法
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class PageHelper implements Interceptor {
    
    /**
     * Mybatis攔截器方法
     *
     * @param invocation 攔截器入參
     * @return 返回執行結果
     * @throws Throwable 拋出異常
     */
    public Object intercept(Invocation invocation) throws Throwable {
        //執行分頁
        return sqlUtil.processPage(invocation);
    }

    /**
     * 只攔截Executor
     *
     * @param target
     * @return
     */
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    /**
     * 設置屬性值
     *
     * @param p 屬性值
     */
    public void setProperties(Properties p) {
        //MyBatis3.2.0版本校驗
        try {
            Class.forName("org.apache.ibatis.scripting.xmltags.SqlNode");//SqlNode是3.2.0之后新增的類
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("您使用的MyBatis版本太低,MyBatis分頁插件PageHelper支持MyBatis3.2.0及以上版本!");
        }
        //數據庫方言
        String dialect = p.getProperty("dialect");
        sqlUtil = new SqlUtil(dialect);
        sqlUtil.setProperties(p);
    }
}

了解了怎樣創建一個攔截器后,下面來看一下攔截器如何生效,也就是如何構建攔截器鏈

2.2攔截器鏈的構建

mybatis中的InterceptorChain類用來創建攔截器鏈,內部持有一個interceptors 的List,攔截器的順序就是在配置文件中配置的攔截器的順序,因為攔截器有順序之分,所以這里用一個List維護。

public class InterceptorChain {
  
  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
  //創建攔截器鏈,target為mybatis中的4大對象中的某個(ParameterHandler,StatementHandler,ResultSetHandler,Executor)
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
    //通過jdk動態代理為目標創建代理對象,如果有多個攔截器,那么會出現代理對象再次被代理的情況,通過這樣層層代理,構建攔截器鏈。注意:target的代理對象再次賦值給target,如果有多個攔截器,代理對象target將再次被代理!
      target = interceptor.plugin(target);
    }
    return target;
  }

  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
  
  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

既然pluginAll(target)方法是用來構建攔截器鏈的,那么,這個方法是在哪里被調用的呢,看下圖

 以Executor為例,繼續看

 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) interceptorChain.pluginAll(executor);
    return executor;
  }

由上面的代碼可知在每次調用newExecutor()方法時將目標對象executor傳入interceptorChain.pluginAll(),返回executor的代理對象,其實在這個代理對象的內部,攔截器鏈已經形成了。

要想搞清楚攔截器鏈怎樣構建的,必須需要深入interceptor.plugin(target)方法的內部一探究竟

以上面的分頁插件為例,如果target是Executor類型,則調用Plugin.wrap(target, this)方法,否則,直接返回傳進來的target。

Plugin類是是mybatis中用來為目標對象創建代理對象的類,實現了InvocationHandler接口,所以對代理對象的所有調用都會調用Plugin類的invoke方法:

public class Plugin implements InvocationHandler {
  //目標對象,可能是一個代理對象,在第一次調用interceptor.plugin(target)時,target不是代理類,
  //之后調用interceptor.plugin(target)時,這里的target就是代理對象了
  private Object target;
  //攔截器對象,因為之后要在invoke方法里面調用攔截器的攔截方法,所以這里需要持有引用
  private Interceptor interceptor;
  //攔截器方法簽名map,在invoke方法內部判斷如果調用的是攔截器支持攔截的方法,否則,直接調用目標對象的方法
  private Map<Class<?>, Set<Method>> signatureMap;

  private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }
  //自己寫的攔截器需要調用該方法對目標對象進行代理
  public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    //獲取目標類的所有方法,包括從父接口繼承的接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,//注意這里第三個參數創建了一個當前類對象,並將目標對象、攔截器對象和方法簽名的map傳入
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      //不為空且攔截器支持對該方法進行攔截,則調用攔截器的攔截方法
      if (methods != null && methods.contains(method)) {
        //注意方法參數,創建一個Invocation並將目標對象、方法和參數傳進去,所以在Invocation對象內部可以通過反射調用目標對象的方法
        return interceptor.intercept(new Invocation(target, method, args));
      }
      //直接調用目標對象的方法
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

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

  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }

}

 對代理對象的方法調用會調用interceptor.intercept(new Invocation(target, method, args),下面來看分頁插件的intercept方法:

 繼續看processPage方法,processPage方法調用了_processPage方法:

public class SqlUtil implements Constant {

    //...
    
    /**
     * Mybatis攔截器方法
     *
     * @param invocation 攔截器入參
     * @return 返回執行結果
     * @throws Throwable 拋出異常
     */
    private Object _processPage(Invocation invocation) throws Throwable {
        final Object[] args = invocation.getArgs();
        RowBounds rowBounds = (RowBounds) args[2];
        if (SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT) {
            return invocation.proceed();
        } else {
            //忽略RowBounds-否則會進行Mybatis自帶的內存分頁
            args[2] = RowBounds.DEFAULT;
            //分頁信息
            Page page = getPage(rowBounds);
            //pageSizeZero的判斷
            if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) {
                //執行正常(不分頁)查詢
                Object result = invocation.proceed();
                //得到處理結果
                page.addAll((List) result);
                //相當於查詢第一頁
                page.setPageNum(1);
                //這種情況相當於pageSize=total
                page.setPageSize(page.size());
                //仍然要設置total
                page.setTotal(page.size());
                //返回結果仍然為Page類型 - 便於后面對接收類型的統一處理
                return page;
            }
            //獲取原始的ms
            MappedStatement ms = (MappedStatement) args[0];
            SqlSource sqlSource = ms.getSqlSource();
            //簡單的通過total的值來判斷是否進行count查詢
            if (page.isCount()) {
                //將參數中的MappedStatement替換為新的qs
                msUtils.processCountMappedStatement(ms, sqlSource, args);
                //查詢總數,繼續調用目標對象(也可能是代理對象)的方法,但是此時修改了MappedStatement的參數,實際是查了數據的條數
                Object result = invocation.proceed();
                //設置總數
                page.setTotal((Integer) ((List) result).get(0));
                if (page.getTotal() == 0) {
                    return page;
                }
            }
            //pageSize>0的時候執行分頁查詢,pageSize<=0的時候不執行相當於可能只返回了一個count
            if (page.getPageSize() > 0 &&
                    ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
                            || rowBounds != RowBounds.DEFAULT)) {
                //將參數中的MappedStatement替換為新的ms,新的ms是原來傳過來的ms+分頁信息
                msUtils.processPageMappedStatement(ms, sqlSource, page, args);
                //執行分頁查詢
                Object result = invocation.proceed();
                //得到處理結果
                page.addAll((List) result);
            }
            //返回結果
            return page;
        }
    }
    //...
}

上面的代碼重點關注invocation.proceed()方法,第一次調用proceed()方法獲取記錄條數,第二次調用proceed()方法是執行原來目標對象的調用邏輯,但是此時的ms已經被修改(加上了分頁信息),下面來看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 getTarget() {
    return target;
  }

  public Method getMethod() {
    return method;
  }

  public Object[] getArgs() {
    return args;
  }

  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }

}

Invocation類的代碼非常簡單,在攔截器中調用proceed()方法時,調用目標對象(也可能是代理對象)的方法,如果是目標對象,則直接調用目標對象方法的原有邏輯,如果是代理對象,則又會調用到 Plugin 類的invoke方法,知道最后調用目標對象的方法,完成攔截器鏈的調用,當然,這時,攔截器鏈還沒有執行完成,當目標對象調用完成並返回,攔截器中interceptor方法層層返回,一次攔截器鏈的調用才算完成。

只看代碼還是不夠清晰,下面用時序的方式重新梳理一下攔截器鏈的構建與調用過程:

2.3攔截器鏈構建時序圖

下面來看一下攔截器調用的時序圖,為了形成攔截器鏈的效果,圖中使用2個攔截器做演示:注意構建的時候先調用的MyInterceptor2,后調用的MyInterceptor1

 

 

 需要注意的是上圖中有2個Invocation對象,因為每調用一次interceptor.intercept(new Invocation(target, method, args))方法就會創建一個新的Invocation對象

2.4攔截器鏈調用時序

 

需要注意的是執行攔截器鏈時攔截器的調用順序和構建的時候是相反的,構建中前面創建代理的攔截器后被調用,因為executor對象的代理對象被再次代理,只有調用executor代理對象的代理對象后,才能調用到更深層的executor代理對象,說起來比較繞,下面再簡單畫個圖描述一下

 

 

3.總結

  mybatis中攔截器的設計還是非常巧妙的,可以說將jdk動態代理用到了極致,使用代理代理類的方法構建攔截器鏈。

  構建攔截器鏈的3個核心對象

    InterceptorChain:持有所有攔截器的List,pluginAll()方法負責遍歷攔截器集合,將創建的代理對象作為目標對象再次代理,形成攔截器鏈。

    Interceptor(接口):實現了該接口的類就是一個攔截器,通過調用Plugin.wrap(target)方法獲取目標類的代理對象,調用前需要先判斷目標對象的類型是否是該攔截器需要攔截的對象,如果不是需要攔截的對象,則直接將原對象返回。

    Plugin:負責創建目標對象的代理對象,該類實現了InvocationHandler接口,所以對目標類的所有調用都將調用該類的invoke方法。

 

  攔截器執行中的3個核心對象

    Plugin:該類實現了InvocationHandler接口,在invoke方法中調用持有的Interceptor對象的intercetor()方法,同時傳遞創建的Invocation對象,以便攔截器內部調用Invocation對象的proceed()方法執行被代理對象的原有邏輯。

    Interceptor(接口):實現了該接口的類就是一個攔截器,在intercept()方法內部完成攔截器的邏輯,調用Invocation對象的proceed()方法執行被代理對象的邏輯,注意:invocation.proceed()可以多次調用,分頁插件中第一次調用proceed()方法查記錄條數,第二次調用proceed()方法前通過invocation.getArgs()拿到調用目標對象的方法參數並修改sql實現分頁功能。

    Invocation:提供proceed()方法供攔截器調用,持有目標對象、調用的方法、方法參數並提供get方法,通過反射(method.invoke(target, args))實現對目標對象的調用。

以上就是mybatis攔截器原理的所有分析了。因個人能力有限,如果有錯誤之處,還請指出,謝謝!

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM