设计模式学习笔记 - 开源实战五(中):如何利用职责链与代理模式实现Mybatis Plugin

devtools/2024/9/25 13:17:50/

概述

上篇文章对 Mybatis 框架做了简单的背景介绍,并通过对比各种 ORM 框架,学习了代码的易用性、性能、灵活性之间的关系。一般来讲,框架提供的高级功能越多,那性能损耗就越大;框架使用起来越简单,那灵活性就越低。

接下来的两篇文章,再学习下 Mybatis 用到的一些经典的设计模式。本章主要讲解 Mybatis Plugin

尽管名字叫 Plugin(插件),但它实际上和 Servlet Filter(过滤器)、Spring Interceptor(拦截器)类似,设计的初衷都是为了框架的扩展性,用到的主要设计模式是职责链模式。

不过相对于 Servlet Filter、Spring Interceptor,Mybatis Plugin 中职责链模式的代码实现稍微有点复杂。它是借助动态代理模式来实现的职责链。本章就带你看下,如何利用这两个模式实现 Mybatis Plugin


Mybatis Plugin 功能介绍

实际上,Mybatis Plugin 跟 Servlet Filter、Spring Interceptor 的功能是类似的,都是在不修改流程代码的情况下,拦截某些方法调用,在拦截的方法调用后,执行一些额外的代码逻辑。它们的唯一区别在于拦截的位置是不同的。Servlet Filter 主要拦截 Servlet 请求,Spring Interceptor 主要拦截 Spring 管理的 Bean 方法(比如 Controller 类的方法等),而 Mybatis Plugin 主要拦截的是 Mybatis 在执行 SQL 的过程中涉及的一些方法。

Mybatis Plugin 使用起来比较简单,下面通过一个例子来快速看下。

假设我们需要统计应用中每个 SQL 的执行耗时,如果使用 Mybatis Plugin 来实现的话,只需要定义一个 SqlCostTimeInterceptor 类,让他实现 Mybatis 的 Interceptor 接口,并且在 Mybatis 的全局配置文件中,简单声明一下就可以了。具体代码如下所示:

@Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),@Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),@Signature(type = StatementHandler.class, method = "batch", args = {Statement.class}),})
public class SqlCostTimeInterceptor implements Interceptor {private static Logger logger = LoggerFactory.getLogger(SqlCostTimeInterceptor.class);@Overridepublic Object intercept(Invocation invocation) throws Throwable {Object target = invocation.getTarget();long startTime = System.currentTimeMillis();StatementHandler statementHandler = (StatementHandler) target;try {return invocation.proceed();} finally {long costTime = System.currentTimeMillis() - startTime;BoundSql boundSql = statementHandler.getBoundSql();String sql = boundSql.getSql();logger.info("执行 SQL:[ {} ],执行耗时[ {} ms]", sql, costTime);}}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {System.out.println("插件配置信息: " + properties);}
}<!-- Mybatis全局配置文件:mybatis-config.xml -->
<plugins><plugin interceptor="com.example.SqlCostTimeInterceptor"><property name="someProperty" value="100"/></plugin>
</plugins>

待会会详细的介绍 Mybatis Plugin 底层的实现原理,所以,这里暂时不对上面的代码做详细地解释。现在,只关注下 @Intercepts 这个注解。

不管是拦截器、过滤器还是插件,都需要明确地标明拦截的目标方法。@Intercepts 注解实际上就是这个作用。其中,@Intercepts 注解又可以嵌套 @Signature 注解。一个 @Signature 注解标明要拦截的方法。如果要拦截多个方法,我们可以像例子中那样,编写多条 @Signature 注解。

@Signature 注解包含三个元素:typemethodargs。其中,type 指明要拦截的类、method 指明方法名、args 指明方法的参数列表。通过这三个元素,就能完全确定要拦截的方法。

默认情况下,Mybatis Plugin 允许拦截的方法有下面这样几个:

方法
Executorupdate、query、flushStatements、commit、rollback、getTransaction、clos、isClosed
ParameterHandlergetParameterObject、setParameters
ResultSetHandlerhandleResultSets、handleOutputParameters
StatementHandlerprepare、parameterize、batch、update、query

为什么默认允许拦截的是这样几个类的方法呢?

Mybatis 底层是通过 Executor 类来执行 SQL 的。Executor 会创建 StatementHandlerResultSetHandlerParameterHandler 三个对象,并且,首先使用 ParameterHandler 设置 SQL 中的占位符参数,然后使用 StatementHandler 执行 SQL 语句,最后使用 ResultSetHandler 封装执行结果。所以,我们需要拦截 ExecutorStatementHandlerResultSetHandlerParameterHandler 这几个类的方法,基本上就能满足我们对整个 SQL 执行流程的拦截了。

实际上,除了统计 SQL 的执行耗时,利用 Mybatis Plugin,还可以做很多事情,比如分库分表、自动分页、数据脱敏、加密解密等等。如果感兴趣,你可以自己实现下。

Mybatis Plugin 的设计与实现

刚刚简单介绍了 Mybatis Plugin 是如何使用的。现在在剖析下源码,看看如此简洁的使用方式,底层是如何实现的,隐藏了哪些复杂的设计。

相对于 Servlet Filter、Spring Interceptor 中职责链的代码实现,Mybatis Plugin 的代码实现还是蛮有技巧的,因为它是借助动态代理来实现职责链的。

在《责任链模式(上)》和《责任链模式(下)》中,我们讲到,职责链模式的实现一般包括处理器(Handler)和处理器链(HandlerChain)两部分。这两个部分对应到 Servlet Filter 源码就是 FilterFilterChain,对应到 Spring Interceptor 源码就是 HandlerInterceptorHandlerExecutionChain对应到 Mybatis Plugin 的源码就是 InterceptorInterceptorChain。此外,Mybatis Plugin 还包含另外一个非常重要的类:PluginPlugin用来生成被拦截对象的动态代理

集成了 Mybatis 的应用在启动时,Mybatis 框架会读取全局配置文件,解析出 Interceptor,并将它们注入到 Configuration 类的 InterceptorChain 对象中。这部分逻辑对应到源码如下所示。

public class XMLConfigBuilder extends BaseBuilder {// ...// 解析配置private void parseConfiguration(XNode root) {try {// ...this.pluginElement(root.evalNode("plugins"));// ...} catch (Exception var3) {throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);}}// ...// 解析插件private void pluginElement(XNode parent) throws Exception {if (parent != null) {Iterator var2 = parent.getChildren().iterator();while(var2.hasNext()) {XNode child = (XNode)var2.next();String interceptor = child.getStringAttribute("interceptor");Properties properties = child.getChildrenAsProperties();// 创建Interceptor类对象Interceptor interceptorInstance = (Interceptor)this.resolveClass(interceptor).getDeclaredConstructor().newInstance();// 调用Interceptor上的setProperties()方法设置propertiesinterceptorInstance.setProperties(properties);// 下面这行代码会调用InterceptorChain.addInterceptor()方法this.configuration.addInterceptor(interceptorInstance);}}}// ...
}public class Configuration {// ...public void addInterceptor(Interceptor interceptor) {this.interceptorChain.addInterceptor(interceptor);}// ...
}

再来看下 InterceptorInterceptorChain 这两个类的代码,如下所示。InterceptorsetProperties() 方法就是单纯的一个 setter 方法,主要是为了方便通过配置文件配置 Interceptor 的一些属性值。Interceptor 类中的 intercept()plugin() 函数,以及 InterceptorChainpluginAll() 函数,是最核心的三个函数,待会再详细解释。

public class Invocation {private final Object target;private final Method method;private final Object[] args;// 省略构造函数和getter方法...public Object proceed() throws InvocationTargetException, IllegalAccessException {return this.method.invoke(this.target, this.args);}
}public interface Interceptor {Object intercept(Invocation var1) throws Throwable;Object plugin(Object target);void setProperties(Properties properties);
}public class InterceptorChain {private final List<Interceptor> interceptors = new ArrayList();public InterceptorChain() {}public Object pluginAll(Object target) {Interceptor interceptor;for(Iterator var2 = this.interceptors.iterator(); var2.hasNext(); target = interceptor.plugin(target)) {interceptor = (Interceptor)var2.next();}return target;}public void addInterceptor(Interceptor interceptor) {this.interceptors.add(interceptor);}public List<Interceptor> getInterceptors() {return Collections.unmodifiableList(this.interceptors);}
}

解析完配置后,所有的 Interceptor 都加载到了 InterceptorChain 中。接下来,再看下,这些拦截器是在什么时候被触发执行的?又是如何被触发执行的?

前面提到,在执行 SQL 的过程中,Mybatis 会创建 ExecutorStatementHandlerResultSetHandlerParameterHandler 这个几个类的对象,对应的创建代码在 Configuration 类中,如下所示:

public class Configuration {// ...public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? this.defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Object executor;if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);} else {executor = new SimpleExecutor(this, transaction);}if (this.cacheEnabled) {executor = new CachingExecutor((Executor)executor);}Executor executor = (Executor)this.interceptorChain.pluginAll(executor);return executor;}// ...public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);parameterHandler = (ParameterHandler)this.interceptorChain.pluginAll(parameterHandler);return parameterHandler;}public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);ResultSetHandler resultSetHandler = (ResultSetHandler)this.interceptorChain.pluginAll(resultSetHandler);return resultSetHandler;}public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);StatementHandler statementHandler = (StatementHandler)this.interceptorChain.pluginAll(statementHandler);return statementHandler;}// ...
}

从上面的代码可以看出,这几个类对象的创建构成都调用了 InterceptorChainpluginAll() 方法。这个方法的代码前面已经给出了。它的代码实现很简单,嵌套调用 InterceptorChain 上每个 Interceptorplugin() 方法。plugin() 是一个接口方法,需要由用户给出具体的实现代码。在之前的例子中, SQLTimeCostInterceptorplugin() 方法通过直接调用 Pluginwrap() 方法来实现。wrap() 方法的代码实现如下所示:

// 借助Java InvocationHandler实现动态代理模式
public class Plugin implements InvocationHandler {private final Object target;private final Interceptor interceptor;private final Map<Class<?>, Set<Method>> signatureMap;private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {this.target = target;this.interceptor = interceptor;this.signatureMap = signatureMap;}// Wrap()静态方法,用来生成 target 对象的动态代理// 动态代理对象 = target对象 + interceptor对象public static Object wrap(Object target, Interceptor interceptor) {Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);Class<?> type = target.getClass();Class<?>[] interfaces = getAllInterfaces(type, signatureMap);if (interfaces.length > 0) {return Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap));}return target;}// 调用target上的f()方法,会触发执行下面这个方法// 这个方法阿包含:执行interceptor的intecept()方法 + 执行targetshangf()方法。@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {Set<Method> methods = signatureMap.get(method.getDeclaringClass());if (methods != null && methods.contains(method)) {return interceptor.intercept(new Invocation(target, method, args));}return method.invoke(target, args);} catch (Exception e) {throw ExceptionUtil.unwrapThrowable(e);}}private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);// issue #251if (interceptsAnnotation == null) {throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());}Signature[] sigs = interceptsAnnotation.value();Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();for (Signature sig : sigs) {Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());try {Method method = sig.type().getMethod(sig.method(), sig.args());methods.add(method);} catch (NoSuchMethodException e) {throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);}}return signatureMap;}private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {Set<Class<?>> interfaces = new HashSet<>();while (type != null) {for (Class<?> c : type.getInterfaces()) {if (signatureMap.containsKey(c)) {interfaces.add(c);}}type = type.getSuperclass();}return interfaces.toArray(new Class<?>[0]);}
}

实际上,Plugin 是借助 Java InvocationHandler 实现的动态代理。用来代理给 target 对象添加 interceptor 功能。其中,要代理的对象就是 ExecutorStatementHandlerResultSetHandlerParameterHandler 这个四个类的对象。wrap() 静态方法是一个工具函数,用来生成 target 对象的动态代理对象。

当然,只有 interceptortarget 互相匹配时,wrap() 方法才会返回代理对象,否则就返回 target 对象本身。怎么才能算匹配呢? 那就是 interceptor 通过 @Signature 朱姐拦截到的类包含 target 对象,具体可以看 wrap() 函数的代码实现。

Mybatis 中的职责链模式的实现方式比较特殊。它对同一个目标对象嵌套多次代理(也就是 InterceptorChainpluginAll() 函数要执行的任务)。每个代理对象(Plugin 对象)代理一个拦截器(Interceptor 对象)功能。为了方便查看,我把 pluginAll() 函数的代码又拷贝到下面。

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;}// 上面这行代码等于下面代码,target(代理对象) = target(目标对象) + interceptpor(拦截器功能)// target=Plugin.wrap(target, interceptor);// ...
}// mybatis像下面这样创建target(Executor、StatementHandler、ResultSetHandler、ParameterHandler),
// 相当于多次嵌套代理
Object target = interceptor.pluginAll(target);

当执行 ExecutorStatementHandlerResultSetHandlerParameterHandler 这四个类方法时,Mybatis 会嵌套执行每层代理对象(Plugin 对象)上的 invoke() 方法,而 invoke() 方法会先执行对象中的 interceptor.intercept() 函数,然后再执行被代理对象上的方法。就这样,一层一层地把代理对象上的 intercept() 函数执行完后,Mybatis 才最终执行那 4 个原始类对象上的方法。

总结

本章剖析了如何利用职责链模式和动态代理模式来实现 Mybatis Plugin。至此,我们学习了三种职责链常用的应用场景:过滤器(Servlet Filter)、拦截器(Spring Interceptor)、插件(Mybatis Plugin)。

职责链模式一般包含处理器和处理器链两部分。这两个部分对应到 Servlet Filter 源码就是 FilterFilterChain,对应到 Spring Interceptor 源码就是 HandlerInterceptorHandlerExecutionChain,对应到 Mybatis Plugin 的源码就是 InterceptorInterceptorChain。此外,Mybatis Plugin 还包含另外一个非常重要的类:PluginPlugin用来生成被拦截对象的动态代理。

这三种应用场景中,职责链模式的实现思路都不打一样。其中,Servlet Filter 采用递归来实现拦截方法前后添加逻辑。Spring Interceptor 的实现比较简单,把拦截方法前后要添加的逻辑放到两个方法中去实现。Mybatis Plugin 采用嵌套动态代理的方法来实现,实现思路很有技巧。


http://www.ppmy.cn/devtools/15361.html

相关文章

Python 基础 (Pandas):Pandas 入门

1. 官方文档 API reference — pandas 2.2.2 documentation 2. 准备知识&#xff1a;Pandas 数据结构 Series & DataFrame 2.1 Series 2.1.1 创建 Series 类型数据 一个 Series 对象包含两部分&#xff1a;值序列、标识符序列。可通过 .values (返回 NumPy ndarry 类型…

小区电梯20楼坠落致1死,如何用FMEA避免悲剧重演

近日&#xff0c;一起小区电梯从20楼坠落导致1人死亡的悲剧引起了广泛关注。这起事故不仅给受害者家庭带来了巨大的伤痛&#xff0c;也让人们对电梯的安全性产生了深深的担忧。那么&#xff0c;我们能否通过一些有效的预防措施来避免类似的悲剧再次发生呢&#xff1f;答案是肯定…

MongoDB聚合运算符:$sampleRate

MongoDB聚合运算符&#xff1a;$sampleRate 文章目录 MongoDB聚合运算符&#xff1a;$sampleRate语法使用举例 $sampleRate聚合运算符用$match&#xff0c;按照指定的抽样比例&#xff0c;从输入的文档中随机选择相应的文档。 语法 { $sampleRate: <non-negative float>…

稀碎从零算法笔记Day55-LeetCode:100291. 统计特殊字母的数量 II

今天可惜了&#xff0c;周赛第二题没看出来&#xff0c;导致第三题时间都不够&#xff0c;最后一题... 题目描述&#xff1a; 给你一个字符串 word。如果 word 中同时出现某个字母 c 的小写形式和大写形式&#xff0c;并且 每个 小写形式的 c 都出现在第一个大写形式的 c 之前…

Github 2024-04-19Java开源项目日报 Top9

根据Github Trendings的统计,今日(2024-04-19统计)共有9个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Java项目9HTML项目1Android开发者实用工具集 创建周期:2820 天开发语言:Java协议类型:Apache License 2.0Star数量:32909 个Fork数量:10631…

C# 扩展运算符(...)的详细解析

在C#编程中&#xff0c;扩展运算符&#xff08;…&#xff09;是一种非常有用的特性&#xff0c;它可以将一个数组或集合转换成一个可迭代的序列。扩展运算符在C# 7.0及以后的版本中引入&#xff0c;提供了一种简洁的方式来创建数组、列表或集合的实例&#xff0c;尤其是在需要…

RustGUI学习(iced)之小部件(一):如何使用按钮和文本标签部件

前言 本专栏是学习Rust的GUI库iced的合集&#xff0c;将介绍iced涉及的各个小部件分别介绍&#xff0c;最后会汇总为一个总的程序。 iced是RustGUI中比较强大的一个&#xff0c;目前处于发展中&#xff08;即版本可能会改变&#xff09;&#xff0c;本专栏基于版本0.12.1. 概述…

【云计算】云计算八股与云开发核心技术(虚拟化、分布式、容器化)

【云计算】云计算八股与云开发核心技术&#xff08;虚拟化、分布式、容器化&#xff09; 文章目录 一、什么是云计算&#xff1f;1、云计算的架构&#xff08;基础设施&#xff0c;平台&#xff0c;软件&#xff09;2、云计算的发展 二、如何做云计算开发&#xff1f;云计算的核…