Mybatis链路分析:JDK动态代理和责任链模式的应用
背景
此前写过关于代理模式的文章,参考:代理模式
动态代理功能:生成一个Proxy代理类,Proxy代理类实现了业务接口,而通过调用Proxy代理类实现的业务接口,实际上会触发代理类的invoke增强处理方法。
责任链功能:可以动态地组合处理者,增加或删除处理者,而不需要修改客户端代码;可以灵活地处理请求,每个处理者可以选择处理请求或将请求传递给下一个处理者。
MybatisAutoConfiguration
这是最初的Mybatis的自动加载类。
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {private final Interceptor[] interceptors;public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider,ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider,ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {this.properties = properties;// 【1】this.interceptors = interceptorsProvider.getIfAvailable();......}@Bean@ConditionalOnMissingBeanpublic SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {SqlSessionFactoryBean factory = new SqlSessionFactoryBean();factory.setDataSource(dataSource);factory.setVfs(SpringBootVFS.class);if (StringUtils.hasText(this.properties.getConfigLocation())) {factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));}applyConfiguration(factory);if (this.properties.getConfigurationProperties() != null) {factory.setConfigurationProperties(this.properties.getConfigurationProperties());}// 【2】if (!ObjectUtils.isEmpty(this.interceptors)) {factory.setPlugins(this.interceptors);}......return factory.getObject();}}
代码分析:
【1】interceptorsProvider.getIfAvailable();获取Interceptor接口的所有的bean,并加载到内存interceptors里
【2】构造SqlSessionFactoryBean,会用到内存的interceptors,填充拦截器bean到SqlSessionFactoryBean里面。
SqlSessionFactoryBean#afterPropertiesSet:初始化完成后执行
因为SqlSessionFactoryBean实现了InitializingBean接口,必然有一个afterPropertiesSet()的实现方法:
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {@Overridepublic void afterPropertiesSet() throws Exception {notNull(dataSource, "Property 'dataSource' is required");notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),"Property 'configuration' and 'configLocation' can not specified with together");// 【3】this.sqlSessionFactory = buildSqlSessionFactory();}
}
代码分析:
【3】这里会构造一个sqlSessionFactory,并调用了buildSqlSessionFactory()
SqlSessionFactoryBean#buildSqlSessionFactory:构造SqlSessionFactory
里面通过遍历SqlSessionFactoryBean的interceptors,逐个把拦截器加载到SqlSessionFactory的interceptorChain里面。
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {......// 【4】if (!isEmpty(this.plugins)) {Stream.of(this.plugins).forEach(plugin -> {targetConfiguration.addInterceptor(plugin);LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");});}// 【5】return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}
代码分析:
【4】逐个把拦截器加载到targetConfiguration对象的interceptorChain里面(也就是拦截器责任链了)。
【5】最终通过sqlSessionFactoryBuilder建造者模式,完成一个对象创建:new DefaultSqlSessionFactory(config)
DefaultSqlSessionFactory#构造器
public class DefaultSqlSessionFactory implements SqlSessionFactory {private final Configuration configuration;public DefaultSqlSessionFactory(Configuration configuration) {this.configuration = configuration;}
}
到此,完成Mybatis的插件bean的类加载和插件责任链的初始化。
上述的实现逻辑,,拆分到了只包含部分逻辑的、功能单一的Handler处理类里,开发人员可以按照业务需求将多个Handler对象组合成一条责任链,实现请求的处理。
那么Mybatis插件什么时候发挥作用呢?
自然是每个sqlSession创建时,在返回Executor对象前,会对执行器进行一个pluginAll的插件处理。
我们发起一次请求,最终会映射打开一个session会话:http://localhost:8891/user_select?id=2
最终debug到下面源码。
DefaultSqlSessionFactory#openSessionFromDataSource
最终debug到:org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource
public class DefaultSqlSessionFactory implements SqlSessionFactory {private final Configuration configuration;private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {Transaction tx = null;try {// 【1】final Environment environment = configuration.getEnvironment();final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);// 【2】tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);// 【3】final Executor executor = configuration.newExecutor(tx, execType);// 【4】return new DefaultSqlSession(configuration, executor, autoCommit);} catch (Exception e) {closeTransaction(tx); // may have fetched a connection so lets call close()throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);} finally {ErrorContext.instance().reset();}}
}
源码分析:
【1】获取环境变量
【2】创建新事务
【3】创建一个新的Executor执行器(非常关键)
【4】返回新构造的DefaultSqlSession
configuration#newExecutor:非常关键
public class Configuration {public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;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);}executor = (Executor) interceptorChain.pluginAll(executor);return executor;}
}
interceptorChain.pluginAll(executor)
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;}}
最终返回的是一个动态代理了Plugin类的自动生产对象。
Interceptor#plugin
public interface Interceptor {Object intercept(Invocation invocation) throws Throwable;default Object plugin(Object target) {// 【1】return Plugin.wrap(target, this);}default void setProperties(Properties properties) {// NOP}
}
此处的 Plugin.wrap(target, this) 是一个静态方法,本质是对插件进行
动态代理,最终返回的是一个动态代理了Plugin类的自动生产对象。
org.apache.ibatis.plugin.Plugin#wrap
org.apache.ibatis.plugin.Plugin#wrappublic 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;
}
源码分析:
最核心的部分,就是Proxy.newProxyInstance
target:Executor执行器
Interceptor:拦截器
signatureMap:拦截签名
new 了一个Plugin类
构造对Plugin类的动态代理,会自动生成一个代理类A#Plugin,最终执行的还是Plugin类的invoke方法
于是wrap最终返回的是一个动态代理了Plugin类的自动生产对象。
代理器是Plugin,被代理类是target(Executor或Handler),
至此,我们分析了完成代理模式的应用部分,下面是责任链模式的应用。
执行SQL时机:SqlSessionInterceptor
也是一个动态代理,
org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor#invoke
最终执行的逻辑,是调用sqlSession去接args参数,这个反射执行的方法是:
SqlSession.selectOne(java.lang.String,java.lang.Object)
而selectOne最终调用了
DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {try {MappedStatement ms = configuration.getMappedStatement(statement);return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);} catch (Exception e) {throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);} finally {ErrorContext.instance().reset();}
}
源码分析:
executor.query:是最终的Executor执行器query方法逻辑。还记得上面一个步骤吗?自动生成一个代理类A#Plugin,它实现了对Executor的增强处理,这块增强处理逻辑要回到Plugin#invoke方法:
@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);}
}
这里会根据signatureMap的调用点,来判断是否执行interceptor的拦截逻辑。
而这里的拦截逻辑,就是我们实现好的,Interceptor拦截器类的intercept方法啦。
new好的Invocation,实际就是调用点。
通过InterceptorChain拦截器链,对Executor进行增强
总结
从图中可以知道,Mybatis的拦截器链运用了动态代理和责任链模式:
其实就是代理对象再次生成代理对象,特殊的是代理对象的target属性
另外配合Invocation类中的proceed方法形成责任链路的调用,这样就可以在我们执行sql的前后,做一些特殊的自定义的事情了。
相关文章:

Mybatis链路分析:JDK动态代理和责任链模式的应用
背景 此前写过关于代理模式的文章,参考:代理模式 动态代理功能:生成一个Proxy代理类,Proxy代理类实现了业务接口,而通过调用Proxy代理类实现的业务接口,实际上会触发代理类的invoke增强处理方法。 责任链功…...
【Spring Boot 3】【Web】解析获取HTTP请求参数
【Spring Boot 3】【Web】解析获取HTTP请求参数 背景介绍开发环境开发步骤及源码工程目录结构背景 软件开发是一门实践性科学,对大多数人来说,学习一种新技术不是一开始就去深究其原理,而是先从做出一个可工作的DEMO入手。但在我个人学习和工作经历中,每次学习新技术总是要…...

conda换源是什么?
换源对于我们在国内的python使用者来说是非常有必要的,之前讲了pip如何换源。 pip更换为国内镜像源的步骤,为什么要更换镜像源 那现在讲一下conda如何换源。 conda换源(清华源) 有时候,conda虽然和pip共用一个本地…...
英文缩写大全(IT 领域和电子行业制造领域)
英文缩写大全(IT 领域和电子行业制造领域) 前言一、计算机通用二、WINDOWS三、LINUX四、编程语言1. 前端 / 设计2. JAVA / Android3. PHP4. Python 四、电子行业制造领域五、常识 前言 本文收集了各类英文缩写大全,方便查阅,主要…...

如何将图片左右翻转?8种方法来将图片进行左右翻转
如何将图片左右翻转?在现代数字化的图像处理过程中,图片的方向调整是常见的操作之一。左右翻转图片不仅可以改变图像的视觉效果,还可以用于修正拍摄时的角度问题,或者满足特定设计和排版需求。比如,当你拍摄的照片由于…...
linux:ln用法详解
文章目录 1. 描述2. 语法2.1 硬链接(Hard Link)2.2. 符号链接(Symbolic Link / Soft Link) 3. ln 命令的常用选项4. 例子 1. 描述 在 Linux 系统中,ln 命令用于创建硬链接(Hard Link)或符号链接…...

0基础跟德姆(dom)一起学AI Python进阶02-Python面向对象高级
1 [重点]定义类的几种语法 方式1:类名 在上一章节的学习过程中,我们都使用了这种定义类的语法: properties class 类名: 代码 ... 例如,使用该方式来定义一个老师类。 python # 1.class 类名: # pass class Te…...

【深度学习】softmax 回归的从零开始实现与简洁实现
前言 小时候听过一个小孩练琴的故事,老师让他先弹最简单的第一小节,小孩练了两天后弹不出。接着,老师让他直接去练更难的第二小节,小孩练习了几天后还是弹不出,开始感觉到挫败和烦躁了。 小孩以为老师之后会让他从简…...

Sollong、IO.NET和 Solana,为何参加 WebX 2024活动?
东京王子花园塔酒店 ChainCatcher_携手 DPCapital_XYZ与WebX_Asia共同打造“世界のStaking最大级集结|Tokyo站”盛会,为全球Staking爱好者与行业精英搭建交流合作桥梁!全球顶尖Staking项目方、知名区块链机构、行业领袖与企业家将齐聚东京,共…...

3个免费好用的网站,可以转换PDF,提取MP3
今天分享的三个网站,分别用于文件转换PDF,QMC转MP3格式和配色网站。 TOPDF 这个网站是一个在线PDF转换工具,可以快速将文本文件、演示文稿、电子表格和图片转换为PDF格式。它支持多种文件格式,如AZW3、BMP、CHM、CSV、DjVu、DOC、…...

PHP智能匹配轻松预订自习室在线订座系统小程序源码
智能匹配,轻松预订——自习室在线订座系统 📚【开篇:告别排队,迎接智能学习新时代】📚 还在为找不到合适的自习室座位而烦恼吗?是不是每次去图书馆或自习室都要提前好久去排队占位?现在&#…...

构建高效医护人员排班系统:Spring Boot框架的优势
1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及,互联网成为人们查找信息的重要场所,二十一世纪是信息的时代,所以信息的管理显得特别重要。因此,使用计算机来管理医护人员排班系统的相关信息成为必然。开发…...

深度学习——引言
一、机器学习的关键因素 1.1 数据 每个数据集由一 个个样本组成,大多情况下,数据遵循独立同分布。通常每个样本由一组特征属性组成。 好的数据集 { 数据样本多 正确的数据 ( g a r b a g e i n , g a r b a g e o u t ) 好的数据集 \begin{cases} 数据…...

安装Android Studio及第一个Android工程可能遇到的问题,gradle下载过慢、sync失败?
Android Studio版本众多,电脑操作系统、电脑型号、电脑硬件也是多种多样,幸运的半个小时内可以完成安装,碰到不兼容的电脑,一天甚至更长时间都无法安装成功。 Android安装及第一个Android工程分为4个步骤,为什么放到一…...

密码学---常见的其他密码
✨费纳姆密码:加解密都需要密钥,以二进制形式表示的密码。(密钥多是一次性的,称位一次性密码本) 加密过程: char_num {A: 1000001, B: 1000010, C: 1000011, D: 1000100,E: 1000101, F: 1000110, G: 100…...

Mysql8 主从复制主从切换(超详细)
文章目录 1 主从复制1.1 实施前提1.2 主节点配置(在192.168.25.91操作)1.3 从节点配置(在192.168.25.92操作)1.4 创建用于主从同步的用户1.5 开启主从同步1.5 主从同步验证 2 主从切换2.1 实施前提2.2 主节点设置只读(在192.168.25.91操作)2.3 检查主从数据是否同步完毕(在192.…...

8月29日wpf
小语 折磨我们的往往是想象,而不是真实。 学wpf 7.07 1.vs如何创建新项目? 退出,创建新项目,点c#,windows,进入界面 2.app.config在哪里? 好像只有这个。。。 试一下,不是 我…...

Android经典实战之SurfaceView原理和实践
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点 SurfaceView 是一个非常强大但也相对复杂的 UI 组件,特别适用于对性能要求较高的绘制任务,如视频播放、游戏等。 1. Su…...
蜜罐的识别
蜜罐技术本质上是对网络攻击方欺骗的一项技术,通过在服务上布置一些仿真的系统、网络服务、或是模拟一些物联网设备来诱惑攻击方对其实施攻击从而捕获攻击行为,分析攻击手段与方式,或是收集一些攻击者的个人信息来进行分析画像达到精准溯源的…...

传感与检测技术
感知技术 传感器基本特性 静态特性 动态特性 传感器分类 电阻式传感器 通常情况下,电阻应变传感器的灵敏系数为常数 根据测量对象不同可分为...

树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...
【决胜公务员考试】求职OMG——见面课测验1
2025最新版!!!6.8截至答题,大家注意呀! 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:( B ) A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...
leetcodeSQL解题:3564. 季节性销售分析
leetcodeSQL解题:3564. 季节性销售分析 题目: 表:sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...

逻辑回归暴力训练预测金融欺诈
简述 「使用逻辑回归暴力预测金融欺诈,并不断增加特征维度持续测试」的做法,体现了一种逐步建模与迭代验证的实验思路,在金融欺诈检测中非常有价值,本文作为一篇回顾性记录了早年间公司给某行做反欺诈预测用到的技术和思路。百度…...

c++第七天 继承与派生2
这一篇文章主要内容是 派生类构造函数与析构函数 在派生类中重写基类成员 以及多继承 第一部分:派生类构造函数与析构函数 当创建一个派生类对象时,基类成员是如何初始化的? 1.当派生类对象创建的时候,基类成员的初始化顺序 …...

DeepSeek源码深度解析 × 华为仓颉语言编程精粹——从MoE架构到全场景开发生态
前言 在人工智能技术飞速发展的今天,深度学习与大模型技术已成为推动行业变革的核心驱动力,而高效、灵活的开发工具与编程语言则为技术创新提供了重要支撑。本书以两大前沿技术领域为核心,系统性地呈现了两部深度技术著作的精华:…...

stm32wle5 lpuart DMA数据不接收
配置波特率9600时,需要使用外部低速晶振...

车载诊断架构 --- ZEVonUDS(J1979-3)简介第一篇
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…...

Vue3 PC端 UI组件库我更推荐Naive UI
一、Vue3生态现状与UI库选择的重要性 随着Vue3的稳定发布和Composition API的广泛采用,前端开发者面临着UI组件库的重新选择。一个好的UI库不仅能提升开发效率,还能确保项目的长期可维护性。本文将对比三大主流Vue3 UI库(Naive UI、Element …...