# 精尽 MyBatis 源码分析 —— 插件体系(一)之原理 # 1. 概述 本文,我们来分享 MyBatis 的插件模块,对应 `plugin` 包。如下图所示:[![`scripting` 包](27-mybatis-插件体系(一)之原理.assets/01.png)](http://static.iocoder.cn/images/MyBatis/2020_03_18/01.png)`scripting` 包 在 [《精尽 MyBatis 源码解析 —— 项目结构一览》](http://svip.iocoder.cn/MyBatis/intro) 中,简单介绍了这个模块如下: > Mybatis 自身的功能虽然强大,但是并不能完美切合所有的应用场景,因此 MyBatis 提供了插件接口,我们可以通过添加用户自定义插件的方式对 MyBatis 进行扩展。用户自定义插件也可以改变 Mybatis 的默认行为,例如,我们可以拦截 SQL 语句并对其进行重写。 > > 由于用户自定义插件会影响 MyBatis 的核心行为,在使用自定义插件之前,开发人员需要了解 MyBatis 内部的原理,这样才能编写出安全、高效的插件。 - 总的来说,MyBatis 的插件,还是基于**动态代理**来实现。所以,😈 胖友有没发现,动态代理,无处不在。嘿嘿。 - 另外,胖友先看看 [《MyBatis 官方文档 —— 插件》](http://www.mybatis.org/mybatis-3/zh/configuration.html#plugins) 文档,对 MyBatis 插件的概念和配置,再有一个了解。 本文涉及的类如下图所示:[![类图](27-mybatis-插件体系(一)之原理.assets/02.png)](http://static.iocoder.cn/images/MyBatis/2020_03_18/02.png)类图 下面,我们逐个类来瞅瞅。 # 2. Interceptor `org.apache.ibatis.plugin.Interceptor` ,拦截器接口。代码如下: ``` // Interceptor.java public interface Interceptor { /** * 拦截方法 * * @param invocation 调用信息 * @return 调用结果 * @throws Throwable 若发生异常 */ Object intercept(Invocation invocation) throws Throwable; /** * 应用插件。如应用成功,则会创建目标对象的代理对象 * * @param target 目标对象 * @return 应用的结果对象,可以是代理对象,也可以是 target 对象,也可以是任意对象。具体的,看代码实现 */ Object plugin(Object target); /** * 设置拦截器属性 * * @param properties 属性 */ void setProperties(Properties properties); } ``` - 关于三个方法,胖友看看代码注释。 ## 2.1 示例 我们来看看 `org.apache.ibatis.plugin.PluginTest` ,提供了 AlwaysMapPlugin 示例。代码如下: ``` @Intercepts({ @Signature(type = Map.class, method = "get", args = {Object.class})}) // <1> public static class AlwaysMapPlugin implements Interceptor { @Override // <4> public Object intercept(Invocation invocation) throws Throwable { return "Always"; } @Override // <2> public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override // <3> public void setProperties(Properties properties) { } } ``` - `<1>` 处,通过 `@Intercepts` 和 `@Signature` 注解,定义了需要拦截的方法为 Map 类型、方法为 `"get"` 方法,方法参数为 `Object.class` 。详细解析,见 TODO。 - `<2>` 处,在实现方法 `#plugin(Object target)` 方法内部,他调用 `Plugin#wrap(Object target, Interceptor interceptor)` 方法,执行代理对象的创建。详细解析,见 TODO。 - `<3>` 处,在实现方法 `#setProperties(Properties properties)` 方法内部,暂未做任何实现。此处可以实现,若 AlwaysMapPlugin 有属性,可以从 `properties` 获取一些需要的属性值。 - `<4>` 处,在实现方法 `#intercept(Invocation invocation)` 方法,直接返回 `"Always"` 字符串。也就是说,当所有的 `target` 类型为 Map 类型,并且调用 `Map#get(Object)` 方法时,返回的都是 `"Always"` 。 这个示例,胖友可以跑下 PluginTest 单元测试里的方法,可以更加好理解。 ## 2.2 Invocation `org.apache.ibatis.plugin.Invocation` ,方法调用信息,作为 `Interceptor#intercept(Invocation invocation)` 的方法参数。代码如下: ``` // Invocation.java public interface Interceptor { /** * 拦截方法 * * @param invocation 调用信息 * @return 调用结果 * @throws Throwable 若发生异常 */ Object intercept(Invocation invocation) throws Throwable; /** * 应用插件。如应用成功,则会创建目标对象的代理对象 * * @param target 目标对象 * @return 应用的结果对象,可以是代理对象,也可以是 target 对象,也可以是任意对象。具体的,看代码实现 */ Object plugin(Object target); /** * 设置拦截器属性 * * @param properties 属性 */ void setProperties(Properties properties); } ``` # 3. InterceptorChain `org.apache.ibatis.plugin.InterceptorChain` ,拦截器 Interceptor 链。 ## 3.1 构造方法 ``` // InterceptorChain.java /** * 拦截器数组 */ private final List interceptors = new ArrayList<>(); ``` ## 3.2 addInterceptor `#addInterceptor(Interceptor interceptor)` 方法,添加拦截器。代码如下: ``` // InterceptorChain.java public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } ``` ------ 该方法在 Configuration 的 `#pluginElement(XNode parent)` 方法中被调用,代码如下: ``` // XMLConfigBuilder.java /** * 解析 标签,添加到 {@link Configuration#interceptorChain} 中 * * @param parent 节点 * @throws Exception 发生异常时 */ private void pluginElement(XNode parent) throws Exception { if (parent != null) { // 遍历 标签 for (XNode child : parent.getChildren()) { String interceptor = child.getStringAttribute("interceptor"); Properties properties = child.getChildrenAsProperties(); // <1> 创建 Interceptor 对象,并设置属性 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); interceptorInstance.setProperties(properties); // 添加到 configuration 中 configuration.addInterceptor(interceptorInstance); } } } ``` - `<1>` 处,创建 Interceptor 对象,并调用 `Interceptor#setProperties(properties)` 方法,设置拦截器的属性。 - `<2>` 处,调用 `Configuration#addInterceptor(interceptorInstance)` 方法,添加到 `Configuration.interceptorChain` 中。代码如下: ``` // Configuration.java /** * 拦截器链 */ protected final InterceptorChain interceptorChain = new InterceptorChain(); public void addInterceptor(Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); } ``` ## 3.3 pluginAll `#pluginAll(Object target)` 方法,应用所有拦截器到指定目标对象。代码如下: ``` // InterceptorChain.java public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } ``` ------ 该方法被 Configuration 的如下四处方法中调用:[![调用处](27-mybatis-插件体系(一)之原理.assets/03.png)](http://static.iocoder.cn/images/MyBatis/2020_03_18/03.png)调用处 - 一共可以有四种目标对象类型可以被拦截:1)Executor;2)StatementHandler;3)ParameterHandler;4)ResultSetHandler 。 # 4. @Intercepts `org.apache.ibatis.plugin.@Intercepts` ,拦截器注解。代码如下: ``` // Intercepts.java @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Intercepts { /** * @return 拦截的方法签名的数组 */ Signature[] value(); } ``` ## 4.1 @Signature `org.apache.ibatis.plugin.@Signature` ,方法签名的注解。代码如下: ``` // Signature.java @Documented @Retention(RetentionPolicy.RUNTIME) @Target({}) public @interface Signature { /** * @return 类 */ Class type(); /** * @return 方法名 */ String method(); /** * @return 参数类型 */ Class[] args(); } ``` # 5. Plugin `org.apache.ibatis.plugin.Plugin` ,实现 InvocationHandler 接口,插件类,一方面提供创建动态代理对象的方法,另一方面实现对指定类的指定方法的拦截处理。 注意,Plugin 是 MyBatis 插件体系的核心类。 ## 5.1 构造方法 ``` // Plugin.java /** * 目标对象 */ private final Object target; /** * 拦截器 */ private final Interceptor interceptor; /** * 拦截的方法映射 * * KEY:类 * VALUE:方法集合 */ private final Map, Set> signatureMap; private Plugin(Object target, Interceptor interceptor, Map, Set> signatureMap) { this.target = target; this.interceptor = interceptor; this.signatureMap = signatureMap; } ``` ## 5.2 wrap `#wrap(Object target, Interceptor interceptor)` **静态**方法,创建目标类的代理对象。代码如下: ``` // Plugin.java public static Object wrap(Object target, Interceptor interceptor) { // <1> 获得拦截的方法映射 Map, Set> signatureMap = getSignatureMap(interceptor); // <2> 获得目标类的类型 Class type = target.getClass(); // <2> 获得目标类的接口集合 Class[] interfaces = getAllInterfaces(type, signatureMap); // <3.1> 若有接口,则创建目标对象的 JDK Proxy 对象 if (interfaces.length > 0) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); // 因为 Plugin 实现了 InvocationHandler 接口,所以可以作为 JDK 动态代理的调用处理器 } // <3.2> 如果没有,则返回原始的目标对象 return target; } ``` - `<1>` 处,调用 `#getSignatureMap(Interceptor interceptor)` 方法,获得拦截的方法映射。代码如下: ``` // Plugin.java private static Map, Set> getSignatureMap(Interceptor interceptor) { Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); // issue #251 if (interceptsAnnotation == null) { throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } Signature[] sigs = interceptsAnnotation.value(); Map, Set> signatureMap = new HashMap<>(); for (Signature sig : sigs) { Set methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>()); 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; } ``` - 基于 `@Intercepts` 和 `@Signature` 注解。 - `<2>` 处,调用 `#getAllInterfaces(Class type, Map, Set> signatureMap)` 方法,获得目标类的接口集合。代码如下: ``` // Plugin.java private static Class[] getAllInterfaces(Class type, Map, Set> signatureMap) { // 接口的集合 Set> interfaces = new HashSet<>(); // 循环递归 type 类,机器父类 while (type != null) { // 遍历接口集合,若在 signatureMap 中,则添加到 interfaces 中 for (Class c : type.getInterfaces()) { if (signatureMap.containsKey(c)) { interfaces.add(c); } } // 获得父类 type = type.getSuperclass(); } // 创建接口的数组 return interfaces.toArray(new Class[interfaces.size()]); } ``` - 从这里可以看出,`@Signature` 注解的 `type` 属性,必须是**接口**。 - `<3.1>` 处,**情况一**,若有接口,则创建目标对象的 JDK Proxy 对象。 - `<3.2>` 处,**情况二**,若无接口,则返回原始的目标对象。 ## 5.3 invoke ``` // Plugin.java @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 获得目标方法是否被拦截 Set 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); } } ``` - 具体,见代码注释。简单~ # 666. 彩蛋 在地铁上,撸完了这篇博客,小文一篇。后续,我们会找一些 MyBatis 的插件,详细解析。另外,MyBatis 并未提供自带的插件实现。 参考和推荐如下文章: - 祖大俊 [《Mybatis3.4.x技术内幕(十九):Mybatis之plugin插件设计原理》](https://my.oschina.net/zudajun/blog/738973) - 田小波 [《MyBatis 源码分析 - 插件机制》](https://www.tianxiaobo.com/2018/08/26/MyBatis-源码分析-插件机制/) - 徐郡明 [《MyBatis 技术内幕》](https://item.jd.com/12125531.html) 的 [「4.1 插件模块」](https://svip.iocoder.cn/MyBatis/plugin-1/#) 小节