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

news2024/11/28 19:02:30

文章目录

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

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

package com.lino.mybatis.plugin;

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

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

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

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

3.3.2 拦截注解

Intercepts.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

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

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

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 获取签名方法

/**
 * 获取方法签名组 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 创建反射代理

/**
 * 用代理把自定义插件行为包裹到目标方法中,也就是 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 包裹反射方法

@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

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

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 中插件的信息
<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

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

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 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

@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));
}

测试结果

参数输出:100
16:52:51.437 [main] INFO  c.l.m.d.pooled.PooledDataSource - Created connention 1164440413.
拦截SQLSELECT 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插件

  • 本章是对代理模式的最佳实践,通过代理对一个目标监听方法中,完成对扩展内容的调用。
    • 而这个扩展内容则是根据依赖倒置原则,面向抽象编程的具体实现。
  • 当一个框架逐步开发完成后,就要开始逐步对外提供扩展能力了,这样才能更好的让一个框架满足不同类用户的扩展需求。
    • 所以我们在做一些业务代码开发时,也应该给扩展留出口子,让后续的迭代更加容易,也更易于维护。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/975803.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【计算机网络】http协议

目录 前言 认识URL URLEncode和URLDecode http协议格式 http方法 GET POST GET与POST的区别 http状态码 http常见header 简易的http服务器 前言 我们在序列化和反序列化这一章中&#xff0c;实现了一个网络版的计算器。这个里面设计到了对协议的分析与处…

【力扣周赛】第 112 场双周赛(统计一个字符串的 k 子序列美丽值最大的数目(贪心+计数+组合数学)

文章目录 竞赛链接Q1&#xff1a;7021. 判断通过操作能否让字符串相等 IQ2&#xff1a;7005. 判断通过操作能否让字符串相等 II&#xff08;贪心&#xff09;Q3&#xff1a;2841. 几乎唯一子数组的最大和竞赛时代码——滑动窗口 Q4&#xff1a;8050. 统计一个字符串的 k 子序列…

电子邮件营销实例有哪些?如何做邮件营销?

可参考的电子邮件营销实例&#xff1f;营销邮件制作技巧有什么&#xff1f; 电子邮件营销是当今数字营销领域中的一个关键策略&#xff0c;旨在通过发送定制化的电子邮件与目标受众建立联系&#xff0c;提高品牌知名度、促进销售和培养客户关系。下面将介绍一些电子邮件营销的…

记录一次WMware网络问题

目录 ​编辑 一、问题描述 二、问题排查 2.1 指令ifconfig 查看ip信息 2.2 nmcli n 查看网卡状态 三、问题解决 3.1 启动 NetworkManager 网络管理器 3.2 ifup ens160 启动网卡 一、问题描述 我在我本地电脑上使用WMware虚拟机部署了k8s&#xff0c;有次正常关机后&am…

初试jsvmp加密

分析目标 目标网站 腾讯的点选验证码(我这边是本地环境&#xff0c;所以没有网址) 目标参数 cap_union_new_verify.collect 目标js文件 tdc.js?app_data 流程分析(分析算法) 我们打开我们要分析的网站&#xff0c;过一下点选验证码&#xff0c;抓一包&#xff0c;可以得到…

防雷工程中防雷接地网的应用方案

防雷接地是指在建筑物或其他设施中设置专门的接地装置&#xff0c;以防止雷电对人员、设备和建筑物造成危害的措施。防雷接地工程是防雷工程的重要组成部分&#xff0c;其主要目的是将雷电电流引入大地&#xff0c;消除雷电过电压&#xff0c;降低雷电危险。防雷接地工程应遵循…

BackgroudWork的详细用法,实例

一、什么是BackgroudWorker? 1、简言 backgroudworkd就是一个异步单线程&#xff0c;专门为入门级人员开发的。还可以显示进度条。操作简单实用,属于老技术。 注意&#xff1a;如果调用两次这个线程&#xff0c;将会出错。 2、backgroudwor…

centos7下docker设置新的下载镜像源并调整存放docker下载镜像的仓库位置

目录 1.设置镜像源 2.调整存放下载镜像的仓库位置 1.设置镜像源 在 /etc/docker下创建一个daemon.json文件。在json中下入 "registry-mirrors": ["https://docker.mirrors.ustc.edu.cn/"] 完成配置 加载配置 systemctl daemon-reload 重启docker sy…

医学影像工作站PACS系统源码,医院PACS系统源码

医学影像(PACS)系统主要进行病人信息和影像的获取、处理、存储、调阅、检索、管理&#xff0c;并通过网络向全院提供病人检查影像及诊断报告&#xff1b;各影像科室之间共享不同设备的病人检查影像及诊断报告&#xff1b;在诊断工作站上&#xff0c;调阅HIS中病人的其它信息&am…

企业架构LNMP学习笔记13

上线商城项目&#xff1a; 1&#xff09;上传项目文件到数据库&#xff1a; 入口文件位置的设计是为了让应用部署更安全&#xff0c;public目录为web可访问目录&#xff0c;其他的文件都可以放到非web访问目录下面。 nginx 默认访问index.html。没有index.html&#xff0c;就会…

软路由ip的优势与劣势:了解其适用场景和限制

在网络技术的快速发展中&#xff0c;软路由IP作为一种灵活且功能强大的网络设备&#xff0c;越来越受到人们的关注。然而&#xff0c;正如任何技术一样&#xff0c;软路由IP也有其优势和劣势。本文将深入探讨软路由IP的优势、劣势以及其适用场景和限制&#xff0c;帮助你更好地…

【回眸】牛客网刷刷刷!(八)——中断专题

目录 前言 1、在CortexM内核中&#xff0c;当系统响应一个中断时 2、用与非门和或非门可以实现其他基本门电路。进而实现任何逻辑电路 3、cpu interface提供了功能包含 4、以Cortex-M3内核为例&#xff0c;如果某个中断在得到响应之前&#xff0c;其请求信号以若干的脉冲的…

kafka-- 安装kafka manager及简单使用

一 、安装kafka manager 管控台&#xff1a; # 安装kafka manager 管控台&#xff1a; ## 上传 cd /usr/local/software ## 解压 unzip kafka-manager-2.0.0.2.zip -d /usr/local/ cd /usr/local/kafka-manager-2.0.0.2/conf vim /usr/local/kafka-manager-2.0.0.2/conf/appl…

MySql学习笔记03——DQL(数据查询)基本命令

DQL 导入数据 首先使用use database进入数据库中&#xff0c;然后使用命令 source D:\mysql_learning\mysql_learning\document\bjpowernode.sql注意文件名不能有双引号&#xff0c;命令结尾没有分号。 SQL脚本 .sql文件是SQL脚本文件&#xff0c;它里面的内容都是SQL语句…

个人炒伦敦银方法大公开

个人炒伦敦银的方法与机构投资者炒这个品种的方法是有不同的&#xff0c;但是双方可能会借鉴一些相同的分析工具&#xff0c;比方说有的机构可能也会使用技术分析&#xff0c;当然&#xff0c;个人投资者对技术分析这个词更是不会陌生。今天我们就从个人投资者的角度出发&#…

计算机竞赛 基于深度学习的人脸识别系统

前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 基于深度学习的人脸识别系统 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f9ff; 更多资料, 项目分享&#xff1a; https://gitee.com/dancheng-senior/…

8、异常机制---- 8.1 Error和Exception

8、异常机制 8.1 Error和Exception 什么是异常 在实际工作中&#xff0c;遇到的情况不可能是非常完美的。比如&#xff1a;你写的某个模块&#xff0c;用户输入不一定符合你的要求、你的程序要打开某个文件&#xff0c;这个文件可能不存在或者文件格式不对&#xff0c;你要读…

23. 带旋转的数独游戏

题目 Description 数独是一个基于逻辑的组合数字放置拼图&#xff0c;在世界各地都很受欢迎。 在这个问题上&#xff0c;让我们关注 网格的拼图&#xff0c;其中包含 个区域。 目标是用十六进制数字填充整个网格&#xff0c;即 &#xff0c;以便每列&#xff0c;每行和每个区…

ABB机器人20032转数计数器未更新故障报警处理方法

ABB机器人20032转数计数器未更新故障报警处理方法 ABB的机器人上面安装有电池,需要定期进行更换(正常一年换一次),如果长时间不更换,电量过低,就会出现转数计数器未更新的报警,各轴编码器的位置就会丢失,在更换新电池后,需要更新转数计数器。 具体步骤如下: 先用手动…

泛微OA流程表单中代码块获取URL的参数

获取URL的参数 需要编辑自定义函数 function getUrlParam(key){var url decodeURI(window.location.href);var paramMap {};var paramStr url.split("?")[2];if (paramStr && paramStr ! "") {var paramStrArr paramStr.split("&&qu…