1. 插件能夠攔截的對象和方法
MyBatis 允許你在映射語句執(zhí)行過程中的某一點進行攔截調(diào)用。默認情況下婶溯,MyBatis 允許使用插件來攔截的方法調(diào)用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
2. 插件的工作流程
解析注冊---->代理----->執(zhí)行時攔截
解析注冊是在解析mybtais-config.xml配置文件時艘包,解析plugin標簽時完成褒颈。把所有的插件都放到interceptorChain中進行保存廓推。這是一個ArrayList甥温。
代理是在創(chuàng)建上面四個對象時執(zhí)行锻煌。具體的代理邏輯,下面分析姻蚓。
執(zhí)行時攔截宋梧,攔截器會攔截配置的對象的方法。例如我有一個攔截器攔截Executor的query方法狰挡,當Executor執(zhí)行query方法時捂龄,會先走攔截器的邏輯释涛。思想上有些類似于Spring的AOP。
3. 一次自定義插件使用的分析
3.1 插件配置
<plugins>
<plugin interceptor="com.tomas.mysql.interceptor.MyPageInterceptor">
</plugin>
</plugins>
代碼實現(xiàn):
實現(xiàn)Interceptor接口倦沧,實現(xiàn)接口的方法唇撬,在intercept方法中實現(xiàn)自己的插件功能。
@Intercepts({@Signature(type = Executor.class,method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class MyPageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("自定義插件開始執(zhí)行...");
Executor executor = (Executor)invocation.getTarget();
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0]; // MappedStatement
BoundSql boundSql = ms.getBoundSql(args[1]); // Object parameter
ResultHandler resultHandler = (ResultHandler) args[3];
String countSql = boundSql.getSql();
System.out.println("獲取到SQL語句:"+countSql);
countSql = countSql + " limit 5";
SqlSource sqlSource = new StaticSqlSource(ms.getConfiguration(),countSql,boundSql.getParameterMappings());
Field field = MappedStatement.class.getDeclaredField("sqlSource");
field.setAccessible(true);
field.set(ms,sqlSource);
try{
return invocation.proceed();
}finally {
System.out.println("自定義插件執(zhí)行結(jié)束!!!");
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
3.2 插件的代理
上面的例子中展融,我們的插件要攔截Executor的query方法窖认。
Mybatis在運行的時候是怎么進行攔截的呢?
首先告希,openSession()的時候會創(chuàng)建一個executor執(zhí)行器扑浸,最后會對這個執(zhí)行器進行插件植入操作。
executor = (Executor) interceptorChain.pluginAll(executor);
然后燕偶,把executor作為target進行代理喝噪,當配置多個攔截器的時候,會進行多層代理指么。代理流程如下:
InterceptorChain.java
// 對executor即進行代理
public Object pluginAll(Object target) {
// 第一步 interceptor是配置的插件
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
MyPageInterceptor.java
// 第二步酝惧,到具體的實現(xiàn)類中調(diào)用plugin方法
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
不管有沒有配置攔截器,可以被攔截的四種對象在創(chuàng)建的過程中都會有攔截器的包裝操作涧尿,但是到底要不要進行包裝呢系奉,由if (interfaces.length > 0) {
這句代碼進行控制檬贰。
Plugin.java
// 第三步姑廉,使用jdk的動態(tài)代理
public static Object wrap(Object target, Interceptor interceptor) {
// 獲取攔截器要攔截的接口的簽名,只要攔截器配置就會有值
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 獲取目標類的類型
Class<?> type = target.getClass();
/**
* 判斷目標類是不是本次要被代理的接口翁涤。因為mybatis的攔截器可以攔截Executor桥言、ParameterHandler、
* ResultSetHandler葵礼、StatementHandler四種對象号阿。這四個對象的創(chuàng)建過程中都會有攔截器邏輯的判斷,都 * 走這個代碼從interceptor中判斷出這個攔截器可以攔截哪些對象鸳粉,從target中知道這個對象是什么扔涧,從而可以 * 判斷本次要不要進行代理操作。
**/
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
到此為止届谈,被攔截的對象的代理工作就完成了枯夜。
3.4 插件的執(zhí)行
3.4.1 JDK動態(tài)代理
JDK動態(tài)代理后方法的執(zhí)行順序:
// jdk動態(tài)代理的一個例子,重寫的invoke()方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object res = method.invoke(person,args);
return res;
}
- 代理對象調(diào)用方法艰山。
- 執(zhí)行h的invoke()方法湖雹。
- 進行前置增強處理。
- method.invoke真正執(zhí)行要到用的方法曙搬。
3.4.2 Mybatis中動態(tài)代理的調(diào)用
我們已經(jīng)知道摔吏,Mybatis是使用JDK的動態(tài)代理鸽嫂,去動態(tài)的代理真正要執(zhí)行的對象的方法。從而實現(xiàn)增強的功能征讲。因此据某,當要執(zhí)行的對象進行方法調(diào)用的時候,會先執(zhí)行實現(xiàn)了InvocationHandler類的invoke方法诗箍。即:
Plugin類中的invoke方法哗脖。
// plugin是觸發(fā)管理類,當被代理的對象的方法執(zhí)行的時候扳还,先執(zhí)行這個invoke方法
@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)) {
// 自定義的攔截方法的實現(xiàn)
return interceptor.intercept(new Invocation(target, method, args));
}
// 不許要被攔截的方法才避,直接調(diào)用
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
此時,當執(zhí)行executor的query方法時氨距,就會調(diào)用到MyPageInterceptor類中的intercept方法桑逝。
try{
return invocation.proceed(); // 這句話需要關(guān)注下
}finally {
System.out.println("自定義插件執(zhí)行結(jié)束!!!");
}
當我們執(zhí)行完自己實現(xiàn)的插件的邏輯之后,還需要執(zhí)行真正要執(zhí)行的方法俏让。什么時候去執(zhí)行呢楞遏?
我們知道jdk動態(tài)代理中是使用method.invoke(target, args);
去執(zhí)行真正的方法的調(diào)用。而invocation.proceed();
做的就是這個事情首昔。
Invocation.java
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
到這里寡喝,攔截器就執(zhí)行完了。
4. 關(guān)鍵的類
InterceptorChain: 用于保存攔截的對象勒奇,使用責任鏈模式的思想预鬓,把所有的攔截器都添加到這個鏈中。
Interceptor接口: Mybtais框架給提供的接口赊颠,所有自己要實現(xiàn)的攔截器都要實現(xiàn)這個接口格二。相當于給定義了一個規(guī)范。
Plugin類:Mybtais框架給提供的類竣蹦,實現(xiàn)了InvocationHandler接口顶猜,在進行invoke方法調(diào)用的時候,它的對象可以作為h參數(shù)痘括。
Invocation類:Mybtais框架給提供的一個封裝的類长窄。把invoke方法的三個參數(shù)封裝了起來,應(yīng)該是為了更加方便的調(diào)用吧纲菌。
5. 總結(jié)一下調(diào)用流程
- executor執(zhí)行query方法挠日。
- 發(fā)現(xiàn)自己被代理了,然后去執(zhí)行invoke方法驰后。
- 此時就來到了Pluigin的invoke方法中肆资,繼續(xù)去執(zhí)行interceptor的intercept()方法。
- 自定義邏輯執(zhí)行完之后執(zhí)行一下
invocation.proceed()
,就完成了method.invoke()的調(diào)用灶芝。
最后借用一下青山老師的圖: