Mybatis源码解读-插件
插件允许对Mybatis的四大对象(Executor、ParameterHandler、ResultSetHandler、StatementHandler)进行拦截
问题
Mybatis插件的注册顺序与调用顺序的关系?
使用
在讲源码之前,先看看如何自定义插件。
mybatis-demo,官方文档
-
创建插件类
自定义插件类需要实现Interceptor
// 注解配置需要拦截的类以及方法 @Intercepts({ @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}) }) // 实现Interceptor接口 public class SqlLogPlugin implements Interceptor { /** * 具体的拦截逻辑 */ @Override public Object intercept(Invocation invocation) throws Throwable { long begin = System.currentTimeMillis(); try { return invocation.proceed(); } finally { long time = System.currentTimeMillis() - begin; System.out.println("sql 运行了 :" + time + " ms"); } } /** * 判断是否需要进行代理 * 此方法有默认实现,一般无需重写 */ /*@Override public Object plugin(Object target) { return Plugin.wrap(target, this); }*/ /** * 自定义参数 */ @Override public void setProperties(Properties properties) { // 这是xml中配置的参数 properties.forEach((k, v) -> { System.out.printf("SqlLogPlugin---key:%s, value:%s%n", k, v); }); } }
-
注册
在配置文件注册插件
<plugins> <plugin interceptor="com.wjw.project.intercaptor.SqlLogPlugin"> <property name="key1" value="root"/> <property name="key2" value="123456"/> </plugin> </plugins>
-
效果
控制输出
SqlLogPlugin---key:key1, value:root SqlLogPlugin---key:key2, value:123456 sql 运行了 :17 ms
源码
原理:Mybatis四大对象创建时,都回去判断是否满足插件的拦截条件,满足,则四大对象就会被Plugin
类代理
源码分3部分讲。注册、包装、调用
-
注册
xml方式的注册,是在XMLConfigBuilder#pluginElement完成的。
不明觉厉的同学,请参考上一篇文章:Mybatis源码解读-配置加载和Mapper的生成
// XMLConfigBuilder#pluginElement(XNode parent) private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { // 读取插件的类路径 String interceptor = child.getStringAttribute("interceptor"); // 读取自定义参数 Properties properties = child.getChildrenAsProperties(); // 反射实例化插件 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance(); interceptorInstance.setProperties(properties); // 将插件添加到配置的插件链中,等待后续使用 configuration.addInterceptor(interceptorInstance); } } }
configuration.addInterceptor做得操作很简单
-
包装
上面讲了插件的注册,最后调用的是configuration.addInterceptor,最终调用的是InterceptorChain#addInterceptor
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<>(); /* * 每当四大对象创建时,都会执行此方法 * 满足拦截条件,则返回Plugin代理,否则返回原对象 * @param target Mybatis四大对象之一 */ public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { // 调用每个插件的plugin方法,判断是否需要代理 target = interceptor.plugin(target); } return target; } // 将拦截器添加interceptors集合中存起来 public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
我们案例是拦截StatementHandler,所以也以此为例
/* * 这是创建StatementHandler的方法 * Configuration#newStatementHandler */ public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); // 可以看到创建完StatementHandler之后,会调用InterceptorChain的pluginAll方法 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; }
那么我们再仔细分析下
pluginAll
方法,pluginAll
调用的是每个插件的plugin
方法default Object plugin(Object target) { return Plugin.wrap(target, this); }
可以看到,最终调用的是
Plugin.*wrap*
/* * Plugin#wrap * 判断是否满足插件的拦截条件,是则返回代理类,否则返回原对象 */ public static Object wrap(Object target, Interceptor interceptor) { // 获取插件的拦截信息(就是获取@Intercepts注解的内容) Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); // 判断是否满足拦截条件 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { // 满足拦截条件则返回Plugin代理对象 return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } // 不满足则返回原对象 return target; }
-
调用
在上一个
包装
步骤提到,满足条件会返回代理对象,即调用StatementHandler
的所有方法,都会经过Plugin
的invoke
方法,去看看// Plugin#invoke 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)) { // 满足拦截条件,则调用插件的intercept方法 return interceptor.intercept(new Invocation(target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } }