手写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.mzph.cn/news/68368.shtml

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

相关文章

冯诺依曼体系结构/什么是OS?

一、体系结构图 示意图 控制器可以控制其它4个硬件&#xff0c;四个硬件直接可以进行数据传输。 5大硬件 但是这些个体需要用“线”连接。 为什么要有存储器&#xff1f; 如果没有&#xff0c;实际速度则为输入、输出设备的速度。 加上后&#xff0c;变为内存的速度。&#…

大厂面试 | 百度一面,顶不住

题目来源&#xff1a;https://www.nowcoder.com/feed/main/detail/d39aabc0debd4dba810b4b9671d54348 前文 本期是【捞捞面经】系列文章的第 2 期&#xff0c;持续更新中…。&#xff08;更多与往期下方仓库直达&#xff09; 《捞捞面经》系列正式开始连载啦&#xff0c;据说看…

Kubernetes(k8s) 架构原理一文详解

目录 一、k8s 概述 1.什么是k8s&#xff1f; 2.特性 3.主要功能 三、集群架构与组件 1.Master 组件 &#xff08;1&#xff09;Kube-apiserver &#xff08;2&#xff09;Kube-controller-manager &#xff08;3&#xff09;Kube-scheduler 2.配置存储中心 3.Node 组…

【Sentinel】ProcessorSlotChain处理器插槽链与Node

文章目录 1、Sentinel的基本概念2、ProcessorSlotChain3、Node 1、Sentinel的基本概念 Sentinel实现限流、隔离、降级、熔断等功能&#xff0c;本质要做的就是两件事情&#xff1a; 统计数据&#xff1a;统计某个资源的访问数据&#xff08;QPS、RT等信息&#xff09;规则判断…

一文读懂GPU显卡的10个重要参数

在当今的高性能计算机世界中&#xff0c;GPU显卡的性能至关重要。这一领域的快速发展&#xff0c;使得图形渲染、游戏体验、视频编辑等高性能计算任务变得更加高效和流畅。正因如此&#xff0c;选择一款合适的GPU显卡变得越来越重要。在挑选GPU显卡时&#xff0c;了解其关键参数…

【真题解析】系统集成项目管理工程师 2023 年上半年真题卷(综合知识)

本文为系统集成项目管理工程师考试(软考) 2023 年上半年真题(全国卷),包含答案与详细解析。考试共分为两科,成绩均 ≥45 即可通过考试: 综合知识(选择题 75 道,75分)案例分析(问答题 4 道,75分)综合知识(选择题*75)1-10 题11-20 题21-30 题31-40 题41-50 题51-60 …

进程、线程与构造方法

进程、线程与构造方法 目录 一&#xff0e; 进程与线程1. 通俗解释2. 代码实现3. 线程生命周期&#xff08;图解&#xff09; 二&#xff0e; 构造方法 一&#xff0e; 进程与线程 1. 通俗解释 进程&#xff1a;就像电脑上运行的软件&#xff0c;例如QQ等。 线程&#xff1a;…

接口测试与功能测试的区别~

今天为大家分享的是我们在日常测试工作中, 一定会接触并且目前在企业中是主要测试内容的 功能测试与接口测试 一.功能测试与接口测试的基本概念。 1.1 什么是功能测试呢? 功能测试: 是黑盒测试的一方面, 检查实际软件的功能是否符合用户的需求 功能测试测试的内容包括以下…

改变金融贷款市场营销方式 ---- 运营商大数据精准获客

与传统的企业网络营销相比&#xff0c;最常见的是网络推广和硬广告推广。一些企业无法找到可靠准确的数据来源&#xff0c;也无法找到一些未知的总数据。这些数据大多存在持续时间长、准确性差的缺点&#xff0c;企业在将这些数据信息应用于商品在线营销时往往会遇到不足。 在…

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…

Nacos 开源版的使用测评

文章目录 一、Nacos的使用二、Nacos和Eureka在性能、功能、控制台体验、上下游生态和社区体验的对比&#xff1a;三、记使使用Nacos中容易犯的错误四、对Nacos开源提出的一些需求 一、Nacos的使用 这里配置mysql的连接方式&#xff0c;spring.datasource.platformmysql是老版本…

软件架构设计(三) B/S架构风格-层次架构(一)

层次架构风格从之前的两层C/S到三层C/S,然后演化为三层B/S架构,三层B/S架构之后仍然在往后面演化,我们来看一下层次架构演化过程中都有了哪些演化的架构风格呢? 而我们先简单了解一下之前的层次架构风格中分层的各个层次的作用。 表现层:由于用户进行交互,比如MVC,MVP,…

大数据平台数据安全具体措施有哪些?有推荐的吗?

大数据平台是企业处理和分析数据的重要工具之一&#xff0c;也是企业数据存储的重要载体&#xff0c;因此保障大数据平台安全至关重要。那你知道大数据平台数据安全具体措施有哪些&#xff1f;有推荐的吗&#xff1f; 大数据平台数据安全具体措施有哪些&#xff1f; 1、数据…

伦敦金投资中挂单的优势

在伦敦金交易中&#xff0c;其实挂单具有无可比拟的优势&#xff1f;首先&#xff0c;如果我们不是全职的投资者&#xff0c;我们在交易时间内可能有其他的工作&#xff0c;那么挂单就可以帮助我们捕捉到市场的机会&#xff0c;即便我们不一定会坐在电脑面前&#xff0c;也可以…

Flutter 完美的验证码输入框 转载

刚开始看到这个功能的时候一定觉得so easy&#xff0c;开始的时候我也是这么觉得的&#xff0c;这还不简单&#xff0c;然而真正写的时候才发现并没有想象的那么简单。 先上图&#xff0c;不上图你们都不想看&#xff0c;我难啊&#xff0c;到Github&#xff1a; https://gith…

C语言:选择+编程(每日一练Day15)

目录 选择题&#xff1a; 题一&#xff1a; 题二&#xff1a; 题三&#xff1a; 题四&#xff1a; 题五&#xff1a; 编程题&#xff1a; 题一&#xff1a;寻找奇数 思路一&#xff1a; 题二&#xff1a;寻找峰值 思路一&#xff1a; 本人实力有限可能对一些地方解…

java八股文面试[数据库]——B树和B+树的区别

B树是一种树状数据结构&#xff0c;它能够存储数据、对其进行排序并允许以O(logn)的时间复杂度进行查找、顺序读取、插入和删除等操作。 1、B树的特性 B树中允许一个结点中包含多个key&#xff0c;可以是3个、4个、5个甚至更多&#xff0c;并不确定&#xff0c;需要看具体的实…

svn checkout 报 ‘svn: E000061: 执行上下文错误: Connection refused‘

问题 svn: E170013svn: E000061 ➜ svn svn checkout https://xxx.xxx.xxx.xxx:9443/svn/project-xxx/ svn: E170013: Unable to connect to a repository at URL https://xxx.xxx.xxx.xxx:9443/svn/project-xxx svn: E000061: 执行上下文错误: Connection refused链接在浏览…

详细说明OSPF常见的LSA

目录 1类LSA &#xff08;Router LSA&#xff09;介绍 总结&#xff1a;1类LSA 2类LSA &#xff08;Network LSA&#xff09;介绍 总结&#xff1a;2类LSA 3类LSA &#xff08;Summary LSA&#xff09;介绍 总结&#xff1a;3类LSA 5类LSA &#xff08;ase LSA&…

ARP欺骗

ARP欺骗定义 ARP欺骗&#xff08;英语&#xff1a;ARP spoofing&#xff09;&#xff0c;又称ARP毒化&#xff08;ARP poisoning&#xff0c;网络上多译为ARP病毒&#xff09;或ARP攻击&#xff0c;是针对以太网地址解析协议&#xff08;ARP&#xff09;的一种攻击技术&#x…