【Mybatis】【插件】Mybatis源码解析-插件机制
1 前言
这节我们来看看插件,插件是来干啥的呢?一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的 好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行 拓展,使其能够更好的工作。以 MyBatis 为例,我们可基于 MyBatis 插件机制实现分页、 分表,监控等功能。那么我们这节就来看看 Mybatis 里的插件。
2 插件类型
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。
3 插件的简单使用
我们简单来个插件的示例:
@Intercepts({@Signature( type= Executor.class, method = "update", args = {MappedStatement.class, Object.class})}) public class MyInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { System.out.println("===================my"); return invocation.proceed(); } }
<plugins> <plugin interceptor="org.apache.ibatis.test.plugin.MyInterceptor"></plugin> </plugins>
这样 MyBatis 在启动时可以加载插件, 并保存插件实例到相关对象(InterceptorChain,拦截器链)中。待准备工作做完后,MyBatis 处于就绪状态。我们在执行 SQL 时,需要先通过 DefaultSqlSessionFactory 创 建 SqlSession 。Executor 实例会在创建 SqlSession 的过程中被创建,Executor 实例创建完毕 后,MyBatis 会通过 JDK 动态代理为实例生成代理类。这样,插件逻辑即可在 Executor 相 关方法被调用前执行。以上就是 MyBatis 插件机制的基本原理。接下来,我们来看一下原 理背后对应的源码是怎样的。
4 源码分析
4.1 入场时机
本节,我将以 Executor 为例,分析 MyBatis 是如何为 Executor 实例植入插件逻辑的。 Executor 实例是在开启 SqlSession 时被创建的,因此,下面我们从源头进行分析。先来看 一下 SqlSession 开启的过程:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { ... // 创建我们的执行器 进入这里创建执行器 final Executor executor = configuration.newExecutor(tx, execType); // 创建 SqlSession 对象 return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
Executor 的创建过程封装在 Configuration 中,我们跟进去:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { ... if (cacheEnabled) { executor = new CachingExecutor(executor); } // 插件 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
如上,newExecutor 方法在创建好 Executor 实例后,紧接着通过拦截器链 interceptorChain 为 Executor 实例植入代理逻辑。那下面我们看一下 InterceptorChain 的代码是怎样的。
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<>(); public Object pluginAll(Object target) { // 遍历我们的插件拦截器 for (Interceptor interceptor : interceptors) { // 调用拦截器的 plugin 方法进行插件的创建 其实就是会创建代理类 target = interceptor.plugin(target); } return target; } // 添加插件 public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } // 获取插件列表 public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
以上是 InterceptorChain 的全部代码,比较简单。它的 pluginAll 方法会调用具体插件的 plugin 方法植入相应的插件逻辑。如果有多个插件,则会多次调用 plugin 方法,最终生成一 个层层嵌套的代理类。那我们就来看下 plugin方法是如何创建代理的。
4.2 plugin 决定是否创建代理
plugin 方法是由具体的插件类实现,不过该方法代码一般比较固定,我们看一下:
// Interceptor 默认实现 default Object plugin(Object target) { // 调用 wrap 方法进行包装 return Plugin.wrap(target, this); }
/** * Plugin 生成代理 * 这个代理可能会嵌套生成,越后嵌套越先执行奥 * @param target 目标对象 * @param interceptor 插件拦截 * @return 代理对象或者目标对象 */ public static Object wrap(Object target, Interceptor interceptor) { // 解析插件拦截器的信息 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // 当前被拦截对象的类型 Class<?> type = target.getClass(); /** * 判断当前的被拦截对象的类型是否存在拦截 * 比如当前的对象是 statementHandler的 而当前的拦截器是配置的 Executor -> [xxx] * 那么当前方法就无需这个插件 */ Class<?>[] interfaces = getAllInterfaces(type, signatureMap); // 当发现需要插件增强的话,创建当前对象的代理类 jdk方式的 if (interfaces.length > 0) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, // 注意这个这是我们的增强 也就是拦截会执行 Plugin 中的 invoke new Plugin(target, interceptor, signatureMap)); } // 不需要的话 直接返回当前对象 return target; }
如上,plugin 方法在内部调用了 Plugin 类的 wrap 方法,用于为目标对象生成代理。Plugin 类实现了InvocationHandler接口,因此它可以作为参数传给 Proxy 的 newProxyInstance方法。
我们再来简单看下 getSignatureMap 方法就是解析我们的插件类获取信息的,我们看下:
/** * 我们来拿一个例子给你解析 * @Intercepts({@Signature( * type= Executor.class, * method = "update", * args = {MappedStatement.class, Object.class})}) * 那么返回的就是 Executor -> [update方法] * @param interceptor 就是我们的插件实现类 * @return 返回类型和方法集合的映射 */ private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { // 获取到我们的 @Intercepts 注解信息 Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); // 没注解的话直接报错 if (interceptsAnnotation == null) { throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } // 获取到 @Signature 数组 Signature[] sigs = interceptsAnnotation.value(); /** * 用于保存结果 相同类的话会放进同一个 key 中 * key 哪种插件类型 value 就是拦截的方法列表 */ Map<Class<?>, Set<Method>> signatureMap = new HashMap<>(); for (Signature sig : sigs) { // 初始化 key 的集合 Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>()); try { // 根据拦截类型 type 以及参数列表获取到对应的拦截方法 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; }
4.3 插件执行逻辑
Plugin 实现了 InvocationHandler 接口,因此它的 invoke 方法会拦截所有的方法调用。 invoke 方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:
// Plugin 当代理对象执行方法的时候,会代理执行到此 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { /** * 获取当前要执行的方法来源于哪个类并获取是否存在方法增强 * 比如当前要执行的是 Executor 的 query方法 而当前拦截器的类型方法映射为 Executor -> [update] * 那么 query 就不在增强范围内,所以就会直接执行目标方法 */ 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); } }
invoke 方法的代码比较少,逻辑不难理解。首先,invoke 方法会检测被拦截方法是否配 置在插件的 @Signature 注解中,若是,则执行插件逻辑,否则执行被拦截方法。插件逻辑 封装在 intercept 中,该方法的参数类型为 Invocation。Invocation 主要用于存储目标类,方法 以及方法参数列表。下面简单看一下该类的定义。
public class Invocation { // 被代理对象 private final Object target; // 当前拦截的方法 private final Method method; // 方法参数 private final Object[] args; public Invocation(Object target, Method method, Object[] args) { // 初始化 this.target = target; this.method = method; this.args = args; } public Object getTarget() { return target; } public Method getMethod() { return method; } public Object[] getArgs() { return args; } public Object proceed() throws InvocationTargetException, IllegalAccessException { // 下一个拦截器执行或者目标方法执行 return method.invoke(target, args); } }
关于插件的机制就到这里了,他的代理记住是一个套一个的逐层执行的哈。
5 小结
这节我们大致看了一下插件的入场以及插件的实现机制,有理解不对的地方欢迎指正哈。