Mybatis映射接口的动态代理实现原理

Mybatis映射接口的动态代理实现原理

在上一节中,我们介绍了MyBatis的核心配置文件加载流程,Mybatis核心配置文件加载流程详解
在文中,我们介绍了MyBatis在加载配置文件的过程中会针对每个接口类都生成一个相应的MapperProxyFactory动态代理工厂类。

在MapperRegistry类中有一个叫做knownMappers的map缓存,其键为映射接口的Class对象,值为MapperProxyFactory对象,其有一个mapperInterface属性用来保存需要创建代理对象的接口类。

在MyBatis中,我们通过调用sqlSession.getMapper方法可以获取映射接口的动态代理对象,然后调用该映射接口定义的方法执行数据库操作时,就会去映射配置文件中找到该方法对应的SQL语句进行执行。

那么,MyBatis是如何实现上述流程的呢?接下来,我们就从源码中来一探究竟。

下面列出了测试主方法:

public class MybatisDemo {private static UserMapper userMapper;public static void main(String[] args) throws IOException {InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);SqlSession sqlSession = sqlSessionFactory.openSession();userMapper = sqlSession.getMapper(UserMapper.class);User u1 = userMapper.selectUserById(1);System.out.println(u1);}
}

因为动态代理的生成步骤是在sqlSession.getMapper方法中,因此,我们先进入DefaultSqlSession类下的到getMapper方法的源码中来看一下:

public <T> T getMapper(Class<T> type) {return configuration.getMapper(type, this);
}

这里会调用Configuration类下的getMapper方法,type即为映射接口类。然后我们进入到Configuration类下的getMapper方法:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {return mapperRegistry.getMapper(type, sqlSession);
}

这里调用了本文初所提及的MapperRegistry类下的getMapper方法,再进入:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);if (mapperProxyFactory == null) {throw new BindingException("Type " + type + " is not known to the MapperRegistry.");}try {return mapperProxyFactory.newInstance(sqlSession);} catch (Exception e) {throw new BindingException("Error getting mapper instance. Cause: " + e, e);}
}

这里,我们就看到了本文初介绍的,MyBatis在MapperRegistry类中会维护一个knownMappers的Map缓存,用来存储在解析核心配置文件的过程中,为每个映射接口生成的代理工厂类对象。
因此,这里就是根据被调用的映射接口,直接从该缓存中获取相应的MapperProxyFactory动态代理工厂类对象,如果获取不到则直接报错提示。

在获取到mapperProxyFactory后,调用newInstance方法来创建一个mapperProxy动态代理对象。接下来,我们进入到MapperProxyFactory类中观察一下它的实现逻辑,以及newInstance方法:

public class MapperProxyFactory<T> {private final Class<T> mapperInterface;private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();public MapperProxyFactory(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}public Class<T> getMapperInterface() {return mapperInterface;}public Map<Method, MapperMethod> getMethodCache() {return methodCache;}@SuppressWarnings("unchecked")protected T newInstance(MapperProxy<T> mapperProxy) {return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}public T newInstance(SqlSession sqlSession) {final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);return newInstance(mapperProxy);}}

MapperProxyFactory中包含了两个属性:

  • mapperInterface:需要创建代理对象的映射接口
  • methodCache:是一个map缓存,其键为映射接口的方法对象,值为这个方法对应的MapperMethodInvoker。实际上,SQL的执行最终会由MapperMethodInvoker方法来完成,后面会详细说明。

再观察MapperProxyFactory中的两个重载的newInstance方法,可以看到最终重载方法会通过执行Proxy.newProxyInstance来创建映射接口的动态代理对象。而基于JDK动态代理的原理我们知道,代理对象方法的执行最终是在其实现的InvocationHandler接口的invoke方法中被执行的。而观察上面的代码不难发现,MapperProxy类必定是InvocationHandler接口的实现类:
在这里插入图片描述

果然,MapeprProxy类实现了InvocationHandler接口,并在创建MapperProxy时,MapperProxyFactory会将其持有的methodCache传递给MapperProxy,因此methodCache的实际读写是由MapperProxy来完成的。

下面来看一下MapperProxy实现的invoke方法:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);} else if (method.isDefault()) {return invokeDefaultMethod(proxy, method, args);}} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}// 从methodCache中根据方法对象获取MapperMethodInvoker来执行sql// 如果获取不到,则创建一个MapperMethodInvoker并添加到methodCache中,再执行Sqlfinal MapperMethod mapperMethod = cachedMapperMethod(method);return mapperMethod.execute(sqlSession, args);
}private MapperMethod cachedMapperMethod(Method method) {return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}

通过上面的代码我们可以知道之前提及的methodCache属性的作用,其实就是缓存了每个方法相应的MapperMethod对象,而MapperMethod对象是做什么用的呢?接下来,我们继续进入到execute方法:

public Object execute(SqlSession sqlSession, Object[] args) {Object result;switch (command.getType()) {case INSERT: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.insert(command.getName(), param));break;}case UPDATE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.update(command.getName(), param));break;}case DELETE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.delete(command.getName(), param));break;}case SELECT:if (method.returnsVoid() && method.hasResultHandler()) {executeWithResultHandler(sqlSession, args);result = null;} else if (method.returnsMany()) {result = executeForMany(sqlSession, args);} else if (method.returnsMap()) {result = executeForMap(sqlSession, args);} else if (method.returnsCursor()) {result = executeForCursor(sqlSession, args);} else {Object param = method.convertArgsToSqlCommandParam(args);result = sqlSession.selectOne(command.getName(), param);if (method.returnsOptional()&& (result == null || !method.getReturnType().equals(result.getClass()))) {result = Optional.ofNullable(result);}}break;case FLUSH:result = sqlSession.flushStatements();break;default:throw new BindingException("Unknown execution method for: " + command.getName());}if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {throw new BindingException("Mapper method '" + command.getName()+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");}return result;
}

喔!这里我们看到几个case标签不就是对应到我们执行SQL的几种类别的操作吗,看来代理对象方法的执行最终会进入到MapperMethodexecute方法里,并根据方法对应SQL的类别执行相应的处理逻辑。那么是如何根据映射接口的方法来得到SQL的执行类别呢?

很明显,就是要根据映射接口的方法来获取到映射配置文件中相应的SQL标签,而这种关联就要利用到MappedStatement对象了。在前文MyBatis的核心配置文件加载过程中,我们详细分析了,在MyBatis加载的过程中,会对映射配置文件中的每一个<select><insert><update><delete>标签根据其namespace+id值生成一个MappedStatement对象,存储在Configuration中。因此,要实现上面的关联,只需要根据映射接口类名+方法名,从Configuration中获取到相应的MappedStatement对象即可。

那么MyBatis的实际实现是不是正如我们上面的分析所料呢?答案就在MapperMethod的构造方法中:

public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {this.command = new SqlCommand(config, mapperInterface, method);this.method = new MethodSignature(config, mapperInterface, method);
}

在该构造方法中,会根据传入的映射接口的Class对象,映射接口被调用方法以及配置类Configuration来创建SqlCommandMethodSignature对象。

  • SqlCommand主要是保存和映射接口被调用方法所关联的MappedStatement的信息;
  • MethodSignature主要是存储映射接口被调用方法的参数信息和返回值信息。

来看一下SqlCommand的构造方法:

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {// 获取映射接口被调用方法的方法名final String methodName = method.getName();// 获取被调用方法的接口的Class对象final Class<?> declaringClass = method.getDeclaringClass();// 获取和映射接口被调用方法关联的MappedStatement对象MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,configuration);if (ms == null) {if (method.getAnnotation(Flush.class) != null) {name = null;type = SqlCommandType.FLUSH;} else {throw new BindingException("Invalid bound statement (not found): "+ mapperInterface.getName() + "." + methodName);}} else {// 将MappedStatement的id值赋值给SqlCommand的name字段name = ms.getId();// 将MappedStatement的Sql命令类别赋值给SqlCommand的type字段// 比如SELECT、INSERT、UPDATE、DELETEtype = ms.getSqlCommandType();if (type == SqlCommandType.UNKNOWN) {throw new BindingException("Unknown execution method for: " + name);}}
}

构造函数中主要做了这些事情:

  • 先获取和被调用方法关联的MappedStatement对象;
  • 然后将MappedStatementid字段赋值给SqlCommandname字段;
  • 最后将MappedStatementsqlCommandType字段赋值给SqlCommandtype字段。
    这样一来,SqlCommand就具备了和被调用方法关联的MappedStatement的信息。那么如何获取和被调用方法关联的MappedStatement对象呢,继续看resolveMappedStatement() 的实现,如下所示:
  private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,Class<?> declaringClass, Configuration configuration) {// 根据接口全限定类名+"."+方法名拼接出MappedStatement的idString statementId = mapperInterface.getName() + "." + methodName;if (configuration.hasStatement(statementId)) {// 根据statementId获取configuration中保存的MappedStatement对象return configuration.getMappedStatement(statementId);} else if (mapperInterface.equals(declaringClass)) {return null;}for (Class<?> superInterface : mapperInterface.getInterfaces()) {if (declaringClass.isAssignableFrom(superInterface)) {// 递归调用 MappedStatement ms = resolveMappedStatement(superInterface, methodName,declaringClass, configuration);if (ms != null) {return ms;}}}return null;}
}

终于在这里,我们就可以利用源码来佐证我们上面的分析了!实际上,MyBatis就是根据被调用映射接口的类名+方法名拼接成statementId,并利用该statementId从Configuration对象中获取到相应的MappedStatement对象。这也就是为什么在MyBatis中老生常谈的问题:映射配置文件中的<select>等标签id要跟映射接口的方法名保持一致,且映射配置文件的<mappers namespace=xxx>namespace属性要写成映射接口的全限定类名。

上面代码中有一处递归调用逻辑,可能比较难以理解。这里简单介绍下,实际上这么做的目的是:

MyBatis3.4.2及以前的版本,只会根据映射接口的全限定名 + “.” + 方法名声明被调用方法的接口的全限定名 + “.” + 方法名ConfigurationmappedStatements缓存中获取MappedStatement,那么按照这样的逻辑,如果是某些继承映射接口的子接口类是无法获取到MappedStatement的,因此在MyBatis3.4.3及之后的版本中,采用了resolveMappedStatement() 方法中的逻辑,以支持继承了映射接口的接口对应的SqlCommand也能和映射接口对应的MappedStatement相关联

对于SqlCommand的分析到此为止。

至此,回到上面定位到MapperMethod类中的execute方法中,我们就知道了MyBatis对于映射接口的动态代理实现原理,以及执行流程。最终在execute方法中,会根据被调用方法关联的SQL执行类别,将方法传入的参数进行转换,最终在SqlSession中调用相应的方法来执行SQL语句。

总结一下本文的主要内容如下:

  1. 动态代理实现原理
    在MyBatis启动加载核心配置文件时,会对每个映射接口生成一个MapperProxyFactory对象,并保存在MapperRegistry对象的knownMappers缓存中,便于后续使用。当调用SqlSession.getMapper方法来获取映射接口的代理对象时,会从MapperRegistry对象的knownMappers缓存中获取该映射接口对应的动态代理工厂类,该工厂类会通过Proxy.newProxyInstance方法来创建一个代理对象。同时,会创建一个MapperProxy对象在invoke方法中执行真正的被调用方法执行逻辑。在invoke方法中,会获取到该调用方法对应的MapperMethod对象,并在其execute方法中结合SqlSession来执行具体的增删改查逻辑。

  2. MyBatis中的动态代理是对接口的代理
    MyBatisJDK动态代理中,是不存在被代理对象的,是对接口的代理。MapperProxy实现了InvocationHandler接口,因此MapperProxyMyBatisJDK动态代理中扮演调用处理器的角色,即调用映射接口的方法时,实际上是调用的MapperProxy实现的invoke() 方法,又因为不存在被代理对象,所以在MapperProxyinvoke() 方法中,并没有去调用被代理对象的方法,而是会基于映射接口和被调用方法的方法对象生成MapperMethod并执行MapperMethodexecute() 方法,即调用映射接口的方法的请求会发送到MapperMethod
    可以理解为映射接口的方法由MapperMethod代理

最后,用一张图归纳一下MyBatis中的动态代理执行流程,如下所示:

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

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

相关文章

【上海大学数字逻辑实验报告】六、时序电路

一、 实验目的 掌握同步二进制计数器和移位寄存器的原理。学会用分立元件构成2位同步二进制加计数器。学会在Quartus II上设计单向移位寄存器。学会在Quartus II上设计环形计数器。 二、 实验原理 同步计数器是指计数器中的各触发器的时钟脉冲输入端连接在一起&#xff0c;接…

FL Studio Producer Edition 21.2.2.3914中文汉化破解版新功能介绍及下载安装教程

FL Studio Producer Edition 21.2.2.3914中文汉化破解版 也就是 Image-Line 出品的一款功能强大的编曲软件&#xff0c;全名 Fruity Loops Studio 简称“FL Studio”今天突然的发现我们经常使用的水果音乐制作软件 FL STUDIO 居然从FL STUDIO 21.1.1 一下子跨越了版本号到了FL …

【产品经理】需求池和版本树

在这个人人都是产品经理的时代&#xff0c;每位入行的产品人进阶速度与到达高度各有不同。本文作者结合自身三年产品行业的经历&#xff0c;根据案例拆解产品行业的极简研发过程、需求池、版本树、产品自我优化等相关具体方法论。 一、产品研发的极简过程 1. 产品概述 产品就…

Server check fail, please check server xxx.xxx.xxx.xxx,port 9848 is available

记录一次服务调用中的错误 背景&#xff1a;我使用了nacos2.x的版本&#xff0c;同时在同一台服务器的三个docker容器中部署了nacos1、2、3&#xff0c;并将它们连接到了同一个docker网络 错误&#xff1a;Server check fail, please check server xxx.xxx.xxx.xxx,port 9848 …

C/C++,动态 DP 问题的计算方法与源程序

1 文本格式 #include <bits/stdc.h> using namespace std; typedef long long LL; const int maxn 500010; const int INF 0x3f3f3f3f; int Begin[maxn], Next[maxn], To[maxn], e, n, m; int size[maxn], son[maxn], top[maxn], fa[maxn], dis[maxn], p[maxn], i…

AI PC行业深度研究报告:AI PC革新端侧AI交互体验

今天分享的人工智能系列深度研究报告&#xff1a;《AI PC行业深度研究报告&#xff1a;AI PC革新端侧AI交互体验》。 &#xff08;报告出品方&#xff1a;华创证券&#xff09; 报告共计&#xff1a;28页 点击添加图片描述&#xff08;最多60个字&#xff09;编辑 一、硬件端…

12.字符串拼接【2023.12.4】

1.问题描述 我们在编程过程中经常会遇到把不同字符串拼接在一起的情况&#xff0c;从而更直观地展示给用户我们所要表达的信息。本题将给出两个字符串&#xff0c;请依次将这两个字符串拼接在一起。 2.解决思路 用字符串拼接符 进行连接两个字符串 3.代码实现 str1input(…

XSS漏洞 深度解析 XSS_labs靶场

XSS漏洞 深度解析 XSS_labs靶场 0x01 简介 XSS原名为Cross-site Sciprting(跨站脚本攻击)&#xff0c;因简写与层叠样式表(Cascading style sheets)重名&#xff0c;为了区分所以取名为XSS。 这个漏洞主要存在于HTML页面中进行动态渲染输出的参数中&#xff0c;利用了脚本语…

Apollo配置发布原理解析

&#x1f4eb;作者简介&#xff1a;小明java问道之路&#xff0c;2022年度博客之星全国TOP3&#xff0c;专注于后端、中间件、计算机底层、架构设计演进与稳定性建设优化&#xff0c;文章内容兼具广度、深度、大厂技术方案&#xff0c;对待技术喜欢推理加验证&#xff0c;就职于…

P1单片机定时器配置及定时器中断——C51(超详细)

目录 1. 简介 1.1 概念解读 1.2 定时器怎么定时 1.什么是晶振 2.什么是时钟周期 3.什么是机器周期 4.加1经过了多少时间 1.3 定时器编程 1.如何算出10ms定时器的初值(TL0 TH0) 2.关于TCON ,怎么知道爆表 3.怎么开始计时(TR0) 4.定时器使用是有很多种模式的&#xf…

深入了解基础故障编排:提升系统故障应对能力的关键

在当今高度数字化的世界中&#xff0c;系统的稳定性和可用性对于业务的成功至关重要。然而&#xff0c;在复杂的软件和硬件环境中&#xff0c;故障不可避免地会发生。为了更有效地应对这些故障&#xff0c;基础故障编排成为一项关键的技术。本文将探讨基础故障编排的概念及作用…

「PPT 下载」Google DevFest Keynote | 复杂的海外网络环境下,如何提升连接质量

&#xff08;全网都在找的《社交泛娱乐出海作战地图》&#xff0c;点击获取&#x1f446;&#xff09; 12 月 10 日&#xff0c;“Google DevFest 2023 上海站”大会如期在上海市东方万国宴会中心举办。延续过往的技术交流碰撞、前沿技术学习基调传统&#xff0c;本届大会聚焦行…

基于导数Zernike多项式拟合技术的干涉测量二维相位展开算法(原文翻译)

Zixin Zhao1&#xff0c;Hong Zhao1、Lu Zhang 1&#xff0c;Fen Gao2&#xff0c;Yuwei Qin3&#xff0c;Hubing Du 摘要: 我们提出了一种适用于一般干涉测量应用的相位展开方法。所提出的方法依赖于导数泽尼克多项式拟合&#xff08;DZPF&#xff09;技术&#xff0c;其中相…

淡化了技术指标 还能做现货黄金交易?

技术指标是分析和预测现货黄金走势的其中一种方法&#xff0c;普通投资者多数依赖技术指标为自己的交易做判断。然而&#xff0c;近几年有一种观点认为&#xff0c;我们应该淡化技术指标&#xff0c;少使用或者不用技术分析来服务我们的交易。这个观点引起了不少投资者的思考&a…

现代密码学复习

密码学总结 目录 密码学总结 第一章——只因础模型与概念 1.1 密码学五元组&#xff08;结合&#x1f40f;皮卷&#xff09; 1.2 Dolev-Yao威胁模型 1.3 攻击类型 1.4 柯克霍夫原则&#xff08;Kerckhoffss principle&#xff09; 1.5 对称、非对称加密 1.6 密码的目标…

优雅玩转实验室服务器(二)传输文件

使用服务器最重要的肯定是传输文件了&#xff0c;我们不仅需要本地的一些资源上传到服务器&#xff0c;好进行实验&#xff0c;也需要将服务器计算得到的实验结果传输到本地&#xff0c;来进行预览或者报告撰写。 首先&#xff0c;由于涉及到服务器操作&#xff0c;我强烈推荐…

【FPGA】Verilog:BCD 加法器的实现 | BCD 运算 | Single-level 16 bit 超前进位加法器 | 2-level 16-bit 超前进位加法器

0x00 BCD 运算 在 BCD 中,使用4位值作为操作数,但由于只表示 0 到 9 的数字,因此只使用 0000 到 1001 的二进制数,而不使用 1010 到 1111 的二进制数(dont care)。 因此,不能使用常规的 2complement 运算来计算,需要额外的处理:如果 4 位二进制数的运算结果在 1010 …

C++共享和保护——(1)作用域

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd; 人生就像骑单车&#xff0c;要想平衡就…

vue项目中 CDN 是vue本身的依赖可以按需加载还是项目中所有的第三方库都可以按需加载?

这是我看到CDN简介时产生的问题 相信很多小伙伴会有 和我一样的疑问 在这里 我也统一回答一下 CDN&#xff08;内容分发网络&#xff09;是一种通过将数据分发到全球各个节点&#xff0c;以提供快速、可靠的内容传输的技术。在Vue项目中&#xff0c;CDN可以用于按需加载Vue本…

MicroSD 卡 使用读卡器 读取速度测试

设备 - - 电脑为m.2固态硬盘 usb口为USB3.2 gen2接口(即支持1GB/s的接口) cpu: amd3600 测试方案1 直接MicroSD卡放入读卡器测试 38MB/s 从sd卡复制到本地C盘 测试方案2 MicroSD卡使用闪迪的SD卡套套上之后一起插入读卡器 76MB/s 从sd卡复制到本地C盘