您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
由 Mybatis 源码畅谈软件设计(六):Interceptor 拦截器的设计
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
由 Mybatis 源码畅谈软件设计(六):Interceptor 拦截器的设计
wy****
2024-12-25
IP归属:北京
21浏览
本节我们来介绍 Mybatis 的拦截器 `Interceptor`,它依靠 `@Intercepts` 和 `@Signature` 注解驱动,配置拦截器的切入方法,这种声明方式非常直观,能够准确的知道每个拦截器的作用范围。而且它是非侵入性的,采用了 **动态代理模式**,在不修改原有逻辑的前提下便能实现功能的扩展,遵循 **开闭原则**。Spring 框架中的 AOP 也采用的是同样的思想,但是它引入了很多概念(切面、连接点、切入点和通知等等),代码量超过 5000 行,而 Mybatis `Interceptor` 的实现仅有 370 行,非常精简,接下来我们先介绍下它的原理。 首先是解析 `mybatis-config.xml` 配置文件中定义的拦截器方法 `Configuration#pluginsElement`,解析创建完成后会被保存在 `InterceptorChain interceptorChain` 对象中: ```java public class Configuration { // ... protected final InterceptorChain interceptorChain = new InterceptorChain(); private void pluginsElement(XNode context) throws Exception { if (context != null) { for (XNode child : context.getChildren()) { // 获取每个节点下的拦截器配置 String interceptor = child.getStringAttribute("interceptor"); // 获取拦截器参数配置增加灵活性 Properties properties = child.getChildrenAsProperties(); // 反射创建拦截器实例 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor() .newInstance(); interceptorInstance.setProperties(properties); // 注册拦截器 configuration.addInterceptor(interceptorInstance); } } } public void addInterceptor(Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); } } ``` 那这些拦截器在哪里生效呢?其实在前面我们讲 SQL 执行流程时,已经见过它的身影,仍然是在 `Configuration` 中,创建四大处理器时都能看到 `pluginAll` 方法的身影(从这里也能知道拦截器的作用范围便是这四大拦截器): ```java public class Configuration { // ... protected final InterceptorChain interceptorChain = new InterceptorChain(); public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); // 拦截器相关逻辑 return (ParameterHandler) interceptorChain.pluginAll(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); // 拦截器相关逻辑 return (ResultSetHandler) interceptorChain.pluginAll(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); // 拦截器相关逻辑 return (StatementHandler) interceptorChain.pluginAll(statementHandler); } public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; // 创建具体的 Executor 实现类 Executor 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 (cacheEnabled) { executor = new CachingExecutor(executor); } // 拦截器相关逻辑 return (Executor) interceptorChain.pluginAll(executor); } } ``` 在每个处理器创建完成后都会执行 `pluginAll` 方法,接着我们看一下 `InterceptorChain#pluginAll` 方法: ```java public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<>(); public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { // 注意 target 引用不断变化,会不断引用已经添加拦截器的对象 target = interceptor.plugin(target); } return target; } public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } } ``` `InterceptorChain` 实现非常简单,内部定义了集合来保存所有配置的拦截器,执行 `pluginAll` 方法时会遍历该集合,逐个调用 `Interceptor#plugin` 方法来不断添加拦截器。注意这里使用到了 **责任链模式**,由 `InterceptorChain` 的命名中包含 `Chain` 也能联想到该模式,之后我们在使用责任链时也可以考虑在命名中增加 `Chain` 以增加可读性。`InterceptorChain` 将多个拦截器串联在一起,每个拦截器负责其特定的逻辑处理,并在执行完自己的逻辑后,调用下一个拦截器或目标方法,这样设计允许不同的拦截器之间的逻辑 **解耦**,同时提供了 **可扩展性**。 接着我们再看 `Interceptor#plugin` 方法: ```java public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; default Object plugin(Object target) { return Plugin.wrap(target, this); } default void setProperties(Properties properties) { // NOP } } ``` 它是定义在接口中的 `default` 方法,在实际业务开发中还是较少看见这个关键字的。继续探究 `Plugin#wrap` 方法: ```java public class Plugin implements InvocationHandler { // 被拦截器代理对象或被代理的拦截器 private final Object target; // 代理拦截器本身 private final Interceptor interceptor; // 要被拦截的方法 Map 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; } public static Object wrap(Object target, Interceptor interceptor) { // 根据 Intercepts 注解和 Signature 注解获取要被拦截的方法 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; } @Override public 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); } } // ... } ``` `Plugin` 实现了 `InvocationHandler` 接口,使用了 **动态代理模式**,是拦截器实现的核心类。在 `wrap` 方法中,会根据 `@Intercepts` 和 `@Signature` 来定位到要被拦截的方法,因为这部分逻辑很简单,便不再赘述了,后续便会创建 `Plugin` 代理对象。在 `invoke` 方法中,它会判断要执行的方法是否为被拦截的方法,并执行对应的逻辑,在执行拦截器逻辑时,它并没有直接将 `method`、`target` 和 `args` 参数直接暴露出去供拦截器做逻辑,而是将它们 **封装** 起来为 `Invocation` 对象: ```java public class Invocation { private static final List<Class<?>> targetClasses = Arrays.asList(Executor.class, ParameterHandler.class, ResultSetHandler.class, StatementHandler.class); private final Object target; private final Method method; private final Object[] args; public Invocation(Object target, Method method, Object[] args) { if (!targetClasses.contains(method.getDeclaringClass())) { throw new IllegalArgumentException("Method '" + method + "' is not supported as a plugin target."); } this.target = target; this.method = method; this.args = args; } public Object getTarget() { return target; } public Method getMethod() { return method; } public Object[] getArgs() { return args; } public Object proceed() throws InvocationTargetException, IllegalAccessException { return method.invoke(target, args); } } ``` `Invocation` 提供了 Get 方法来获取这些元素,并没有将它们全部都隐藏起来,而且也提供了方便的 `process` 方法调用,在构造方法中还添加了对应的校验,降低了 **复杂度**,这样在实现拦截器的大部分情况下,只需执行 `process` 方法即可,简单易用。 到现在为止拦截器的实现原理已经介绍完了,我们来看一下拦截器使用的例子,定义两个拦截器,以 `org.apache.ibatis.plugin.PluginTest` 为例: ```java class PluginTest { // 更换库名 @Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})) public static class SwitchCatalogInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); Connection con = (Connection) args[0]; con.setSchema(SchemaHolder.get()); return invocation.proceed(); } } // 打印执行前、后的日志 @Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})) public class LogInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { System.out.println("Before Process..."); Object res = invocation.proceed(); System.out.println("After Process..."); return res; } } } ``` 在 `mybatis-config.xml` 中定义的拦截器顺序如下,在向 `interceptorChain` 中添加时 `SwitchCatalogInterceptor` 在前,`LogInterceptor` 在后: ```xml <configuration> <!-- ...--> <plugins> <plugin interceptor="org.apache.ibatis.plugin.PluginTest$SwitchCatalogInterceptor" /> <plugin interceptor="org.apache.ibatis.plugin.PluginTest$LogInterceptor" /> </plugins> </configuration> ``` 那么在应用拦截器时 **越定义在后面的拦截器越会先生效**,如下所示,`LogInterceptor` 在 `SwitchCatalogInterceptor` 外层: ![interceptor_example.png](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-12-15-14-46nzmHqXNZYtzJ72M.png) 如果要考虑拦截器的执行顺序,那么便需要在添加拦截器时多注意了。
上一篇:京东电商搜索:深度强化学习的探索与落地
下一篇:由 Mybatis 源码畅谈软件设计(三):简单 SQL 执行流程
wy****
文章数
33
阅读量
2656
作者其他文章
01
高性能MySQL实战(一):表结构
最近因需求改动新增了一些数据库表,但是在定义表结构时,具体列属性的选择有些不知其所以然,索引的添加也有遗漏和不规范的地方,所以我打算为创建一个高性能表的过程以实战的形式写一个专题,以此来学习和巩固这些知识。1. 实战我使用的 MySQL 版本是 5.7,建表 DDL 语句如下所示:根据需求创建 接口调用日志 数据库表,请大家浏览具体字段的属性信息,它们有不少能够优化的点。CREATE TABLE
01
分布式服务高可用实现:复制
1. 为什么需要复制我们可以考虑如下问题:当数据量、读取或写入负载已经超过了当前服务器的处理能力,如何实现负载均衡?希望在单台服务器出现故障时仍能继续工作,这该如何实现?当服务的用户遍布全球,并希望他们访问服务时不会有较大的延迟,怎么才能统一用户的交互体验?这些问题其实都能通过 “复制” 来解决:复制,即在不同的节点上保存相同的副本,提供数据冗余。如果一些节点不可用,剩余的节点仍然可以提供数据服务
01
高性能MySQL实战(三):性能优化
这篇主要介绍对慢 SQL 优化的一些手段,而在讲解具体的优化措施之前,我想先对 EXPLAIN 进行介绍,它是我们在分析查询时必要的操作,理解了它输出结果的内容更有利于我们优化 SQL。为了方便大家的阅读,在下文中规定类似 key1 的表示二级索引,key_part1 表示联合索引的第一部分,unique_key1 则表示唯一二级索引,primary_key 表示主键索引。高性能MySQL实战(一
01
从2PC和容错共识算法讨论zookeeper中的Create请求
最近在读《数据密集型应用系统设计》,其中谈到了zookeeper对容错共识算法的应用。这让我想到之前参考的zookeeper学习资料中,误将容错共识算法写成了2PC(两阶段提交协议),所以准备以此文对共识算法和2PC做梳理和区分,也希望它能帮助像我一样对这两者有误解的同学。1. 2PC(两阶段提交协议)两阶段提交 (two-phase commit) 协议是一种用于实现 跨多个节点的原子事务(分布
wy****
文章数
33
阅读量
2656
作者其他文章
01
高性能MySQL实战(一):表结构
01
分布式服务高可用实现:复制
01
高性能MySQL实战(三):性能优化
01
从2PC和容错共识算法讨论zookeeper中的Create请求
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号