1. 程式人生 > 實用技巧 >mybatis原始碼(十一) mybatis外掛原理及其應用

mybatis原始碼(十一) mybatis外掛原理及其應用

mybatis原始碼(十一) mybatis外掛原理及其應用

mybatis外掛:MyBatis提供了擴充套件機制,能夠在執行Mapper時改變SQL的執行行為。這種擴充套件機制是通過攔截器來實現的,使用者自定義的攔截器也被稱為MyBatis 外掛。MyBatis框架支援對Executor、ParameterHandler、ResultSetHandler、 StatementHandler四種元件的方法進行攔截。

  Executor 的建立是在SqlSession建立的時候建立的

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean
autoCommit) { Transaction tx = null; try { // 獲取Mybatis主配置檔案配置的環境資訊 final Environment environment = configuration.getEnvironment(); // 建立事務管理器工廠 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); // 建立事務管理器 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 根據Mybatis主配置檔案中指定的Executor型別建立對應的Executor例項 final Executor executor = configuration.newExecutor(tx, execType); // 建立DefaultSqlSession例項 return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
DefaultSqlSessionFactory

  StatementHandler 的建立是在執行sql語句前建立的,例如

public class SimpleExecutor extends BaseExecutor {

  public SimpleExecutor(Configuration configuration, Transaction transaction) {
    super(configuration, transaction);
  }

  @Override
  public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.update(stmt);
    } finally {
      closeStatement(stmt);
    }
  }
SimpleExecutor

  ParameterHandler、ResultSetHandler是在SimpleStatementHandler、PreparedStatementHandler、CallableStatementHandler的構造方法中,呼叫父類的構造方法實現的

  protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    this.configuration = mappedStatement.getConfiguration();
    this.executor = executor;
    this.mappedStatement = mappedStatement;
    this.rowBounds = rowBounds;

    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.objectFactory = configuration.getObjectFactory();

    if (boundSql == null) { // issue #435, get the key before calculating the statement
      generateKeys(parameterObject);
      boundSql = mappedStatement.getBoundSql(parameterObject);
    }

    this.boundSql = boundSql;

    this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
  }
View Code

  總之,都是在執行sql前,就建立的物件

1.為什麼只能攔截這四種外掛呢?

  1.mybatis可以根據使用者配置的引數建立不同的例項,根據Configuration物件。(這4個元件)

  2.mybatis通過工廠方式建立4種元件,在工廠方法中,可以執行攔截邏輯

  Configuration類部分程式碼如下:

  public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    // 執行攔截器鏈的攔截邏輯
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
  }

  public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
      ResultHandler resultHandler, BoundSql boundSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    // 執行攔截器鏈的攔截邏輯
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }

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

  public Executor newExecutor(Transaction transaction) {
    return newExecutor(transaction, defaultExecutorType);
  }

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    // 根據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);
    }
    // 如果cacheEnabled屬性為ture,這使用CachingExecutor對上面建立的Executor進行裝飾
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    // 執行攔截器鏈的攔截邏輯
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

可以看到這四個方法裡面,都有一個interceptorChain.pluginAll 的呼叫過程

pluginAll 返回ParameterHandler、ResultSetHandler、StatementHandler、Executor的代理物件

可以看下InterceptorChain 的原始碼

public class InterceptorChain {

  // 通過List物件維護所有攔截器例項
  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

  // 呼叫所有攔截器物件的plugin()方法執行攔截邏輯
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

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

}

由上程式碼可以看到,mybatis的攔截器執行鏈中維護了一個所有攔截器的List集合。攔截器鏈執行的方法呼叫是interceptor.plugin(targer),

看下Interceptor介面,裡面只有三個方法

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}

Interceptor介面中定義了3個方法:

  intercept()方法用於定義攔截邏輯,該方法會在目標方法呼叫時執行。  

  plugin()方法用於建立Executor、ParameterHandler、 ResultSetHandler 或Statementh Handler的代理物件,該方法的引數即為Executor、ParameterHandler、ResultSetHandler或StatementHandler元件的例項。

  setProperties()方法用於設定外掛的屬性值。

  需要注意的是,intercept()接 收一一個Invocation物件作為引數,Invocation物件中封裝了目標物件的方法及引數資訊。

  Invocation類的原始碼如下:

  Invocation類中提供了一個proceed()方法,該方法用於執行目標方法的邏輯。所以在自定義外掛類中,攔截邏輯執行完畢後一般都需要呼叫proceed()方法執行目標方法的原有邏輯。

public class Invocation {

  // 目標物件,即ParameterHandler、ResultSetHandler、StatementHandler或者Executor例項
  private final Object target;
  // 目標方法,即攔截的方法
  private final Method method;
  // 目標方法引數
  private final 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;
  }

  /**
   * 執行目標方法
   * @return 目標方法執行結果
   * @throws InvocationTargetException
   * @throws IllegalAccessException
   */
  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }

}
View Code

當我們自定義攔截器的時候,需要實現該介面。mybatis中提供了ExamplePlugin類,供我們參考,可以看一下原始碼

@Intercepts({})
public class ExamplePlugin implements Interceptor {
  private Properties properties;
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    // TODO:自定義攔截邏輯
    return invocation.proceed();
  }

  @Override
  public Object plugin(Object target) {
    // 呼叫Plugin類的wrap()方法返回一個動態代理物件
    return Plugin.wrap(target, this);
  }

  @Override
  public void setProperties(Properties properties) {
    // 設定外掛的屬性資訊
    this.properties = properties;
  }

  public Properties getProperties() {
    return properties;
  }

}
我們深入interceptor.plugin(target);的方法,發現他呼叫的是Plugin.wrap(target, this); 繼續深入,檢視PluginPlugin的原始碼如下:
public class Plugin implements InvocationHandler {

    //目標物件,即Executor、ParameterHandler、ResultSetHandler、StatementHandler物件
    private final Object target;
    // 使用者自定義攔截器例項
    private final Interceptor interceptor;
    // Intercepts註解指定的方法
    private final 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;
    }

    /**
     * wrap()方法的第一個引數為目標物件,即Executor、ParameterHandler、 ResultSetHandler、 StatementHandler類的例項;
     * 第二個引數為攔截器例項。在wrap()方法中首先呼叫getSignatureMap()方法獲取Intercepts註解指定的要攔截的元件及方法,
     * 然後呼叫getAllInterfaces()方法獲取當前Intercepts註解指定要攔截的元件的介面資訊,接著呼叫Proxy類的靜態方法
     * newProxyInstance()建立一個動態代理物件。
     * 該方法用於建立Executor、ParameterHandler、ResultSetHandler、StatementHandler的代理物件
     *
     * @param target
     * @param interceptor
     * @return
     */
    public static Object wrap(Object target, Interceptor interceptor) {

        // 呼叫getSignatureMap()方法獲取自定義外掛中,通過Intercepts註解指定的方法
        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;
    }

    /**
     * Plugin類的invoke()方法會在呼叫目標物件的方法時執行,在invoke()方法中首先判斷該方法是否被Intercepts註解指定為被攔截的方法,如果是,則呼叫使用者自定義
     * 攔截器的intercept()方法,並把目標方法資訊封裝成Invocation物件作為intercept()方法的引數。
     *
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            // 如果該方法是Intercepts註解指定的方法,則呼叫攔截器例項的intercept()方法執行攔截邏輯
            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);
        }
    }

    /**
     * Plugin類的getSignatureMap()方法中,首先獲取Intercepts註解,然後獲取Intercepts註解中配置的所有Signature註解,接著對所有的Signature註解資訊進行遍歷,
     * 將Signature註解中指定要攔截的元件及方法新增到Map物件中,其中
     * Key為Executor、 ParameterHandler、ResultSetHandler或StatementHandler對應的Class物件,Value為攔截的所有方法對應的Method物件陣列。
     *
     * @param interceptor
     * @return
     */
    private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
        // 獲取Intercepts註解資訊
        Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
        if (interceptsAnnotation == null) {
            throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
        }
        // 獲取所有Signature註解資訊
        Signature[] sigs = interceptsAnnotation.value();
        Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
        // 對所有Signature註解進行遍歷,把Signature註解指定攔截的元件及方法新增到Map中
        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;
    }

    /**
     * 獲取目標型別的介面資訊
     *
     * @param type
     * @param signatureMap
     * @return
     */
    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()]);
    }

} 

由上述程式碼,發現Plugin,實現了InvocationHandler 即JDK內建的動態代理方式建立代理物件,同時使用Proxy.newProxyInstance返回了代理物件。

如上面的程式碼所示,wrap()方法的第一個引數為目標物件,即Executor、ParameterHandler、 ResultSetHandler、 StatementHandler類的例項;第二個引數為攔截器例項。在wrap()方法中首先呼叫
getSignatureMap()方法獲取Intercepts註解指定的要攔截的元件及方法,然後呼叫getAllInterfaces()方法獲取當前Intercepts註解指定要攔截的元件的介面資訊,接著呼叫Proxy類的靜態方法
newProxyInstance()建立一個動態代理物件。

2.自定義mybatis外掛

  2.1mybatis自定義外掛,都必須實現Interceptor介面,並在intercept()中編寫攔截邏輯

  2.2 攔截器上面要新增@Intercepts註解,標註攔截器的型別和攔截的方法

  例如:

@Intercepts({
        @Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class, Integer.class})
})
public class PageInterceptor implements Interceptor {

  2.3 通過plugin()方法返回一個動態代理物件  

  例如:

    /**
     * 攔截器對應的封裝原始物件的方法
     */
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

  2.4通過setProperties方法設定Plugin標籤中配置的屬性值

  例如:

    /**
     * 設定註冊攔截器時設定的屬性
     */
    public void setProperties(Properties properties) {
        this.databaseType = properties.getProperty("databaseType");
    }

  2.5 mybaits提供了ExamplePlugin的例子供參考

@Intercepts({})
public class ExamplePlugin implements Interceptor {
  private Properties properties;
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    // TODO:自定義攔截邏輯
    return invocation.proceed();
  }

  @Override
  public Object plugin(Object target) {
    // 呼叫Plugin類的wrap()方法返回一個動態代理物件
    return Plugin.wrap(target, this);
  }

  @Override
  public void setProperties(Properties properties) {
    // 設定外掛的屬性資訊
    this.properties = properties;
  }

  public Properties getProperties() {
    return properties;
  }

}
ExamplePlugin

3.mybatis外掛載入的地方

  3.1 在mybatis的plugins標籤下面,配置要載入的外掛
  

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <!DOCTYPE configuration
 3     PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
 4     "http://mybatis.org/dtd/mybatis-3-config.dtd">
 5 
 6 <configuration>
 7   <settings>
 8      <setting name="useGeneratedKeys" value="true"/>
 9   </settings>
10  
11 // 配置外掛
12   <plugins>
13     <plugin interceptor="com.blog4java.plugin.pager.PageInterceptor">
14        <property name="databaseType" value="hsqldb"/>
15     </plugin>
16 
17     <plugin interceptor="com.blog4java.plugin.slowsql.SlowSqlInterceptor">
18        <property name="limitSecond" value="0"/>
19     </plugin>
20   </plugins>
21 
22   <environments default="dev" >
23      <environment id="dev">
24        <transactionManager type="JDBC">
25           <property name="" value="" />
26        </transactionManager>
27        <dataSource type="UNPOOLED">
28           <property name="driver" value="org.hsqldb.jdbcDriver" />
29           <property name="url" value="jdbc:hsqldb:mem:mybatis" />
30           <property name="username" value="sa" />
31           <property name="password" value="" />
32        </dataSource>
33      </environment>
34      <environment id="qa">
35        <transactionManager type="JDBC">
36           <property name="" value="" />
37        </transactionManager>
38        <dataSource type="UNPOOLED">
39           <property name="driver" value="org.hsqldb.jdbcDriver" />
40           <property name="url" value="jdbc:hsqldb:mem:mybatis_qa" />
41           <property name="username" value="admin" />
42           <property name="password" value="admin" />
43        </dataSource>
44      </environment>
45   </environments>
46 
47   <mappers>
48      <mapper resource="com/blog4java/mybatis/example/mapper/UserMapper.xml"/>
49   </mappers>
50 </configuration>
mybatis-config

  3.2 從XmlConfigBuilder類開始解析mybaits-config的主配置檔案

  首先呼叫XmlConfigBuilder.parse()方法 

  public Configuration parse() {
    // 防止parse()方法被同一個例項多次呼叫
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // 呼叫XPathParser.evalNode()方法,建立表示configuration節點的XNode物件。
    // 呼叫parseConfiguration()方法對XNode進行處理
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  然後呼叫parseConfiguration 解析<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);
    }
  }

 然後呼叫pluginElement(root.evalNode("plugins")); 解析mybatis主配置檔案下面的plugins標籤

    private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 獲取<plugin>標籤的interceptor屬性
        String interceptor = child.getStringAttribute("interceptor");
        // 獲取攔截器屬性,轉換為Properties物件
        Properties properties = child.getChildrenAsProperties();
        // 建立攔截器例項
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        // 設定攔截器例項屬性資訊
        interceptorInstance.setProperties(properties);
        // 將攔截器例項新增到攔截器鏈中
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

由上程式碼可以看出,迴圈解析<plugins>標籤下面的每一個<plugin> ,然後建立攔截器新增到集合中

Configuration.addInterceptor(interceptorInstance) 的原始碼如下:

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

剛才說過,interceptorChain內部維護了一個List集合,用來儲存攔截器  

4.Mybatis外掛建立動態代理物件的工作原理

  以執行一個查詢操作為例,SqlSession是MyBatis中提供的面向使用者的操作資料庫的介面,而真正執行SQL操作的是Executor元件。MyBatis通過工廠模式建立Executor例項,
Configuration類中提供了一個newExecutor()工廠 方法,該方法返回的實際上是一個Executor的動態代理物件。
SqlSession獲取Executor例項的過程如下:
  (1) SqlSession中會呼叫Configuration類提供的newExecutor()工廠方法建立Executor物件。
  (2) Configuration類中通過一個InterceptorChain物件維護了使用者自定義的攔截器鏈。newExecutor()工廠方法中呼叫InterceptorChain物件的pluginAll()方法。
  (3) InterceptorChain物件 的pluginAll()方法中會呼叫自定義攔截器的plugin()方法。
  (4)自定義攔截器的plugin()方法是由我們來編寫的,通常會呼叫Plugin類的wrap()靜態方法建立一個代理物件。

                    mybatis動態代理物件建立過程

Sq|Session獲取到的Executor例項實際上已經是一個動態代理物件了。當我們呼叫SqlSession物件的selectOne()方法執行查詢操作時,大致會經歷下面幾個過程:
  (1)SqlSession操作資料庫需要依賴於Executor元件,SqlSession會呼叫Configuration物件的newExecutor()方法獲取Executor的例項。
  (2) SqlSession獲取到的是Executor元件的代理物件,執行查詢操作時會呼叫代理物件的query()方法。
  (3)按照JDK動態代理機制,呼叫Executor代理物件的query()方法時,會呼叫Plugin類的invoke()方法。
  (4) Plugin類的invoke()方法中會呼叫自定義攔截器物件的intercept()方法執行攔截邏輯。
  (5)自定義攔截器物件的intercept()方法呼叫完畢後,呼叫目標Executor物件的query()方法。
  (6)所有操作執行完畢後,會將查詢結果返回給SqlSession物件。

                    mybatis外掛攔截邏輯執行過程