手写Mybatis:第17章-Plugin插件功能实现

文章目录

  • 一、目标:Plugin插件
  • 二、设计:Plugin插件
  • 三、实现:Plugin插件
    • [3.1 工程结构](#3.1 工程结构)
    • [3.2 Plugin插件代理模式类图](#3.2 Plugin插件代理模式类图)
    • [3.3 自定义拦截注解](#3.3 自定义拦截注解)
      • [3.3.1 方法签名](#3.3.1 方法签名)
      • [3.3.2 拦截注解](#3.3.2 拦截注解)
    • [3.4 拦截器接口定义](#3.4 拦截器接口定义)
      • [3.4.1 调用信息](#3.4.1 调用信息)
      • [3.4.2 拦截器接口](#3.4.2 拦截器接口)
    • [3.5 类代理包装操作](#3.5 类代理包装操作)
      • [3.5.1 获取签名方法](#3.5.1 获取签名方法)
      • [3.5.2 创建反射代理](#3.5.2 创建反射代理)
      • [3.5.3 包裹反射方法](#3.5.3 包裹反射方法)
    • [3.6 拦截器链和配置项修改](#3.6 拦截器链和配置项修改)
      • [3.6.1 拦截器链](#3.6.1 拦截器链)
      • [3.6.2 配置项](#3.6.2 配置项)
    • [3.7 解析XML插件配置](#3.7 解析XML插件配置)
  • 四、测试:Plugin插件
    • [4.1 自定义插件](#4.1 自定义插件)
    • [4.2 修改XML配置文件](#4.2 修改XML配置文件)
    • [4.3 单元测试](#4.3 单元测试)
  • 五、总结:Plugin插件

一、目标:Plugin插件

💡 Mbatis Plugin的插件功能

  • Mybatis Plugin 的插件功能是非常重要的一个功能点,包括我们可以结合插件的扩展:分页、数据库表路由、监控日志等。
  • 这些核心功能的扩展,都是来自于 Mybatis Plugin 提供对类的代理扩展,并在代理中调用我们自定义插件的逻辑行为。
  • 对于插件的使用,我们按照 Mybatis 框架提供的拦截器接口,实现自己的功能实现类,并把这个类配置到 MybatisXML 配置中。

二、设计:Plugin插件

💡 Mybatis Plugin 插件功能的实现设计

  • Mybatis Plugin 插件功能的实现设计也是一种 依赖倒置 的实现方式,让插件的功能依赖于抽象接口,不依赖于具体的实现。
    • 这个过程中对抽象进行编程,不对实现进行编程,这样就降低了客户与实现模块间的耦合。
  • Mybatis Plugin 插件的具体实现落地,由框架提供拦截器接口,交由使用方实现,并通过匹配的方式把实现添加到 Mybatis 框架中。
    • 这样在具体的监听点上,包括:ParameterHandler、ResultSetHandler、StatementHandler、Executor。每一个创建过程中,都可以把插件部分嵌入进去
    • 当调用任意类对应的接口方法时,都能调用到用户实现拦截器接口的插件内容,也就是实现类自定义扩展的效果。
  • XML 解析为入口,解析用户自定义插件,提取拦截器接口实现类,保存到配置项的拦截器链对象中。
  • 接下来在创建语句处理器 StatementHandler 时,使用代理的方式构建实现类,并把拦截器作为对象中调用过程的一部分。
  • 那么这个拦截器的调用是一种方法过滤判断的方式,通过拦截器实现类上配置的注解,提取要拦截的方法。
  • Mybatis 框架执行到这些节点时,如调用 StatementHandler.prepare 方法时,则进行拦截器执行用户扩展的插件操作。

三、实现:Plugin插件

3.1 工程结构

java 复制代码
mybatis-step-16
|-src
  |-main
  | |-java
  |   |-com.lino.mybatis
    |     |-annotations
    |     | |-Delete.java
  |     | |-Insert.java
  |     | |-Select.java
    |     | |-Update.java
    |     |-binding
    |     | |-MapperMethod.java
  |     | |-MapperProxy.java
  |     | |-MapperProxyFactory.java
    |     | |-MapperRegistry.java
    |     |-builder
    |     | |-annotations
    |     | | |-MapperAnnotationBuilder.java
    |     | |-xml
    |     | | |-XMLConfigBuilder.java
    |     | | |-XMLMapperBuilder.java
    |     | | |-XMLStatementBuilder.java
    |     | |-BaseBuilder.java
    |     | |-MapperBuilderAssistant.java
    |     | |-ParameterExpression.java
    |     | |-ResultMapResolver.java
    |     | |-SqlSourceBuilder.java
    |     | |-StaticSqlSource.java
  |     |-datasource
  |     | |-druid
  |     | | |-DruidDataSourceFacroty.java
  |     | |-pooled
  |     | | |-PooledConnection.java
  |     | | |-PooledDataSource.java
  |     | | |-PooledDataSourceFacroty.java
  |     | | |-PoolState.java
  |     | |-unpooled
  |     | | |-UnpooledDataSource.java
  |     | | |-UnpooledDataSourceFacroty.java
  |     | |-DataSourceFactory.java
  |     |-executor
  |     | |-keygen
  |     | | |-Jdbc3KeyGenerator.java
  |     | | |-KeyGenerator.java
  |     | | |-NoKeyGenerator.java
  |     | | |-SelectKeyGenerator.java
  |     | |-parameter
  |     | | |-ParameterHandler.java
  |     | |-result
  |     | | |-DefaultResultContext.java
  |     | | |-DefaultResultHandler.java
  |     | |-resultset
  |     | | |-DefaultResultSetHandler.java
  |     | | |-ResultSetHandler.java
  |     | | |-ResultSetWrapper.java
  |     | |-statement
  |     | | |-BaseStatementHandler.java
  |     | | |-PreparedStatementHandler.java
  |     | | |-SimpleStatementHandler.java
  |     | | |-StatementHandler.java
  |     | |-BaseExecutor.java
  |     | |-Executor.java
  |     | |-SimpleExecutor.java
    |     |-io
    |     | |-Resources.java
    |     |-mapping
    |     | |-BoundSql.java
    |     | |-Environment.java
    |     | |-MappedStatement.java
    |     | |-ParameterMapping.java
    |     | |-ResultFlag.java
    |     | |-ResultMap.java
    |     | |-ResultMapping.java
    |     | |-SqlCommandType.java
    |     | |-SqlSource.java
    |     |-parsing
    |     | |-GenericTokenParser.java
    |     | |-TokenHandler.java
    |     |-plugin
    |     | |-Interceptor.java
    |     | |-InterceptorChain.java
    |     | |-Intercepts.java
    |     | |-Invocation.java
    |     | |-Plugin.java
    |     | |-Signature.java
  |     |-reflection
  |     | |-factory
  |     | | |-DefaultObjectFactory.java
  |     | | |-ObjectFactory.java
  |     | |-invoker
  |     | | |-GetFieldInvoker.java
  |     | | |-Invoker.java
  |     | | |-MethodInvoker.java
  |     | | |-SetFieldInvoker.java
  |     | |-property
  |     | | |-PropertyNamer.java
  |     | | |-PropertyTokenizer.java
  |     | |-wrapper
  |     | | |-BaseWrapper.java
  |     | | |-BeanWrapper.java
  |     | | |-CollectionWrapper.java
  |     | | |-DefaultObjectWrapperFactory.java
  |     | | |-MapWrapper.java
  |     | | |-ObjectWrapper.java
  |     | | |-ObjectWrapperFactory.java
  |     | |-MetaClass.java
  |     | |-MetaObject.java
  |     | |-Reflector.java
  |     | |-SystemMetaObject.java
  |     |-scripting
  |     | |-defaults
  |     | | |-DefaultParameterHandler.java
  |     | | |-RawSqlSource.java
  |     | |-xmltags
  |     | | |-DynamicContext.java
  |     | | |-DynamicSqlSource.java
    |     | | |-ExpressionEvaluator.java
    |     | | |-IfSqlNode.java
  |     | | |-MixedSqlNode.java
    |     | | |-OgnlCache.java
    |     | | |-OgnlClassResolver.java
  |     | | |-SqlNode.java
    |     | | |-StaticTextSqlNode.java
  |     | | |-TextSqlNode.java
    |     | | |-TrimSqlNode.java
  |     | | |-XMLLanguageDriver.java
  |     | | |-XMLScriptBuilder.java
  |     | |-LanguageDriver.java
  |     | |-LanguageDriverRegistry.java
    |     |-session
    |     | |-defaults
    |     | | |-DefaultSqlSession.java
    |     | | |-DefaultSqlSessionFactory.java
    |     | |-Configuration.java
    |     | |-ResultContext.java
    |     | |-ResultHandler.java
    |     | |-RowBounds.java
    |     | |-SqlSession.java
    |     | |-SqlSessionFactory.java
    |     | |-SqlSessionFactoryBuilder.java
    |     | |-TransactionIsolationLevel.java
    |     |-transaction
    |     | |-jdbc
    |     | | |-JdbcTransaction.java
    |     | | |-JdbcTransactionFactory.java
    |     | |-Transaction.java
    |     | |-TransactionFactory.java
    |     |-type
    |     | |-BaseTypeHandler.java
    |     | |-DateTypeHandler.java
    |     | |-IntegerTypeHandler.java
    |     | |-JdbcType.java
    |     | |-LongTypeHandler.java
    |     | |-SimpleTypeRegistry.java
    |     | |-StringTypeHandler.java
    |     | |-TypeAliasRegistry.java
    |     | |-TypeHandler.java
    |     | |-TypeHandlerRegistry.java
  |-test
    |-java
    | |-com.lino.mybatis.test
    | |-dao
    | | |-IActivityDao.java
    | |-plugin
    | | |-TestPlugin.java
    | |-po
    | | |-Activity.java
    | |-ApiTest.java
        |-resources
      |-mapper
      | |-Activity_Mapper.xml
          |-mybatis-config-datasource.xml

3.2 Plugin插件代理模式类图

  • 首先是以扩展 XMLConfigBuilder 解析自定义插件配置,将自定义插件写入配置项的拦截器链中。而每一个用户实现的拦截器接口都包装了插件的代理操作。
    • 这就像是一个代理器的盒子,把原有类的行为和自定义的插件行为,使用代理包装到一个调度方法中。
  • 接下来是对自定义插件的激活部分,也就是把这个插件的调用挂在哪个节点下。
    • 这里通过在 Configuration 配置项在创建各类操作时,把自定义插件嵌入进去。
  • 基于 StatementHandler 创建语句处理器时,使用拦截器链将定义插件包裹到 StatementHandler 目标方法中,这样在后续调用 StatementHandler 的方法时,就顺便调用自定义实现的拦截器了。

3.3 自定义拦截注解

  • 关于 Mybatis Plugin 插件的使用,需要实现 Interceptor 拦截器接口,完成使用方自身功能的扩展。但也需要基于注解来指定,需要在哪个类的哪个方法下,做调用处理。
    • 例如:@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})
    • 这就是一个插件实现类上的注解,指定了在 StatementHandler 语句处理器调用入参为 Connectionprepare 方法准备语句阶段,完成自定义插件的处理。

3.3.1 方法签名

Signature.java

java 复制代码
package com.lino.mybatis.plugin;

/**
 * @description: 方法签名
 */
public @interface Signature {

    /**
     * 被拦截类
     */
    Class<?> type();

    /**
     * 被拦截类的方法
     */
    String method();

    /**
     * 被拦截类的方法的参数
     */
    Class<?>[] args();
}
  • Signature 方法签名接口,定义了被拦截类的 type,也就是如我们拦截 StatementHandler 语句处理器。
  • 另外就是在这个类下需要根据方法名称和参数来确定是这个类下的哪个方法,只有这2个信息都存在,才能确定唯一类下的方法。

3.3.2 拦截注解

Intercepts.java

java 复制代码
package com.lino.mybatis.plugin;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @description: 拦截注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {

    Signature[] value();
}
  • Intercepts 注解一个目的是作为标记存在,所有的插件实现都需要有这个自定义的注解标记。
  • 另外这个注解中还有另外一个注解的存在,就是方法签名注解,用于定位需要在哪个类的哪个方法下完成插件的调用。

3.4 拦截器接口定义

  • 需要定义一个拦截器接口,这个是面向抽象编程的依赖倒置的入口,插件只定义标准,具体调用处理结果交由使用方决定。

3.4.1 调用信息

Invocation.java

java 复制代码
package com.lino.mybatis.plugin;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @description: 调用信息
 */
public class Invocation {

    /**
     * 调用的对象
     */
    private Object target;
    /**
     * 调用的方法
     */
    private Method method;
    /**
     * 调用的参数
     */
    private 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;
    }

    /**
     * 放行:调用执行
     *
     * @return 对象
     * @throws InvocationTargetException 调用对象异常
     * @throws IllegalAccessException    异常
     */
    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        return method.invoke(target, args);
    }
}

3.4.2 拦截器接口

Interceptor.java

java 复制代码
package com.lino.mybatis.plugin;

import java.util.Properties;

/**
 * @description: 拦截器接口
 */
public interface Interceptor {

    /**
     * 拦截,使用方实现
     *
     * @param invocation 调用信息
     * @return 对象
     * @throws Throwable
     */
    Object intercept(Invocation invocation) throws Throwable;

    /**
     * 代理
     *
     * @param target 代理对象
     * @return Object
     */
    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 设置属性
     *
     * @param properties 属性
     */
    default void setProperties(Properties properties) {
        // NOP
    }
}
  • Interceptor 提供了3个方法,一个 intercept 方法是交由使用方实现的,另外2个算是 default 方法,使用方不需要做实现。
  • 这样每一个 Interceptor 的实现类就都通过解析的方式,注册到拦截器链中,在后续需要基于 StatementHandler 语句处理器创建时,就可以通过代理的方式,把自定义插件包装到代理方法中。
  • setProperties 方法是属性处理,相当于可以把用户配置到 XML 下插件中的属性信息,通过这里传递。

3.5 类代理包装操作

  • 插件的实现核心逻辑,就在 Plugin 插件这个类下处理的。
  • Plugin 通过实现 InvocationHandler 代理接口,在 invoke 方法中包装对插件的处理。
  • 当任何一个被代理的类,包括:ParameterHandler、ResultSetHandler、StatementHandler、Executor,在执行方法调用时,就可以调用到用户自定义的插件。

Plugin.java

java 复制代码
package com.lino.mybatis.plugin;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * @description: 代理模式插件
 */
public class Plugin implements InvocationHandler {

    private Object target;
    private Interceptor interceptor;
    private Map<Class<?>, Set<Method>> signatureMap;

    public Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
        this.target = target;
        this.interceptor = interceptor;
        this.signatureMap = signatureMap;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 获取声明的方法列表
        Set<Method> methods = signatureMap.get(method.getDeclaringClass());
        // 过滤需要拦截的方法
        if (methods != null && methods.contains(method)) {
            // 调用 Interceptor#intercept 插入自己的反射逻辑
            return interceptor.intercept(new Invocation(target, method, args));
        }
        return method.invoke(target, args);
    }

    /**
     * 用代理把自定义插件行为包裹到目标方法中,也就是 Plugin.invoke 的过滤调用
     *
     * @param target      调用对象
     * @param interceptor 拦截器接口
     * @return 返回对象
     */
    public static Object wrap(Object target, Interceptor interceptor) {
        // 取得签名
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        // 取得要改变行为的类(ParameterHandler|ResultSetHandler|StatementHandler|Executor),目前只添加了 StatementHandler
        Class<?> type = target.getClass();
        // 取得接口
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        // 创建代理(StatementHandler)
        if (interfaces.length > 0) {
            // Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
            return Proxy.newProxyInstance(
                    type.getClassLoader(),
                    interfaces,
                    new Plugin(target, interceptor, signatureMap)
            );
        }
        return target;
    }

    /**
     * 获取方法签名组 Map
     *
     * @param interceptor 拦截器接口
     * @return 方法签名组 Map
     */
    private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
        // 取得 Intercepts 注解,例子可参见 TestPlugin.java
        Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
        // 必须得有 Intercepts 注释,没有报错
        if (interceptsAnnotation == null) {
            throw new RuntimeException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
        }
        // value是数组型,Signature的数组
        Signature[] sigs = interceptsAnnotation.value();
        // 每个 class 类有多个可能有多个 Method 需要被拦截
        Map<Class<?>, Set<Method>> signatureMap = new HashMap<>(16);
        for (Signature sig : sigs) {
            Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
            try {
                // 例如获取到方法;StatementHandler.prepare(Connection connection)、StatementHandler.parameterize(Statement statement)...
                Method method = sig.type().getMethod(sig.method(), sig.args());
                methods.add(method);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
            }
        }
        return signatureMap;
    }

    /**
     * 获取接口
     *
     * @param type         类类型
     * @param signatureMap 方法签名组 Map
     * @return 接口列表
     */
    private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
        Set<Class<?>> interfaces = new HashSet<>();
        while (type != null) {
            for (Class<?> c : type.getInterfaces()) {
                // 拦截 ParameterHandler|ResultSetHandler|StatementHandler|Executor
                if (signatureMap.containsKey(c)) {
                    interfaces.add(c);
                }
            }
            type = type.getSuperclass();
        }
        return interfaces.toArray(new Class<?>[0]);
    }
}

3.5.1 获取签名方法

java 复制代码
/**
 * 获取方法签名组 Map
 *
 * @param interceptor 拦截器接口
 * @return 方法签名组 Map
 */
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    // 取得 Intercepts 注解,例子可参见 TestPlugin.java
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    // 必须得有 Intercepts 注释,没有报错
    if (interceptsAnnotation == null) {
        throw new RuntimeException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
    }
    // value是数组型,Signature的数组
    Signature[] sigs = interceptsAnnotation.value();
    // 每个 class 类有多个可能有多个 Method 需要被拦截
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>(16);
    for (Signature sig : sigs) {
        Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
        try {
            // 例如获取到方法;StatementHandler.prepare(Connection connection)、StatementHandler.parameterize(Statement statement)...
            Method method = sig.type().getMethod(sig.method(), sig.args());
            methods.add(method);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
        }
    }
    return signatureMap;
}
  • getSignatureMap 所完成的动作就是为了获取代理类的签名操作,返回这个类下在哪个方法下执行调用插件操作。
    1. 根据入参 Interceptor 的接口的实现,从实现类的注解上获取方法的签名信息。
    2. 方法签名可以是一个数组结构,也就是一个插件可以监听多个配置的类以及多个类内的方法,当这些类的方法被调用的时候,就会调用到执行的自定义插件。
    3. 而在这个方法下,把符合监听方法返回一个列表,用于代理类中判断是否调用插件。

3.5.2 创建反射代理

java 复制代码
/**
 * 用代理把自定义插件行为包裹到目标方法中,也就是 Plugin.invoke 的过滤调用
 *
 * @param target      调用对象
 * @param interceptor 拦截器接口
 * @return 返回对象
 */
public static Object wrap(Object target, Interceptor interceptor) {
    // 取得签名
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // 取得要改变行为的类(ParameterHandler|ResultSetHandler|StatementHandler|Executor),目前只添加了 StatementHandler
    Class<?> type = target.getClass();
    // 取得接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 创建代理(StatementHandler)
    if (interfaces.length > 0) {
        // Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
        return Proxy.newProxyInstance(
            type.getClassLoader(),
            interfaces,
            new Plugin(target, interceptor, signatureMap)
        );
    }
    return target;
}
  • wrap 方法是用于给 ParameterHandler、ResultSetHandler、StatementHandler、Executor 创建代理类时调用的。
    • 这个创建的目的,就是把插件内容,包装到代理中。
  • 代理的创建是通过 Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler 实现的。
    • 而入参 InvocationHandler 的实现类,则是这个 Plugin 代理插件实现类。

3.5.3 包裹反射方法

java 复制代码
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 获取声明的方法列表
    Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    // 过滤需要拦截的方法
    if (methods != null && methods.contains(method)) {
        // 调用 Interceptor#intercept 插入自己的反射逻辑
        return interceptor.intercept(new Invocation(target, method, args));
    }
    return method.invoke(target, args);
}
  • 最终对于插件的核心调用,都会体现到 invoke 方法中。如一个被代理的类 ParameterHandler 当调用它的方法时,都会进入 invoke中。
    • invoker 方法中,通过前面方法的判断确定使用方自己实现的插件,是否在此时调用的方法上。
    • 如果是则进入插件调用,插件的实现中处理完自己的逻辑则进行 invocation.proceed() 放行。
    • 如果不在这个方法上,则直接通过 method.invoke(target, args) 调用原本的方法即可。

3.6 拦截器链和配置项修改

3.6.1 拦截器链

InterceptorChain

java 复制代码
package com.lino.mybatis.plugin;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * @description: 拦截器链
 */
public class InterceptorChain {

    private final List<Interceptor> interceptors = new ArrayList<>();

    public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
            target = interceptor.plugin(target);
        }
        return target;
    }

    public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
    }

    public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
    }
}

3.6.2 配置项

Configuration.java

java 复制代码
package com.lino.mybatis.session;

import com.lino.mybatis.binding.MapperRegistry;
import com.lino.mybatis.datasource.druid.DruidDataSourceFactory;
import com.lino.mybatis.datasource.pooled.PooledDataSourceFactory;
import com.lino.mybatis.datasource.unpooled.UnpooledDataSourceFactory;
import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.executor.SimpleExecutor;
import com.lino.mybatis.executor.keygen.KeyGenerator;
import com.lino.mybatis.executor.parameter.ParameterHandler;
import com.lino.mybatis.executor.resultset.DefaultResultSetHandler;
import com.lino.mybatis.executor.resultset.ResultSetHandler;
import com.lino.mybatis.executor.statement.PreparedStatementHandler;
import com.lino.mybatis.executor.statement.StatementHandler;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.Environment;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.mapping.ResultMap;
import com.lino.mybatis.plugin.Interceptor;
import com.lino.mybatis.plugin.InterceptorChain;
import com.lino.mybatis.reflection.MetaObject;
import com.lino.mybatis.reflection.factory.DefaultObjectFactory;
import com.lino.mybatis.reflection.factory.ObjectFactory;
import com.lino.mybatis.reflection.wrapper.DefaultObjectWrapperFactory;
import com.lino.mybatis.reflection.wrapper.ObjectWrapperFactory;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.scripting.LanguageDriverRegistry;
import com.lino.mybatis.scripting.xmltags.XMLLanguageDriver;
import com.lino.mybatis.transaction.Transaction;
import com.lino.mybatis.transaction.jdbc.JdbcTransactionFactory;
import com.lino.mybatis.type.TypeAliasRegistry;
import com.lino.mybatis.type.TypeHandlerRegistry;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * @description: 配置项
 */
public class Configuration {

    ...

    /**
     * 结果映射,存在Map里
     */
    protected final Map<String, ResultMap> resultMaps = new HashMap<>(16);
    /**
     * 键值生成器,存在Map里
     */
    protected final Map<String, KeyGenerator> keyGenerators = new HashMap<>(16);
    /**
     * 插件拦截器链
     */
    protected final InterceptorChain interceptorChain = new InterceptorChain();

    ...

    /**
     * 创建语句处理器
     *
     * @param executor        执行器
     * @param mappedStatement 映射器语句类
     * @param parameter       参数
     * @param rowBounds       分页记录限制
     * @param resultHandler   结果处理器
     * @param boundSql        SQL语句
     * @return StatementHandler 语句处理器
     */
    public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter,
                                                RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        StatementHandler statementHandler = new PreparedStatementHandler(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
        // 嵌入插件,代理对象
        statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
        return statementHandler;
    }

    ...

    public void addInterceptor(Interceptor interceptorInstance) {
        interceptorChain.addInterceptor(interceptorInstance);
    }
}
  • newStatementHandler 方法中嵌入插件代理对象。

3.7 解析XML插件配置

  • 接下来需要在 XML Config 的解析操作中,添加关于插件部分的解析处理,也就是处理配置在 mybatis-config-datasource.xml 中插件的信息
xml 复制代码
<plugins>
    <plugin interceptor="com.lino.mybatis.test.plugin.TestPlugin">
        <property name="test00" value="100"/>
        <property name="test01" value="200"/>
    </plugin>
</plugins>
  • 这部分解析处理 interceptor 是自定义插件的实现类,property 两个属性信息通常是不需要使用的。这里是为了测试需要。

XMLConfigBuilder.java

java 复制代码
package com.lino.mybatis.builder.xml;

import com.lino.mybatis.builder.BaseBuilder;
import com.lino.mybatis.datasource.DataSourceFactory;
import com.lino.mybatis.io.Resources;
import com.lino.mybatis.mapping.Environment;
import com.lino.mybatis.plugin.Interceptor;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.transaction.TransactionFactory;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xml.sax.InputSource;
import javax.sql.DataSource;
import java.io.InputStream;
import java.io.Reader;
import java.util.List;
import java.util.Properties;

/**
 * @description: XML配置构建器,建造者模式,集成BaseBuilder
 */
public class XMLConfigBuilder extends BaseBuilder {

    ...

    /**
     * 解析配置:类型别名、插件、对象工厂、对象包装工厂、设置、环境、类型转换、映射器
     *
     * @return Configuration
     */
    public Configuration parse() {
        try {
            // 插件添加
            pluginElement(root.element("plugins"));
            // 环境
            environmentsElement(root.element("environments"));
            // 解析映射器
            mapperElement(root.element("mappers"));
        } catch (Exception e) {
            throw new RuntimeException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
        return configuration;
    }

    /**
     * Mybatis 允许你在某一点切入映射语句执行的调度
     * <plugins>
     * <plugin interceptor="cn.bugstack.mybatis.test.plugin.TestPlugin">
     * <property name="test00" value="100"/>
     * <property name="test01" value="100"/>
     * </plugin>
     * </plugins>
     */
    private void pluginElement(Element parent) throws Exception {
        if (parent == null) {
            return;
        }
        List<Element> elements = parent.elements();
        for (Element element : elements) {
            String interceptor = element.attributeValue("interceptor");
            // 参数配置
            Properties properties = new Properties();
            List<Element> propertyElementList = element.elements("property");
            for (Element property : propertyElementList) {
                properties.setProperty(property.attributeValue("name"), property.attributeValue("value"));
            }
            // 获取插件实现类并实例化:com.lino.mybatis.test.plugin.TestPlugin
            Interceptor interceptorInstance = (Interceptor) resolveAlias(interceptor).newInstance();
            interceptorInstance.setProperties(properties);
            configuration.addInterceptor(interceptorInstance);
        }
    }

    ...

}
  • 解析插件的处理需要判断插件是否存在,如果存在则按照插件配置的列表分别进行解析,提取配置中的接口信息以及属性配置,存放到 Configuration 配置的插件拦截器链中。
  • 通过这样的方式把插件和要触发的监控点建立起连接。
  • 解析流程:在解析方法提供后,则放入到顺序解析的操作方法中即可。XMLConfigBuilder#pluginElement(root.element("plugins"))

四、测试:Plugin插件

4.1 自定义插件

TestPlugin.java

java 复制代码
package com.lino.mybatis.test.plugin;

import com.lino.mybatis.executor.statement.StatementHandler;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.plugin.Interceptor;
import com.lino.mybatis.plugin.Intercepts;
import com.lino.mybatis.plugin.Invocation;
import com.lino.mybatis.plugin.Signature;
import java.sql.Connection;
import java.util.Properties;

/**
 * @description: 测试插件
 */
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})
public class TestPlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取 StatementHandler
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // 获取SQL信息
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        // 输出SQL
        System.out.println("拦截SQL:" + sql);
        // 放行
        return invocation.proceed();
    }

    @Override
    public void setProperties(Properties properties) {
        System.out.println("参数输出:" + properties.getProperty("test00"));
    }
}
  • TestPlugin 自定义插件实现 Interceptor 接口,同时通过注解 @Intercepts 配置插件的触发时机。
  • 这里则是在调用 StatementHandler#prepare 方法时,处理自定义插件的操作。
  • 在这个自定义插件中,获取到 StatementHandler 语句处理器下的绑定 SQL 信息。
    • 注意 :这个 StatementHandler#getBoundSql 获取绑定 SQL 方法是新增的方法。
  • 另外是实现了 setProperties 获取注解的操作,这里是打印注解配置的信息。

4.2 修改XML配置文件

mybatis-config-datasource.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <plugins>
        <plugin interceptor="com.lino.mybatis.test.plugin.TestPlugin">
            <property name="test00" value="100"/>
            <property name="test01" value="200"/>
        </plugin>
    </plugins>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url"
                          value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true&amp;characterEncoding=utf8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <!--XML配置-->
        <mapper resource="mapper/Activity_Mapper.xml"/>
    </mappers>
</configuration>

4.3 单元测试

ApiTest.java

java 复制代码
@Test
public void test_queryActivityById() {
    // 1.获取映射器对象
    IActivityDao dao = sqlSession.getMapper(IActivityDao.class);
    // 2.测试验证
    Activity activity = new Activity();
    activity.setActivityId(100001L);
    Activity result = dao.queryActivityById(activity);
    logger.info("测试结果:{}", JSON.toJSONString(result));
}

测试结果

java 复制代码
参数输出:100
16:52:51.437 [main] INFO  c.l.m.d.pooled.PooledDataSource - Created connention 1164440413.
拦截SQL:SELECT activity_id, activity_name, activity_desc, create_time, update_time
        FROM activity
         where activity_id = ?
16:52:51.446 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:100001
16:52:51.454 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:{"activityDesc":"测试活动","activityId":100001,"activityName":"活动名","createTime":1628424890000,"updateTime":1628424890000}
  • 通过测试结果看,插件功能的实现已经验证通过。

五、总结:Plugin插件

  • 本章是对代理模式的最佳实践,通过代理对一个目标监听方法中,完成对扩展内容的调用。
    • 而这个扩展内容则是根据依赖倒置原则,面向抽象编程的具体实现。
  • 当一个框架逐步开发完成后,就要开始逐步对外提供扩展能力了,这样才能更好的让一个框架满足不同类用户的扩展需求。
    • 所以我们在做一些业务代码开发时,也应该给扩展留出口子,让后续的迭代更加容易,也更易于维护。
相关推荐
F-2H1 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
苹果酱05671 小时前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
_oP_i2 小时前
Pinpoint 是一个开源的分布式追踪系统
java·分布式·开源
mmsx2 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
武子康2 小时前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
豪宇刘3 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
秋恬意3 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
FF在路上4 小时前
Knife4j调试实体类传参扁平化模式修改:default-flat-param-object: true
java·开发语言
真的很上进4 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
众拾达人5 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言