1. 插件能够拦截的对象和方法
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
2. 插件的工作流程
解析注册---->代理----->执行时拦截
解析注册是在解析mybtais-config.xml配置文件时,解析plugin标签时完成。把所有的插件都放到interceptorChain中进行保存。这是一个ArrayList。
代理是在创建上面四个对象时执行。具体的代理逻辑,下面分析。
执行时拦截,拦截器会拦截配置的对象的方法。例如我有一个拦截器拦截Executor的query方法,当Executor执行query方法时,会先走拦截器的逻辑。思想上有些类似于Spring的AOP。
3. 一次自定义插件使用的分析
3.1 插件配置
代码实现:
实现Interceptor接口,实现接口的方法,在intercept方法中实现自己的插件功能。
@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("自定义插件开始执行...");
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("自定义插件执行结束!!!");
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
3.2 插件的代理
上面的例子中,我们的插件要拦截Executor的query方法。
Mybatis在运行的时候是怎么进行拦截的呢?
首先,openSession()的时候会创建一个executor执行器,最后会对这个执行器进行插件植入操作。
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
// 第二步,到具体的实现类中调用plugin方法
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
不管有没有配置拦截器,可以被拦截的四种对象在创建的过程中都会有拦截器的包装操作,但是到底要不要进行包装呢,由if (interfaces.length > 0) {
这句代码进行控制。
Plugin.java
// 第三步,使用jdk的动态代理
public static Object wrap(Object target, Interceptor interceptor) {
// 获取拦截器要拦截的接口的签名,只要拦截器配置就会有值
Map, Set> signatureMap = getSignatureMap(interceptor);
// 获取目标类的类型
Class> type = target.getClass();
/**
* 判断目标类是不是本次要被代理的接口。因为mybatis的拦截器可以拦截Executor、ParameterHandler、
* ResultSetHandler、StatementHandler四种对象。这四个对象的创建过程中都会有拦截器逻辑的判断,都 * 走这个代码从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 插件的执行
3.4.1 JDK动态代理
JDK动态代理后方法的执行顺序:
// jdk动态代理的一个例子,重写的invoke()方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object res = method.invoke(person,args);
return res;
}
- 代理对象调用方法。
- 执行h的invoke()方法。
- 进行前置增强处理。
- method.invoke真正执行要到用的方法。
3.4.2 Mybatis中动态代理的调用
我们已经知道,Mybatis是使用JDK的动态代理,去动态的代理真正要执行的对象的方法。从而实现增强的功能。因此,当要执行的对象进行方法调用的时候,会先执行实现了InvocationHandler类的invoke方法。即:
Plugin类中的invoke方法。
// plugin是触发管理类,当被代理的对象的方法执行的时候,先执行这个invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set 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);
}
此时,当执行executor的query方法时,就会调用到MyPageInterceptor类中的intercept方法。
try{
return invocation.proceed(); // 这句话需要关注下
}finally {
System.out.println("自定义插件执行结束!!!");
}
当我们执行完自己实现的插件的逻辑之后,还需要执行真正要执行的方法。什么时候去执行呢?
我们知道jdk动态代理中是使用method.invoke(target, args);
去执行真正的方法的调用。而invocation.proceed();
做的就是这个事情。
Invocation.java
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
到这里,拦截器就执行完了。
4. 关键的类
InterceptorChain: 用于保存拦截的对象,使用责任链模式的思想,把所有的拦截器都添加到这个链中。
Interceptor接口: Mybtais框架给提供的接口,所有自己要实现的拦截器都要实现这个接口。相当于给定义了一个规范。
Plugin类:Mybtais框架给提供的类,实现了InvocationHandler接口,在进行invoke方法调用的时候,它的对象可以作为h参数。
Invocation类:Mybtais框架给提供的一个封装的类。把invoke方法的三个参数封装了起来,应该是为了更加方便的调用吧。
5. 总结一下调用流程
- executor执行query方法。
- 发现自己被代理了,然后去执行invoke方法。
- 此时就来到了Pluigin的invoke方法中,继续去执行interceptor的intercept()方法。
- 自定义逻辑执行完之后执行一下
invocation.proceed()
,就完成了method.invoke()的调用。
最后借用一下青山老师的图: