MyBatis plugin的使用與源碼解析

MyBatis plugin的使用與源碼解析

這一節來講下Mybatis中的plugin的使用,plugin作爲對執行期間對Executor、StatementHandler的一種增強等等,我見過用的最多的應該就是Mybatis的分頁插件PageHelper,PageHelper因爲簡單易用被廣泛用於各種大小工程中,雖說PageHelper使用起來確實挺舒服,但是在遇到一些查詢性能上的問題時,PageHelper帶來的弊端倒是挺大的,這個以後有空單獨抽一個專題出來說下PageHelper,這一節就不再多說了,就直說Plugin用法與原理。

1. Plugin用法


Plugin用法說起來挺簡單的,和寫攔截器一樣,在這也想好具體寫什麼Plugin,就寫一個攔截query方法的Plugin吧,打印方法具體耗時,命名爲ExecuteLogPlugin,如下:

@Intercepts({@Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class,
                Object.class,
                RowBounds.class,
                ResultHandler.class}
)})
public class ExecuteLogPlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long start = System.currentTimeMillis();
        Object target = invocation.proceed();
        long end = System.currentTimeMillis();
        System.out.println("intercept " + invocation.getMethod().getName() + " cost : " + (end - start));
        return target;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

這段裏面真正起作用的就是invocation.proceed()這段,就是執行代理方法,然後在前後埋點,最後輸出對應method耗時,在這圖簡便,就沒有單獨引入log的包,就用System輸出了,這個Plugin中最重要的就是@Intercepts註解,表明攔截的是什麼類型,攔截方法。這個註解的解析在後面會提到。

除了編寫Plugin之外,還需要做的事情就是在xml中需要配置Plugin,具體如下:

<plugins>
        <plugin interceptor="cn.com.plugin.ExecuteLogPlugin"></plugin>
</plugins>

這一切做完後,在運行main程序時就能得到期望的效果,在這幾乎每個query方法都進行了攔截,如果只想對某一個或者幾個方法進行攔截的話,invocation中args字段中有對應的class類與方法名,可以在這做些文章。

用法就到此爲止了,運行中的截圖也沒啥好貼出來的,運行時可以自己看下,下面開始對原理性的分析。

2. 原理分析


分析Plugin的加載仍然需要回到configuration的初始化,繼續回到Main程序的第一行開始分析,我們的main程序中的SessionFactory的初始化如下:

String resource = "conf.xml";
        //使用類加載器加載mybatis的配置文件(它也加載關聯的映射文件)
        InputStream is = Main.class.getClassLoader().getResourceAsStream(resource);
        //構建sqlSession的工廠
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(is);

還是進入到build方法中,查看configuration的初始化過程.

private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

如同第一篇文章!Mybatis源碼解析之配置加載(一)一樣,我們回到解析configuration的源碼處,此次其他均不講,只說pluginElement(root.evalNode(“plugins”))這一行,這一行就是plugin的具體加載過程,進入到pluginElement()中。

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

在這裏解析出Plugin,並進行實例化操作,(Interceptor) resolveClass(interceptor).newInstance(),這一步追蹤到最後就是調用了Class.forName()的過程,沒有太多深究的東西。

在實例化完了後,將當前的plugin實例保存進InterceptorCahin中,等待Executor執行時調用,接下來我們再看session獲取到mapper,mapper執行的過程。

UserMapper userMapper = session.getMapper(UserMapper.class);
//執行查詢返回一個唯一user對象的sql
User user = userMapper.getUser(1);

從前幾篇文章我們知這裏的Mapper對象其實就是一個代理對象,真正在起作用的爲MapperProxy,然後在執行調用方法時,是調用MapperProxy的invoke方法,然後對於select方法,最終都是調用selectList,selectList經過多層調用後最終還是調用doQuery方法,我們回到doQuery處。

@Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

此處我們看獲取StatementHandler的方法,進入到newStatementHandler。

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

此處調用了interceptorChain.pluginAll(statementHandler)得到statementHandler對象,而pluginAll中做的操作爲:

public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

就是調用了每個plugin的plugin方法,返回一個包裝對象,然後在statementHandler調用的時候發揮代理的作用,這最終解釋了Plugin中intercept方法發揮作用的時機,但是仍然沒有解釋到@Intercepts註解的作用,以及何時進行攔截,這裏就要說下Plugin中plugin方法的作用了。plugin方法中做的事情爲:

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

對target與當前對象進行了一層包裝,進入到warp方法中。

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,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

進行註解解析的一行代碼爲:

Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);

跳轉到getSignatureMap方法中。

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

這個方法做的事情就是解析**@Signature**註解,將對應的type、method保存進Map中返回。

然後在wrap方法中對當前class進行判斷,如果是當前class是@Signature中指定的type的子類,則進行代理操作。

Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }

對當前class的判斷方法爲getAllInterfaces。

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()]);
  }

這裏判斷了當前signatureMap中是否包含當前class,如果是則返回包含當前class的數組,如果不是則返回空數組,而在上一代碼塊中得知,只有噹噹前數組長度大於0時,纔會返回代理對象,我們在@Signature註解中標註攔截的type爲Executor,那麼所有的方法基本都會被返回代理對象,但是我們選擇攔截方法是query,不能攔截update、delete等操作,所以這裏的玄機就在於實例化的對象傳入的參數包括signatureMap。

new Plugin(target, interceptor, signatureMap))

代理對象最終執行的方法爲invoke方法,我們看當前Plugin的invoke方法。

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)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

這裏一切明瞭,只有signatureMap取出的methods中包含當前方法名纔會調用代理對象的intercept方法,否則直接進行method.invoke操作,不會對方法進行攔截。

Plugin的用法以及原理分析就到此爲止了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章