Mybatis链路分析:JDK动态代理和责任链模式的应用

c1e38ab917451ae60c6705914a983e39.jpeg

背景

此前写过关于代理模式的文章,参考:代理模式

动态代理功能:生成一个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. 【1】获取环境变量

  2. 【2】创建新事务

  3. 【3】创建一个新的Executor执行器(非常关键)

  4. 【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;
}

源码分析:

8002fed400aa97bca32e90c74078d413.png

  • 最核心的部分,就是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

4c887eed1463cd6242d169ac1bf6e63b.png

最终执行的逻辑,是调用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进行增强

总结

608af4af16f0937a52e68cc481095185.png

从图中可以知道,Mybatis的拦截器链运用了动态代理和责任链模式:

  • 其实就是代理对象再次生成代理对象,特殊的是代理对象的target属性

  • 另外配合Invocation类中的proceed方法形成责任链路的调用,这样就可以在我们执行sql的前后,做一些特殊的自定义的事情了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/416154.html

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

相关文章

算法笔试-编程练习-好题-03

这是一道非常综合的质数类的题目&#xff0c;值得仔细理解。 题目描述 n个正整数ai&#xff0c;希望你求出这些数的阶乘全部乘在一起生成的大数有多少个因子 输入描述 第一行输入一个正整数n。 第二行输入n个正整数ai​&#xff0c;用空格隔开 1≤n≤2 1≤ai≤ 输出描述 一个…

IP地址存在的意义及更改方法探析

在互联网的广阔天地中&#xff0c;‌每一个连接的设备都拥有一个独特的身份标识——IP地址。‌它不仅是设备在网络中的“身份证”&#xff0c;‌更是确保数据传输准确无误的基石。‌然而&#xff0c;‌随着网络环境的不断变化&#xff0c;‌有时我们需要更改设备的IP地址以适应…

关于SpringMVC的理解

1、SpringMVC 应用 1.1、简介 1.1.1、MVC 体系结构 三层架构&#xff1a; 我们的开发架构⼀般都是基于两种形式&#xff0c;⼀种是 C/S 架构&#xff0c;也就是客户端/服务器&#xff1b;另⼀种是 B/S 架构&#xff0c;也就是浏览器服务器。在 JavaEE 开发中&#xff0c;⼏乎…

2024最新PyCharm下载安装激活汉化教程!(附激活码)

激活码&#xff08;文末附带精品籽料&#xff09;&#xff1a; K384HW36OB-eyJsaWNlbnNlSWQiOiJLMzg0SFczNk9CIiwibGljZW5zZWVOYW1lIjoibWFvIHplZG9uZyIsImxpY2Vuc2VlVHlwZSI6IlBFUlNPTkFMIiwiYXNzaWduZWVOYW1lIjoiIiwiYXNzaWduZWVFbWFpbCI6IiIsImxpY2Vuc2VSZXN0cmljdGlvbiI6I…

Qt模态对话框与非模态对话框

前言 在 Qt 中&#xff0c;模态对话框和非模态对话框是两种常见的对话框类型&#xff0c;它们的主要区别在于用户与应用程序的交互方式。 正文 对话框就是指QDialog嘛。 模态对话框 (Modal Dialog) 定义: 模态对话框是指在弹出对话框期间&#xff0c;用户无法与应用程序的…

Linux的远程登录教程(超详细)

我们在进行远程登录时要用的一种协议叫SSH&#xff0c;那什么叫SSH呢&#xff1f; SSH&#xff08;Secure Shell&#xff09;是一种网络协议&#xff0c;用于在不安全的网络中提供安全的远程登录和其他网络服务。它通过加密技术确保数据在传输过程中的机密性和完整性&#xff…

Python | Leetcode Python题解之第393题UTF-8编码验证

题目&#xff1a; 题解&#xff1a; class Solution:def validUtf8(self, data: List[int]) -> bool:MASK1, MASK2 1 << 7, (1 << 7) | (1 << 6)def getBytes(num: int) -> int:if (num & MASK1) 0:return 1n, mask 0, MASK1while num & m…

如何快速采集淘宝商品数据?

无论是谁&#xff0c;如果单凭人工的方式去收集淘宝、天猫等平台的商品数据信息&#xff0c;工作量是巨大的&#xff0c;如果借助有采集软件的第三方公司操作&#xff0c;则可实现对大数据的轻松掌握&#xff0c;但是外包给第三方公司需要支付一定的费用&#xff0c;包含技术费…

【IPV6从入门到起飞】2-2 获取你的IPV6(Teredo隧道)

【IPV6从入门到起飞】2-2 获取你的IPV6&#xff08;Teredo隧道&#xff09; 1 打工人的忧伤2 Teredo介绍2.1 背景2.2 工作原理 3 Linux 服务器获取IPV63.1 安装3.2 设置开机自启动和启动3.3 开放防火墙 UDP 35443.4 查看IPV6以及ping包测试3.5 修改Teredo服务器3.6 重启服务3.7…

SpringBoot 项目集成 xxl-job

1. xxl-job 官网 https://www.xuxueli.com/xxl-job/ 2. git 拉取 xxl-job 源码 2.1 源码仓库地址 https://github.com/xuxueli/xxl-job http://gitee.com/xuxueli0323/xxl-job 2.2 git 拉取源码 git clone https://gitee.com/xuxueli0323/xxl-job.git 2.3 git拉取源码时&…

C++11重大新增特性:左值引用 右值引用 移动构造 移动赋值

C11重大新增特性&#xff1a;左值引用 & 右值引用 & 移动构造 & 移动赋值 一、右值引用和左值引用概念和区别1.1 左值 & 左值引用1.2 右值 & 右值引用 二、左值引用和右值引用对比2.1 左值引用2.1 右值引用 三、右值和右值引用诞生的意义四、移动构造 &…

【射频通信电子线路基础第一讲】射频电子线路基础绪论——射频概念、通信系统、语义通信

1. 射频与高频广义上的概念厘清 高频&#xff1a;就是频率高&#xff08;大于10K&#xff09;&#xff0c;单位一般用MHz&#xff08;兆赫&#xff09;表示。 射频&#xff1a;Radio Frequency&#xff0c;简称RF&#xff0c;300K-300G。射频就是射频电流&#xff0c;它是一种…

Java详解String 字符串类以及String内存原理、StringBuilder类、StringJoiner类(附有代码+案例)

文章目录 九.String 字符串类型9.0 String概述9.1 字符串常用方法9.2 String内存图9.2.1直接赋值9.2.2new出来 9.3字符串比较9.4 字符串遍历9.4.1 统计字符串大小写及数字9.4.2 拼接字符串9.4.3字符串反转 9.5 StringBuilder类9.5.1StringBuilder 构造方法9.5.2StringBuilder常…

es集群详解

1、基本介绍 1.1、为什么需要集群 单台 Elasticsearch 服务器提供服务&#xff0c;往往都有最大的负载能力&#xff0c;超过这个阈值&#xff0c;服务器性能就会大大降低甚至不可用&#xff0c;所以生产环境中&#xff0c;ES 一般都是运行在指定服务器集群中。 除了负载能力&…

九银十拿到大模型(LLM)offer,面试八股

金九银十拿到大模型&#xff08;LLM&#xff09;offer&#xff0c;面试八股 从事大模型的朋友在 金J九银十拿到了一份不错的offer&#xff0c;面试十几家公司&#xff0c;通过了六家。好在分享了大佬总结的大模型方向面试的常见题目&#xff08;含答案&#xff09;&#xff0c;…

RS232转RS485

1.232转485转换器 232转485转换器是RS-232与RS-485之间的双向接口的转换器&#xff0c;应用于主控机之间&#xff0c;主控机与单片机或外设之间构成点到点&#xff0c;点到多点远程多机通信网络&#xff0c;实现多机应答通信&#xff0c;广泛地应用于工业自动化控制系统&#x…

LLM代码实现-Qwen(Function Calling)

简介 Function Calling 是一种让 Chat Completion 模型调用外部函数的能力&#xff0c;可以让模型不仅仅根据自身的数据库知识进行回答&#xff0c;而是可以额外挂载一个函数库&#xff0c;然后根据用户提问去函数库检索&#xff0c;按照实际需求调用外部函数并获取函数运行结…

Unknown command: “create-react-app“

在创建react项目时出现报错" Unknown command: "create-react-app" " 解决方法&#xff1a; 配置全局变量&#xff0c;" win r " 打开cmd窗口&#xff0c;输入下列命令&#xff0c;回车等待结束即可&#xff1a; npx create-react-app my-pro…

Docker部署项目时的服务端口设置——给容器添加新端口映射

Docker给容器添加新端口映射 1 Docker安装Ubuntu22.042 创建新容器3 给容器添加端口映射3.1 查看运行的容器3.2 查看容器挂载目录3.3 停止容器3.4 停止docker服务3.5 进入容器挂载目录3.6 修改config.v2.json文件3.7 修改hostconfig.json文件3.8 启动docker3.9 启动容器 4 端口…

七款最佳的渗透测试工具(非常详细)零基础入门到精通,收藏这一篇就够了

渗透测试工具是模拟对计算机系统、网络或 Web 应用程序的网络攻击的软件应用程序&#xff0c;它们的作用是在实际攻击者之前发现安全漏洞。它们可以作为系统的压力测试&#xff0c;揭示哪些区域可能会受到真正的威胁。 本文我将介绍七款最佳的渗透测试工具。 1 Kali Linux K…