Mybatis允许用户使用自定义拦截器对sql语句执行过程中的某一点进行拦截,默认情况下,Mybatis允许拦截器拦截Executor的方法、ParameterHandler的方法、ResultSetHandler的方法以及StatementHandler的方法。具体可拦截的方法如下:
1 Executor中的update()、query()、flushStatement()、commit()、rollback()、getTransaction()、close()、isClose()方法
2 ParameterHandler中的getParameterObject()、setParameter()
3 ResultSetHandler中的handlerResultSets()、handlerOutputParameter()
4 StatementHandler中的prepare()、parameterize()、batch()、update()、query()
Mybatis中使用的拦截器都需要实现Interceptor接口。Interceptor接口是Mybatis实现Plugin的核心,其定义如下:
public interface Interceptor {
//执行拦截逻辑的方法
Object intercept(Invocation var1) throws Throwable;
//决定是否触发interceptor()方法
Object plugin(Object var1);
//根据配置初始化Interceptor对象
void setProperties(Properties var1);
}
Mybatis通过拦截器可以改变Mybatis的默认行为,例如实现Sql重写之类的功能。本章将从插件的配置和编写、插件的运行原理,插件注册、执行拦截的时机等多个方面对插件进行介绍。
用户自定义的拦截器除了要继承Interceptor接口,还需要使用@Intercepts和@Signature两个注解进行标识。@Intercepts注解中指定一个@Signature注解列表,每个@Signature注解中都标识了该插件需要拦截的方法信息,其中@Signature注解的type属性指定需要拦截的类型,method属性指定需要拦截的方法,args属性指定了被拦截的方法的参数列表。通过这三个属性值,@Signature注解就可以表示一个方法签名,唯一确定一个方法。如下示例所示,该拦截器需要拦截Executor接口的两个方法,分别是query(MappedStatement, Object, RowBouonds, ResultHandler)方法和close(boolean)方法
@Intercepts({
@Signature(Type=Executor.class,method="query",args={MappedStatement.class, Object.class, RowBounds.class,ResultHandler.class}),
@Signature(type=Executor.class, method="close",args={boolean.class})
})
public class ExamplePlugin implement Interceptor{
private int testProp;
......
}
完成一个自定义的拦截器之后,需要在mybaits-config.xml配置文件中对该拦截器进行配置,如下所示
<plugins>
<plugin interceptor="com.test.ExamplePlugin">
<property name="testProp",value="100">
</plugin>
</plugins>
到此为止,一个用户自定义的拦截器就配置好了。在Mybatis初始化时,会通过XMLConfigBuilder.pluginElement()方法解析mybatis-config.xml配置文件中定义的<plugin>节点,得到相应的Interceptor对象以及配置的相应属性,之后会调用Interceptor.setProperties(properties)方法完成对Interceptor对象初始化配置,最后将Interceptor对象添加到Configuration.interceptorChain字段中保存。
完成Interceptor的加载之后,继续介绍Mybatis的拦截器如何对Executor、ParameterHandler、ResultSetHandler、StatementHandler进行拦截。在Mybatis中使用的这四类的对象,都是通过Configuration.new*()系列的方法创建的。如果配置了用户自定义拦截器,则会在该系列方法中,通过InterceptorChina.pluginAll()方法为目标对象创建代理对象,所以通过Configuration.new*()系列方法得到的对象实际上是一个代理对象。
下面以Configuration.newExecutor()方法为例进行分析,Configuration中的newParameterHandler()方法,newResultSetHandler()方法、newStatementHandler()方法原理类似,
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor 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.pluginAll()方法创建Executor的代理对象
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
InterceptorChain中使用interceptors字段(ArrayList<Interceptor>类型)记录了mybatis-config.xml文件中配置的拦截器。在InterceptorChain.pluginAll()方法中会遍历该interceptors集合,并调用其中的每个元素的plugin()方法创建代理对象,具体的实现如下所示。
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
用户自定义的拦截器的plugin()方法,可以考虑使用Mybatis提供的Plugin工具类实现,它实现了InvocationHandler接口,并提供了一个wrap()静态方法用于创建代理对象。Plugin.wrap()方法的具体实现如下:
public static Object wrap(Object target, Interceptor interceptor) {
//获取用户自定义Interceptor中@Signature注解的信息,getSignatureMap()方法负责处理@Signture注解
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
//获取目标类型
Class<?> type = target.getClass();
//获取目标类型实现的接口,正如前文所述,拦截器可以拦截4类对象都实现了相应的接口,这也是能使用JDK动态代理的方式创建对象的基础
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
//使用JDK动态代理的方式创建代理对象
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
}
在Plugin.invoke()方法中,会将当前调用的方法与signatueMap集合中记录的方法信息进行比较,如果当前的方法是需要被拦截的方法,则调用其intercept()方法进行处理,如果不能被拦截则直接调用target的相应方法。Plugin.invoke()方法的具体实现如下:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//获取当前所在类或集合中,可被当前Interceptor拦截的方法
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//如果当前调用的方法需要被拦截,则调用interceptor.intercept()方法进行拦截处理
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
//如果当前方法不能被拦截,则调用target对象的相应方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
Interceptor.intercept()方法的参数是Invocation对象,其中封装了目标对象、目标方法以及调用目标方法的参数,并提供了proceed()方法调用目标方法,如下所示。所以在Interceptor.intercept()方法中执行完拦截处理之后,如果需要调用目标方法,则通过Invocation.proceed()方法实现
public Object proceed() throws InvocationTargetException, IllegalAccessException{
return method.invoke(target, args);
}