Mybatis插件機制探究及延伸思考

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的內部結構。

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