在进行软件开发过程当中总会遇到一些公用代码须要被提取出来,这个时候代理是一个好的解决方案,Mybatis也借助JDK代理实现了一套拦截器机制,能够在被拦截的方法先后加入通用逻辑,并经过@Intercepts和@Signature注解指定须要被代理的接口和方法。java
场景:须要在插入或修改数据库时动态加入修改时间和修改人,并记录下执行数据库操做花费时间。sql
@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")); } }
<plugins> <plugin interceptor="org.apache.ibatis.plugin.MyTestInterceptor"> <property name="name" value="abc" /> </plugin> </plugins>
看日志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
Interceptor接口是暴露给用户的,一般在自定义Interceptor的plugin方法中实现代理,(其实给Interceptor子类传入被代理对象后理论上能够对该对象作任何事,包括修改数据,替换对象,甚至从新实现一套代理机制等),Mybaits已经实现了代理机制,而且提供了@Intercepts和@Signature使用规则,Mybaits的代理实现封装在Plugin工具类中,只须要调用Plugin的wrap方法便可,看下面测试代码:apache
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。数组
@Intercepts和@Signature注解,这两个注解定义了该自定义拦截器拦截哪一个接口的哪一个方法。mybatis
@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,须要拦截的方法,拦截方法的参数app
Intercepts中Signature是个数组,说明能够配置多个拦截规则工具
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。测试
Mybatis在解析配置文件的时候会把自定义的拦截器加到InterceptorChain中,InterceptorChain中有个interceptors集合, 用InterceptorChain能够对接口进行连接拦截,它的pluginAll实际上就是遍历interceptors集合调用plugin。
mybatis内部在建立几个关键对象的时候进行interceptorChain.pluginAll(obj),这些对象的上层接口分别是Executor, ParameterHandler, ResultSetHandler, StatementHandler,这4个接口涵盖了了从获取参数到构造sql,再到执行sql解析结果的过程,因此能够在这个过程的任何一个地方进行拦截,只须要配置好拦截的接口及方法参数便可。