1、前言
Mybatis提供了插件機制,可以讓我們介入到底層執行的一些流程,例如SQL執行前打印下SQL語句。這裏的插件,在mybatis裏面實際上是攔截器。在深入探究之前,先看看如何使用,然後再分析原理,最後會提一下筆者從裏面得到的一些啓發。
2、使用示例
首先我們需要在mybatis的配置文件(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>
<!--可以在這裏配置插件,比如打印SQL-->
<plugins>
<plugin interceptor="com.gameloft9.demo.dataaccess.interceptor.DialectStatementHandlerInterceptor">
<property name="debug" value="true"/>
</plugin>
</plugins>
</configuration>
然後編寫插件類,需要實現Interceptor接口:
/**
* SQL攔截器,控制日誌打印
* @author gameloft9
* 2019-11-29
*/
@Slf4j
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
public class DialectStatementHandlerInterceptor implements Interceptor {
/**是否開啓debug模式*/
private String debug;
// 攔截方法
public Object intercept(Invocation invocation) throws Throwable {
RoutingStatementHandler statement = (RoutingStatementHandler) invocation
.getTarget();
if ("true".equals(debug)) { // 打印日誌
log.info("Executing SQL: {}", statement.getBoundSql().getSql().replaceAll("\\s+", " "));
log.info("\twith params: {}", statement.getBoundSql().getParameterObject());
}
// 攔截鏈繼續執行
return invocation.proceed();
}
// 對攔截器進行代理,返回代理對象
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
// 可以讀取xml的屬性配置
public void setProperties(Properties properties) {
this.debug = (String) properties.getProperty("debug");
}
}
其中setProperties方法,可以將xml配置的屬性注入到我們插件裏面來。intercept就是具體的攔截方法,例如我們這裏需要打印一下SQL語句,方便排查問題。plugin是一個包裝動態代理的方法,爲什麼需要這個額外的方法?因爲mybatis插件機制,底層是通過動態代理實現的。這個稍後會講到。
現在我們完成了要做的事情(讀取屬性,打印SQL等),但是我們不知道它攔截的到底是哪個方法。這個功能需要兩個註解來完成,@Intercepts和@Signature。@Intercepts用於接收Signature集合,@Signature則具體給出了需要攔截哪個類的哪個方法。例如上面我們要攔截的是StatementHandler.prepare(Connection)方法。
3、原理分析
進行原理分析前,我們腦海裏先問幾個問題:
1-Interceptor是任何地方都可以攔截嗎?
2-多個攔截器是如何一起工作的?
3-Plugin.wrap()這個彆扭的方法是幹什麼的?
我們寫插件,首先需要實現Interceptor這個接口:
public interface Interceptor {
// 攔截方法
Object intercept(Invocation var1) throws Throwable;
// 假裝暫時不知道它是幹嘛的
Object plugin(Object var1);
// 設置屬性
void setProperties(Properties var1);
}
接口很簡單,只有三個方法,裏面出現了Invocation這個類,我們繼續看:
public class Invocation {
private Object target;
private Method method;
private Object[] args;
//省略不相干的代碼
......
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return this.method.invoke(this.target, this.args); // 非常熟悉的反射調用
}
}
這個Invocation簡單得不能再簡單了,僅僅是對method的反射調用來了一層包裝而已。看到這裏我們隱隱約約感覺到事情有點不簡單了,有經驗的同學,看到這裏就可能猜到可能和jdk的動態代理有什麼關係。不過沒猜到也沒關係,我們從plugin方法開始入手。
在Interceptor裏面的,我們實現plugin方法很簡單,直接調用了Plugin.wrap方法。下面我們來看看,它做了什麼:
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);// 拿到方法簽名列表
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap); // 拿到接口列表
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target; // jdk動態代理
}
看到了這裏,有沒有恍然大悟的感覺?Proxy.newProxyInstance,多麼熟悉的代碼,mybatis插件機制,是通過jdk動態代理實現的無疑了。
getSignatureMap通過拿到攔截器上面的@Intercept註解,把需要攔截的類及方法一個個找出來,存在了一個HashMap裏面:
Map<Class<?>/**被攔截類*/, Set<Method>/**被攔截的方法*/> map。
getAllInterfaces首先拿到被代理對象的接口,如果剛好是需要攔截的類(signatureMap已經存了所有要攔截的類及方法),那麼就添加到動態代理的接口列表裏面。
至此,我們解決了問題3:Plugin.wrap這個彆扭的方法是幹什麼的?它可以對目標類進行動態代理,將攔截器注入到InvocationHandler裏面去,方便後面實現攔截邏輯。
很顯然,重點就是這個plugin類,我們來看看它有什麼:
/**
* Plugin實現了InvocationHandler,表明它就是動態代理的具體實現
*/
public class Plugin implements InvocationHandler {
private Object target; // 被代理的對象
private Interceptor interceptor; // 插件對象(本質上是一個攔截器),合適的時機就可以通過調用它處理攔截邏輯
private Map<Class<?>, Set<Method>> signatureMap; // 分析插件對象上的@Intercepts 註解後得到的需要攔截的接口、方法清單
// 動態代理
public static Object wrap(Object target, Interceptor interceptor) {
// 省略代碼
}
// 具體實現
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass()); // 拿到所有需要攔截的方法
return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args); // 如果剛好是需要攔截的,則進行攔截,否則放過去,調用原生方法
} catch (Exception var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
}
從上面的invoke代碼中我們可以看到,當方法執行的時候,首先對方法進行了一個過濾操作,是我們要攔截的方法才調用攔截器的intercept方法,此時Invocation對象也出現了。這裏就很自然的進入到我們的SQL插件的intercept方法裏面來了:
// 攔截方法
public Object intercept(Invocation invocation) throws Throwable {
RoutingStatementHandler statement = (RoutingStatementHandler) invocation
.getTarget();
if ("true".equals(debug)) {
log.info("Executing SQL: {}", statement.getBoundSql().getSql().replaceAll("\\s+", " "));
log.info("\twith params: {}", statement.getBoundSql().getParameterObject());
}
// 攔截鏈繼續執行
return invocation.proceed();
}
上面的攔截邏輯寫完後,一定不要忘記加一句return invocation.proceed(),否則僅僅執行了這一個插件方法,後面的插件方法都被忽略了。
那麼問題來了,我只看到了InvocationHandler裏面存了一個interceptor,如果有多個interceptor,那麼如何工作呢?
別急,我們發現了一個很有趣的類,InterceptorChain。顧名思義它應該就是組合多個攔截器的。
public class InterceptorChain {
// 攔截器列表
private final List<Interceptor> interceptors = new ArrayList();
// 把所有攔截器組合起來
public Object pluginAll(Object target) {
Interceptor interceptor;
for(Iterator i$ = this.interceptors.iterator(); i$.hasNext(); target = interceptor.plugin(target)) {
interceptor = (Interceptor)i$.next();
}
return target;
}
// 省略不相干代碼
}
說實話,當筆者看到pluginAll這個方法的時候,我感覺到了高潮。簡單的循環裏面,對target對象進行了層層代理!居然還可以這麼玩?!通過這樣一個層層代理,把所有的攔截器給串聯了起來!至此,我們解決了問題2-多個攔截器是如何一起工作的?通過層層的jdk動態代理,把多個攔截器進行層層包裝,最後形成一個大的動態代理對象。(要知道動態代理新生成的類,是放在元數據區,這樣層層包裝之後,最後元數據區是新增了一個類還是多個類呢?這個問題扯的有點遠,暫且放下。)
下面我們搜搜看,看是哪裏用到了這個pluginAll方法:
我們發現,ParamterHandler、Executor、StatementHandler、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); // 對statementHandler進行了層層代理
return statementHandler; // 返回了代理對象
}
以StatementHandler爲例,我們創建完statementHandler對象後,對其進行了層層代理,然後返回了代理對象。這樣執行方法的時候,很自然的會走到攔截鏈裏面去。至此,我們解決了問題1-Interceptor是任何地方都可以攔截嗎?不是的,只能夠攔截ParamterHandler、Executor、StatementHandler、ResultSetHandler四個對象。
至此,Mybatis的插件機制就探究完了。但是筆者的思考並沒有跟着結束,我看完後思考了兩個問題:
4、延伸思考
4.1、關於plugin方法
在Interceptor接口中,定義了plugin方法:
Object plugin(Object target);
plugin方法主要是爲了提供一個動態代理的包裝,基本上插件的實現類都會這麼寫:
// 對攔截器進行代理,返回代理對象
public Object plugin(Object target) {
return Plugin.wrap(target, this); // 基本就這就行了
}
如果僅僅是爲了生成動態代理,而且每次都這麼寫一遍也挺無趣的。這個方法完全沒有必要由插件類來實現。我們可以將其移到InterceptChain裏面進行處理,例如:
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
// 原來是target = interceptor.plugin(target);
target = Plugin.wrap(target,interceptor);
}
return target;
}
這樣可以減少一個接口方法,插件類也不用關心如何去做動態代理了。那麼這裏爲什麼要把動態代理的過程暴露給插件實現類呢,我想可能作者有什麼考量吧?在代理目標對象前,可能需要對其進行一些邏輯處理?
4.2、責任鏈模式?
當我看到那個puginAll層層代理的實現的時候,說實話,我的腦海裏立刻聯想到了責任鏈模式。雖然接觸過不少設計模式,但是筆者對責任鏈模式情有獨鍾,很多複雜的業務都是需要通過責任鏈進行拆分處理的。在筆者的一個支付類項目中,核心業務就是通過雙向責任鏈構造的。核心服務類大概長這個樣子(去掉了敏感的和與責任鏈模式不相干的東西):
// 核心服務類
public class BizService {
private String serviceId; // 服務id
private List<IProcessFilter> filters; // 業務過濾鏈
private IProcessFilter businessHandler; // 業務處理器,本質上是最後一個過濾器
/**
* 開始進行業務處理
*/
public void execute(BizContext context, IFilterCallback lastCallback)
throws ProcessException {
// 初始化責任鏈
LinkedList<IProcessFilter> filterList = new LinkedList<IProcessFilter>();
if (filters!= null) {
filterList.addAll(filters);
}
filterList.add(businessHandler);
FilterChain filterChain = new FilterChain(filterList);
try {
filterChain.process(context, lastCallback); // 開始責任鏈處理
} finally {
log.info("完成業務請求");
}
}
}
然後是責任鏈類:
public class FilterChain implements IFilterChain {
private LinkedList<IProcessFilter> filters; // 使用LinkedList保證執行順序
public FilterChain(LinkedList<IProcessFilter> filters) {
this.filters = filters;
}
@Override
public void process(BizContext context, IFilterCallback callback)
throws ProcessException {
IProcessFilter nextFilter = filters.removeFirst(); // 取出第一個過濾器
nextFilter.process(context, callback, this);// 調用處理邏輯
}
}
一個簡單的Filter大概就是下面這個樣子:
@Slf4j
@Service
public class XxxFilter implements IProcessFilter {
@Override
public void process(BizContext context, final IFilterCallback callback,
IFilterChain filterChain) throws ProcessException {
// 過濾邏輯
.....
// 繼續責任鏈處理
filterChain.process(context, new IFilterCallback() {
@Override
public void onPostProcess(ProcessContext context)
throws ProcessException {
// 回調邏輯
......
// 繼續調用上個回調
callback.onPostProcess(context);
}
});
}
最後通過spring xml注入的方式,就可以組裝一個完整的服務對象了。
上面的雙向責任鏈實現,是科室的創始人寫的(老大已經去了螞蟻金服),很巧妙,但是也很複雜。特別是當Filter很多,每個Filter又有回調的時候,理解起來就沒那麼容易了。我曾經試圖給同事講清楚這個責任鏈,他聽完後,仍然一臉懵逼。這個雙向責任鏈有什麼問題呢?目前我想到的有兩個:
1-在處理完Filter邏輯後,必須顯示的調用 filterChain.process()來讓責任鏈繼續執行下去。(這個問題並不好處理)
2-不管你有沒有回調,你都必須顯示的傳遞一個回調對象給下個Filter,因爲上個Filter的回調需要你顯示的去調用,也就是這裏callback.onPostProcess(context)。
並不是所有的人都熟悉這個責任鏈,因此在做業務的時候,很有可能忘記調用callback.onPostProcess(context);導致程序出現莫名其妙的問題。筆者就曾因爲忘記調用這個回調,導致出現樂觀鎖的問題,你說這是不是低級錯誤?
回到正題,通過mybatis的插件機制,對我們的雙向責任鏈有什麼啓發呢?啓發很大,插件的本質是攔截鏈,和我們過濾鏈是類似,我們同樣可以對過濾器進行層層代理來達到雙向責任鏈的效果!
4.2.1、基於jdk動態代理的雙向責任鏈
首先是Filter的改造,之前的IProcessFilter只有一個process方法,現在我們新增一個回調方法:
/**
* Created by gameloft9 on 2020/4/14.
*/
public interface Filter {
// 過濾處理
default Object filter(Invocation invocation) throws Throwable{
return invocation.proceed(); // 默認不處理
};
default Object callBack(Invocation invocation) throws Throwable{
return invocation.getResult(); // 默認直接返回結果
};
}
然後是FilterChain,類似於mybatis的InterceptorChain,我們對filter進行層層代理:
/**
* Created by gameloft9 on 2020/4/14.
*/
public class LinkedFilterChain {
private LinkedList<Filter> filters; // 過濾器列表
public LinkedFilterChain(){
this.filters = new LinkedList<Filter>();
}
public LinkedFilterChain(LinkedList<Filter> list){
filters = list;
}
public void addAll(LinkedList<Filter> list){
filters.addAll(list);
}
/**
* 代理目標對象
* @param target
* @return
*/
public Object proxy(ProcessContext context,Object target) {
return FilterInvocationHandler.wrapAll(context,target,filters);
}
}
最後是InvocationHandler的實現類,負責具體的代理包裝,業務調用:
/**
* InvocationHandler實現類
* Created by gameloft9 on 2020/4/13.
*/
public class FilterInvocationHandler implements InvocationHandler {
private final Object target; // 被代理對象
private final Filter filter; // 過濾器
private final ProcessContext context; // 業務的上下文
public FilterInvocationHandler(ProcessContext context, Object target, Filter filter) {
this.context = context;
this.target = target;
this.filter = filter;
}
/**這裏invoke寫比較簡單,沒有過濾掉一些通用的方法*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Invocation invocation = new Invocation(context, target, method, args);
// 進行過濾
filter.filter(invocation);
// 回調
return filter.callBack(invocation);
}
/**
* 包裝代理
*/
public static Object wrap(ProcessContext context, Object target, Filter filter) {
Class type = target.getClass();
Class[] interfaces = target.getClass().getInterfaces();
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new FilterInvocationHandler(context, target, filter));
}
return target;
}
/**
* 層層代理
* @param target
* @param filters
* @return
*/
public static Object wrapAll(ProcessContext context, Object target, LinkedList<Filter> filters) {
for (Filter filter : filters) {
target = wrap(context, target, filter);
}
return target;
}
}
然後我們對Invocation對象做了擴展,將業務的上下文和執行結果保存下來,方便後面使用。
/**
* 封裝一下method.invoke
* 而且還可以放更多有用的對象,例如context對象。
* Created by gameloft9 on 2020/4/13.
*/
@Data
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
private Object result; // 執行結果
private final ProcessContext context; // 業務的上下文
public Invocation(ProcessContext context,Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
this.context = context;
this.result = null;
}
public Object proceed() throws InvocationTargetException, IllegalAccessException {
Object result = method.invoke(target,args);
this.result = result; // 保存一下執行結果,方便後面獲取
return result;
}
}
業務上下文可以根據自己的業務需求來定製,這裏只給一個簡單的例子:
/**
* 上下文
* Created by gameloft9 on 2020/4/14.
*/
@Data
public class ProcessContext {
/**
* 服務開始時間(nano)
*/
private long serviceAcceptNanoTime = System.nanoTime();
/**
* 請求原文
*/
private String jsonRequest;
/**
* 應答原文
*/
private String jsonResponse;
// 還可以放一些通用的東西
// ......
/**
* 存放額外的數據
* */
private Map<String,String> map = new HashMap<>();
public ProcessContext(){
}
}
到這裏我們的雙向責任鏈就完成了!我們試試如何使用它。
/**
* 基於jdk動態代理的雙向責任鏈
* 不同於普通的雙向責任鏈
* Created by gameloft9 on 2020/4/13.
*/
@Slf4j
public class Client {
public static void main(String[] args) {
// 拿到過濾列表
Filter filterOne = new FilterOne();
Filter filterTwo = new FilterTwo();
LinkedList<Filter> list = new LinkedList<>();
list.add(filterTwo);
list.add(filterOne);
// 生成責任鏈
LinkedFilterChain filterChain = new LinkedFilterChain(list);
// 創建業務上下文
ProcessContext context = new ProcessContext();
Request request = new Request(5,3);
context.setJsonObjectRequest((JSONObject) JSON.toJSON(request));
// 代理最終業務目標,業務裏面做加法操作,很簡單就不貼代碼了
Service service = (Service) filterChain.proxy(context,new ServiceImpl());
// 執行業務
int result = service.doService(context);
log.info("執行結果:{}",result);
// 看下Context
log.info("context:{}",context);
}
}
我們看看Filter怎麼寫的:
/**
* Created by gameloft9 on 2020/4/14.
*/
@Slf4j
public class FilterOne implements Filter {
@Override
public Object filter(Invocation invocation) throws Throwable {
log.info("Filter One 處理邏輯..");
// 處理context
ProcessContext context = invocation.getContext();
context.getMap().put("preOne","preOne");
// 繼續調用
return invocation.proceed();
}
@Override
public Object callBack(Invocation invocation) throws Throwable {
// 操作結果
Object result = invocation.getResult();
log.info("Filter One 對結果進行+1,result:{}", result = (Integer)result + 1);
// 往context裏面塞點東西
ProcessContext context = invocation.getContext();
context.getMap().put("afterOne","afterOne");
return result;
}
}
從這裏看出,雖然執行過濾邏輯後,仍然要調用invocation.proceed(),但是我們的回調變得簡單了,不需要塞內部匿名類,也不需要擔心忘記調用callback.postProcess。而且Filter給出了默認實現,因此如果不需要處理過濾或者回調,連相應的方法也不需要實現和重寫!
下面是執行結果:
根據這個責任鏈,我們還可以對我們BizService進行重構,限於篇幅這裏就不貼代碼了。
5、總結
Mybatis通過jdk動態代理,可以爲ParamterHandler、Executor、StatementHandler、ResultSetHandler四個對象添加攔截器,靈活的進行一些業務處理。不過,任何事情都有兩面性,如果你瞭解插件的機制,那麼可以做很多很棒的事情,例如分頁,查詢結果轉換。如果不瞭解,那麼你的插件很有可能破壞mybatis的內部結構。