手写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.hqwc.cn/news/98417.html

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

相关文章

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

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

行军遇到各种复杂地形怎么处理?

行军遇到各种复杂地形怎么处理&#xff1f; 【安志强趣讲《孙子兵法》第30讲】 【原文】 凡军好高而恶下&#xff0c;贵阳而贱阴&#xff0c;养生而处实&#xff0c;军无百疾&#xff0c;是谓必胜。 【注释】 阳&#xff0c;太阳能照到的地方。阴&#xff0c;太阳照不到的地方。…

5个强大的Java分布式缓存框架推荐

在开发中大型Java软件项目时&#xff0c;很多Java架构师都会遇到数据库读写瓶颈&#xff0c;如果你在系统架构时并没有将缓存策略考虑进去&#xff0c;或者并没有选择更优的缓存策略&#xff0c;那么到时候重构起来将会是一个噩梦。 在开发中大型Java软件项目时&#xff0c;很…

ClickHouse进阶(六):副本与分片-2-Distributed引擎

进入正文前&#xff0c;感谢宝子们订阅专题、点赞、评论、收藏&#xff01;关注IT贫道&#xff0c;获取高质量博客内容&#xff01; &#x1f3e1;个人主页&#xff1a;含各种IT体系技术,IT贫道_Apache Doris,大数据OLAP体系技术栈,Kerberos安全认证-CSDN博客 &#x1f4cc;订阅…

CloudCompare 二次开发(9)——半径滤波

目录 一、概述二、代码集成三、结果展示本文由CSDN点云侠原创,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫。 一、概述 使用CloudCompare与PCL的混合编程实现点云半径滤波。半径滤波的算法原理见:PCL 半径滤波器。基于PCL将半径滤波集成到Cl…

Linux系统的安装

文章目录 1 Linux介绍1.1 Linux是什么1.2 Linux的特点1.3 Linux的应用1.4 Linux的发行版本1.5 Linux的Shell 2 Linux安装2.1 安装方式2.2 什么是VMware2.3 VMware主要功能2.4 什么是CentOS2.5 VMware与CentOS与Linux的关系2.6 VMware安装CentOS的步骤 1 Linux介绍 1.1 Linux是…

购物商场项目实践

1.项目开始 1&#xff09;此项目为在线电商项目 2&#xff09;包含首页&#xff0c;搜索列表&#xff0c;商品详情&#xff0c;购物车&#xff0c;订单&#xff0c;支付&#xff0c;用户登录/注册等多个子模块 3&#xff09;使用Vue全家桶ES6webpackAxios等前端技术 4&…

面试题查漏补缺 i++和 ++ i哪个效率更高

i 和 i 哪个效率更高&#xff1f; 在这里声明&#xff0c;简单地比较前缀自增运算符和后缀自增运算符的效率是片面的&#xff0c;因为存在很多因素影响这个问题的答案。首先考虑内建数据类型的情况:如果自增运算表达式的结果没有被使用&#xff0c;而是仅仅简单地用于增加一员…

PyQt5报错Process finished with exit code -1073740791 (0xC0000409)

点击按钮之后&#xff0c;就直接退出程序&#xff0c;控制台出现一个提示&#xff1a;解决办法&#xff1a; 在PyCharm中打开Run菜单&#xff0c;找到Edit Configurations进入&#xff0c;勾选Emulate terminal in output console即可。 然后再运行一下程序&#xff0c;就可以…

爬虫项目(二):中国大学排名

《Python网络爬虫入门到实战》京东购买地址&#xff0c;这里讲解了大量的基础知识和实战&#xff0c;由本人编著&#xff1a;https://item.jd.com/14049708.html配套代码仓库地址&#xff1a;https://github.com/sfvsfv/Crawer文章目录 分析第一步&#xff1a;获取源码分析第一…

lv3 嵌入式开发-6 linux shell脚本编程(概念、变量、语句)

1 Shell脚本概述 1.1Shell脚本概述 Shell脚本是利用 shell 的功能所写的一个程序。这个程序是使用纯文本文件&#xff0c;将一些 shell 的语法与命令&#xff08;含外部命令&#xff09;写在里面&#xff0c;搭配正则表达式、管道命令与数据流重定向等功能 1.2Shell脚本编写流…

权限提升-Linux提权-环境变量文件配合SUID提权

LINUX系统提权项目介绍 一个综合类探针&#xff1a; Linux&#xff1a;https://github.com/liamg/traitor 一个自动化提权&#xff1a; Linux&#xff1a;https://github.com/AlessandroZ/BeRoot 两个信息收集&#xff1a; Linux&#xff1a;https://github.com/rebootuser/Lin…