在进行软件开发过程中总会遇到一些公用代码需要被提取出来,这个时候代理是一个好的解决方案,Mybatis也借助JDK代理实现了一套拦截器机制,可以在被拦截的方法前后加入通用逻辑,并通过@Intercepts和@Signature注解指定需要被代理的接口和方法。
一、实例
场景:需要在插入或修改数据库时动态加入修改时间和修改人,并记录下执行数据库操作花费时间。
1. 实现自定义拦截器
@Intercepts({@Signature(type=Executor.class, method="update", args={MappedStatement.class, Object.class})})
public class MyTestInterceptor implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
Object arg = invocation.getArgs()[1];
if(arg instanceof BaseBean) {
BaseBean bean = (BaseBean) arg;
bean.setUpdatetime(System.currentTimeMillis());
bean.setUpdator("login user");
}
long t1 = System.currentTimeMillis();
//执行后面业务逻辑
Object obj = invocation.proceed();
long t2 = System.currentTimeMillis();
System.out.println("cost time:" + (t2-t1) + "ms");
return obj;
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public void setProperties(Properties properties) {
System.out.println("这是配置文件传过来的参数: " + properties.get("name"));
}
}
2. 加入配置文件
<plugins>
<plugin interceptor="org.apache.ibatis.plugin.MyTestInterceptor">
<property name="name" value="abc" />
</plugin>
</plugins>
3. 触发插入操作
看日志updatetime和updator参数是跟intercept中传入的一样
这是配置文件传过来的参数: abc
DEBUG [main] - ==> Preparing: insert into Author (id,username,password,email,bio,updatetime,updator) values (?,?,?,?,?,?,?)
DEBUG [main] - ==> Parameters: 500(Integer), 张三(String), ******(String), 张三@222.com(String), Something...(String), 1478778990865(Long), login user(String)
DEBUG [main] - <== Updates: 1
cost time:773ms
二、源码分析
1. 拦截器接口Interceptor
Interceptor接口是暴露给用户的,通常在自定义Interceptor的plugin方法中实现代理,(其实给Interceptor子类传入被代理对象后理论上可以对该对象做任何事,包括修改数据,替换对象,甚至重新实现一套代理机制等),Mybaits已经实现了代理机制,并且提供了@Intercepts和@Signature使用规则,Mybaits的代理实现封装在Plugin工具类中,只需要调用Plugin的wrap方法即可,看下面测试代码:
public class PluginTest {
@Test
public void mapPluginShouldInterceptGet() {
Map map = new HashMap();
map = (Map) new AlwaysMapPlugin().plugin(map);
assertEquals("Always", map.get("Anything"));
}
@Intercepts({ @Signature(type = Map.class, method = "get", args = { Object.class }) })
public static class AlwaysMapPlugin implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
return "Always";
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public void setProperties(Properties properties) {
}
}
}
对Map接口的get方法进行了拦截,map.get("Anything")的时候会被拦截进入到AlwaysMapPlugin的intercept方法中,在这里被截断返回Always。
2. 注解规则
@Intercepts和@Signature注解,这两个注解定义了该自定义拦截器拦截哪个接口的哪个方法。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Signature {
Class<?> type();
String method();
Class<?>[] args();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
Signature[] value();
}
Signature中有三个属性,分别是目标接口的Class,需要拦截的方法,拦截方法的参数
Intercepts中Signature是个数组,说明可以配置多个拦截规则
3. 代理工具类Plugin
wrap方法中先获取了Interceptor子类上面的@Intercepts和@Signature注解,根据注解可以取到需要被代理的接口,再把这些接口跟代理目标类的接口取交集,并把这些交集接口用JDK实现代理对象并返回,JDK实现代理机制需要一个执行处理类InvocationHandler,Plugin本身也实现了JDK的InvocationHandler类,在构造JDK代理对象的时候传入Plugin对象,
public class Plugin implements InvocationHandler {
private Object target;
private Interceptor interceptor;
private Map<Class<?>, Set<Method>> signatureMap;
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
public static Object wrap(Object target, Interceptor interceptor) {
// interceptor上定义的需要被代理的接口及方法
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 被代理对象的class
Class<?> type = target.getClass();
//上面两者的交集就是需要被代理的接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
//使用jdk实现动态代理
return Proxy.newProxyInstance(type.getClassLoader(), interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
/**
* @Intercepts( {
@Signature(type = Executor.class, method = "query", args = {
MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class }),
@Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) }
* @param interceptor
* @return
*/
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
if (interceptsAnnotation == null) { // issue #251
throw new PluginException(
"No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException(
"Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
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)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
}
调用代理对象方法是会进入到Plugin的invoke方法,invoke方法为动态代理执行方法,执行invoke方法时会判断是否是拦截的方法,如果是则执行自定义interceptor的intercept方法,并把拦截的目标、方法、参数封装到Invocation中传过去, 这就进入到了自定义拦截器的控制范围,可以在待执行的目标方法前后添加逻辑,如上面例子中的MyTestInterceptor。
4. 代理链InterceptorChain
Mybatis在解析配置文件的时候会把自定义的拦截器加到InterceptorChain中,InterceptorChain中有个interceptors集合, 用InterceptorChain可以对接口进行链接拦截,它的pluginAll实际上就是遍历interceptors集合调用plugin。
5. mybatis使用拦截机制
mybatis内部在创建几个关键对象的时候进行interceptorChain.pluginAll(obj),这些对象的上层接口分别是Executor, ParameterHandler, ResultSetHandler, StatementHandler,这4个接口涵盖了了从获取参数到构造sql,再到执行sql解析结果的过程,所以可以在这个过程的任何一个地方进行拦截,只需要配置好拦截的接口及方法参数即可。