Mybatis 插件開發與責任鏈模式

概述

插件是用來改變或者擴展 Mybatis 的原有的功能,Mybatis 的插件就是通過繼承 Interceptor 攔截器實現的。在沒有完全理解插件之前禁止使用插件對 Mybatis 進行擴展,又可能會導致嚴重的問題。

Mybatis 中能使用插件進行攔截的接口和方法如下:

  • Executor(update、query 、 flushStatment 、 commit 、 rollback 、 getTransaction 、 close 、 isClose)
  • StatementHandler(prepare 、 paramterize 、 batch 、 update 、 query)
  • ParameterHandler(getParameterObject 、 setParameters )
  • ResultSetHandler(handleResultSets 、 handleCursorResultSets 、 handleOutputParameters)

關鍵接口

Interceptor

package org.apache.ibatis.plugin;

import java.util.Properties;

public interface Interceptor {

  //執行攔截邏輯的方法,調用代理對象的invoke方法
  Object intercept(Invocation invocation) throws Throwable;

  //target是被攔截的對象,它的作用就是給被攔截的對象生成一個代理對象
  Object plugin(Object target);

  //讀取在plugin中設置的參數
  void setProperties(Properties properties);

}

Invocation

public class Invocation {

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

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

}

示例:慢查詢檢測插件

配置類

@Intercepts({
	@Signature(type=StatementHandler.class,method="query",args={Statement.class, ResultHandler.class})
//	@Signature(type=StatementHandler.class,method="query",args={MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class})
})

public class ThresholdInterceptor implements Interceptor {
	
	private long threshold;

	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		long begin = System.nanoTime();
		//執行query方法
		Object ret = invocation.proceed();
		long runTime = (System.nanoTime()- begin)/1000;
		if(runTime >= threshold){
			Object[] args = invocation.getArgs();
			//拿到的其實是PreparedStatement的一個動態代理
			//InvocationHandler是PreparedStatementLogger
			Statement stat = (Statement) args[0];
			//反射工具類
			MetaObject metaObjectStat = SystemMetaObject.forObject(stat);
			//還記得h.invoke()嗎
			PreparedStatementLogger statementLogger = (PreparedStatementLogger)metaObjectStat.getValue("h");
			Statement statement = statementLogger.getPreparedStatement();
			//TODO 日誌相關操作
			System.out.println("sql語句:“"+statement.toString()+"”執行時間爲:"+runTime+"毫秒,已經超過閾值!");
		}
		return ret;
	}

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

	@Override
	public void setProperties(Properties properties) {
		this.threshold = Long.valueOf(properties.getProperty("threshold"));
	}
	
}

關於

@Signature(type=StatementHandler.class,method="query",args={Statement.class, ResultHandler.class})

type:攔截的類型
method:方法
args:參數的類型
在這裏插入圖片描述

配置文件

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>	
 	<plugins>

		<plugin interceptor="com.enjoylearning.mybatis.Interceptors.ThresholdInterceptor"> 
			<property name="threshold" value="10"/>
		</plugin>
			
<!--  		 <plugin interceptor="com.github.pagehelper.PageInterceptor">
			<property name="pageSizeZero" value="true" />
		</plugin> -->
		
	</plugins>
	
</configuration>  

運行結果

隨便做個 select 測試,結果如下
在這裏插入圖片描述
如果真正用在項目裏,閾值應該設置大一些。

原理

責任鏈模式

責任鏈模式:就是把一件工作分別經過鏈上的各個節點,讓這些節點依次處理這個工作;和裝飾器模式不同,每個節點都知道後繼者是誰;適合爲完成同一個請求需要多個處理類的場景。

在這裏插入圖片描述
角色

  • Handler:定義了一個處理請求的標準接口
  • ConcreteHandler:具體的處理者,處理它負責的部分,根據業務可以結束處理流程,也可以將請求轉發給它的後繼者

優點

  • 降低耦合度。它將請求的發送者和接收者解耦。
  • 簡化了對象。使得對象不需要知道鏈的結構。
  • 增強給對象指派職責的靈活性。通過改變鏈內的成員或者調動它們的次序,允許動態地新增或者刪除責任。
  • 增加新的請求處理類很方便。

源碼

XMLConfigBuilder

類 XMLConfigBuilder 裏的 parseConfiguration 方法裏,有一步是

//解析<plugins>節點
pluginElement(root.evalNode("plugins"));

跟 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對象,底層使用ArrayList保存所有的插件
        //這裏體現了責任鏈模式的順序
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

Configuration

在這裏插入圖片描述
可以看到 4 種對象的實例化的方法都在這個類裏。
具體實例化的方法裏都有 interceptorChain.pluginAll 方法。

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);
    }
    //通過interceptorChain遍歷所有的插件爲executor增強,添加插件的功能
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

InterceptorChain

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      //注意這裏,調用plugin方法,這是自己重寫的方法
      target = interceptor.plugin(target);
    }
    return target;
  }

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

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

}

重寫的 plugin 方法,其實是參照 ExamplePlugin 的。
在這裏插入圖片描述

@Intercepts({})
public class ExamplePlugin implements Interceptor {
  private Properties properties;
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    return invocation.proceed();
  }

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

  @Override
  public void setProperties(Properties properties) {
    this.properties = properties;
  }

  public Properties getProperties() {
    return properties;
  }

}

Plugin

public class Plugin implements InvocationHandler {
  //封裝的真正提供服務的對象
  private final Object target;
  //自定義的攔截器
  private final Interceptor interceptor;
  //解析@Intercepts註解得到的signature信息
  private final Map<Class<?>, Set<Method>> signatureMap;
 
  //靜態方法,用於幫助Interceptor生成動態代理
  public static Object wrap(Object target, Interceptor interceptor) {
	//解析Interceptor上@Intercepts註解得到的signature信息
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();//獲取目標對象的類型
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);//獲取目標對象實現的接口(攔截器可以攔截4大對象實現的接口)
    if (interfaces.length > 0) {
      //使用jdk的方式創建動態代理
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          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)) {//如果當前方法需要被攔截,則調用interceptor.intercept方法進行攔截處理
        //自己重寫的intercept方法,就是在這裏被調用
        return interceptor.intercept(new Invocation(target, method, args));
      }
      //如果當前方法不需要被攔截,則調用對象自身的方法
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

//省略了一些方法
}

相關資料

https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/Interceptor.md

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