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

文章目录

  • 一、目标: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;}@Overridepublic 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),目前只添加了 StatementHandlerClass<?> 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.javaIntercepts 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|Executorif (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.javaIntercepts 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),目前只添加了 StatementHandlerClass<?> 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.TestPluginInterceptor 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 {@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 获取 StatementHandlerStatementHandler statementHandler = (StatementHandler) invocation.getTarget();// 获取SQL信息BoundSql boundSql = statementHandler.getBoundSql();String sql = boundSql.getSql();// 输出SQLSystem.out.println("拦截SQL:" + sql);// 放行return invocation.proceed();}@Overridepublic 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_timeFROM activitywhere 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.rhkb.cn/news/121110.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vulnhub: Hogwarts: Bellatrix靶机

kali&#xff1a;192.168.111.111 靶机&#xff1a;192.168.111.228 信息收集 端口扫描 nmap -A -sC -v -sV -T5 -p- --scripthttp-enum 192.168.111.228访问80端口 查看源码&#xff0c;提示ikilledsiriusblack.php和文件包含的参数名file 漏洞利用 ikilledsiriusblack.p…

seata升级1.1.0后遇到io.seata.common.exception.ShouldNeverHappenException

我们这一节主要讲的是seata升级后的主要修改&#xff0c;至于seata的基本部署可以参考我之前的随笔。 一开始我在升级SpringBoot版本之后&#xff0c;seata就突然启动不起来了&#xff0c;报了下面的错&#xff1a; Caused by: io.seata.common.exception.ShouldNeverHappenExc…

etcd读写请求的执行过程

etcd读请求如何执行 首先&#xff0c;etcdctl 会对命令中的参数进行解析。在解析完请求中的参数后&#xff0c;etcdctl 会创建一个 clientv3 库对象通过gRPC API来访问 etcd server。对应流程一。 然后通过负载均衡算法选择一个etcd server节点&#xff0c;然后调用 etcd ser…

互联网医院|医疗系统新模式改善看病效率

伴随着互联网时代的进步&#xff0c;医疗也在不断的发展&#xff0c;越来越多的医院和诊所开始使用医疗软件。医疗软件广泛的被使用着&#xff0c;软件几乎覆盖了我们的日常生活。在我们日常生活当中健康一直是需求专业渠道&#xff0c;医疗软件开发会把用户的数据打造出一个数…

2023年了,java后端还有未来吗?

前言 Java当下确实是比较的内卷&#xff0c;但关键在于个人&#xff0c;可以看看不同地方&#xff08;这里主要举例北上广深一线城市&#xff09;对于Java开发工程师这个职位的具体要求&#xff1a; 在以下北上广深这些一线大城市的面试招聘当中不难看出&#xff0c;凡是工资…

电视盒子什么品牌好?数码博主盘点目前性能最好的电视盒子

电视盒子是非常重要的&#xff0c;老人小孩基本每天都会看电视&#xff0c;而电视盒子作为电视盒子的最佳拍档销量十分火爆&#xff0c;我自己每个月都会测评几次电视盒子&#xff0c;今天给大家详细解读一下电视盒子什么品牌好&#xff0c;看看目前性能最好的电视盒子是哪些&a…

危险边缘:揭示 Python 编程中易被忽视的四个安全陷阱

今天我们将要谈论一个非常重要的话题&#xff1a;Python 编程中的安全问题。作为一门广受欢迎的编程语言&#xff0c;Python 已经成为了许多开发者、计算机专业学生以及打工人的必备技能。 原文链接食用更佳 危险边缘&#xff1a;揭示 Python 编程中易被忽视的四个安全问题 然…

睿趣科技:抖音小店多久可以做起来

随着社交媒体的迅猛发展&#xff0c;抖音成为了全球最受欢迎的短视频平台之一&#xff0c;吸引了数以亿计的用户。在抖音上&#xff0c;人们不仅可以分享自己的生活、才艺和创意&#xff0c;还可以创业经营抖音小店。但是&#xff0c;很多人都想知道&#xff0c;一个抖音小店到…

10个免费PPT下载资源网站分享

PPT超级市场https://pptsupermarket.com/ PPT超级市场是一个完全免费的PPT模板下载网站&#xff0c;不需要注册登录&#xff0c;点击下载就能直接使用。 叮当设计https://www.dingdangsheji.com/ 叮当设计是一个完全免费的PPT模板下载网站&#xff0c;每一套PPT的质量都很高。除…

【Java从0到1学习】14 Java多线程

1. 多线程概述 人们在日常生活中&#xff0c;很多事情都是可以同时进行的。例如&#xff0c;一个人可以一边听音乐&#xff0c;一边打扫房间&#xff0c;可以一边吃饭&#xff0c;一边看电视。在使用计算机时&#xff0c;很多任务也是可以同时进行的。例如&#xff0c;可以一边…

VSCode 配置 C 语言编程环境

目录 一、下载 mingw64 二、配置环境变量 三、三个配置文件 四、格式化代码 1、安装插件 2、保存时自动格式化 3、左 { 不换行 上了两年大学&#xff0c;都还没花心思去搭建 C 语言编程环境&#xff0c;惭愧&#xff0c;惭愧。 一、下载 mingw64 mingw64 是著名的 C/C…

Leetcode130. 被围绕的区域

Every day a Leetcode 题目来源&#xff1a;130. 被围绕的区域 本题给定的矩阵中有三种元素&#xff1a; 字母 X&#xff1b;被字母 X 包围的字母 O&#xff1b;没有被字母 X 包围的字母 O。 本题要求将所有被字母 X 包围的字母 O都变为字母 X &#xff0c;但很难判断哪些 …

KC705开发板——MGT IBERT测试记录

本文介绍使用KC705开发板进行MGT的IBERT测试。 KC705开发板 KC705开发板的图片如下图所示。FPGA芯片型号为XC7K325T-2FFG900C。 MGT MGT是 Multi-Gigabit Transceiver的缩写&#xff0c;是Multi-Gigabit Serializer/Deserializer (SERDES)的别称。MGT包含GTP、GTX、GTH、G…

点成案例丨比浊仪助力牙周炎诱发因素研究

牙周炎概述 牙周炎&#xff08;Periodontitis&#xff09;是一种炎症性疾病&#xff0c;其主要特征为牙周袋的形成及袋壁的炎症、牙槽骨吸收而导致牙龈与牙齿分离、牙齿逐渐松动或掉落等。牙周炎主要是由积聚在牙龈及其附近牙面、齿颈缘的牙菌斑内细菌所分泌的毒素令牙周组织发…

2023.9.1 简单认识 JVM

目录 JVM 内存划分 本地方法栈 虚拟机栈 程序计数器 堆区 元数据区 JVM 类加载机制 加载 验证 准备 解析 初始化 类被加载的几种情况&#xff08;懒汉模式 ---> 只要被用到才会被加载&#xff09; 双亲委派模型 JVM 内存划分 JVM 是一个应用程序&#xff0c;在…

QT C++ 基于TCP通信的网络聊天室

一、基本原理及流程 1&#xff09;知识回顾&#xff08;C语言中的TCP流程&#xff09; 2&#xff09;QT中的服务器端/客户端的操作流程 二、代码实现 1&#xff09;服务器 .ui .pro 在pro文件中添加network库 .h #ifndef WIDGET_H #define WIDGET_H#include <QWidget>…

【MySQL学习笔记】(八)复合查询

在前面的笔记中做的查询基本都是对一张表进行查询&#xff0c;在实际开发中远远不够&#xff0c;本篇文章内容是复合查询相关的笔记。需要用到oracle9i的经典测试表&#xff0c;在笔记&#xff08;六&#xff09;中已经教大家如何导入了。 复合查询 基本查询回顾多表查询子连接…

SpringCloudAlibaba之Sentinel介绍

文章目录 1 Sentinel1.1 Sentinel简介1.2 核心概念1.2.1 资源1.2.2 规则 1.3 入门Demo1.3.1 引入依赖1.3.2 集成Spring1.3.3 Spring中资源规则 1.4 Sentinel控制台1.5 核心原理1.5.1 NodeSelectorSlot1.5.2 ClusterBuilderSlot1.5.3 LogSlot1.5.4 StatisticSlot1.5.5 Authority…

通讯行业:看完这篇文章,我的认知被刷新了!

在现代社会中&#xff0c;通讯系统已经成为我们生活中不可或缺的一部分&#xff0c;它们支撑着信息传递、数据交流和社交互动。然而&#xff0c;通讯系统的可靠性和连续性依赖于电源的稳定供应。电源中断或波动可能导致通讯中断&#xff0c;给个人、企业和组织带来巨大的不便和…

ModuleNotFoundError: No module named ‘transformers.modeling_bart‘解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…