您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
源码学习之Mybatis的底层查询原理
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
源码学习之Mybatis的底层查询原理
自猿其说Tech
2022-03-16
IP归属:未知
305960浏览
计算机编程
在平时的代码编写中,发现了Mybatis一个低版本的bug(3.4.5之前的版本),由于现在很多工程中的版本都是低于3.4.5的,因此在这里用一个简单的例子复现一下问题,并且从源码角度分析Mybatis一次查询的流程,让大家了解一下Mybatis的查询原理。 ### 1 问题现象 #### 1.1 场景问题复现 如下图所示,在示例Mapper中,我们提供了一个方法queryStudents,从student表中查询出符合查询条件的数据,入参可以为student_name或者student_name的集合,示例中参数我们只传入的是studentName的List集合 ```java List<String> studentNames = new LinkedList<>(); studentNames.add("lct"); studentNames.add("lct2"); condition.setStudentNames(studentNames); ``` ![](//img1.jcloudcs.com/developer.jdcloud.com/0bd190c6-964d-402a-822d-a78e5a4fa3b220220316143243.png) 我们期望运行的结果是 ```sql select * from student WHERE student_name IN ( 'lct' , 'lct2' ) ``` 但是实际上运行的结果是 ![](//img1.jcloudcs.com/developer.jdcloud.com/be4587a1-17ba-4c76-9559-3a593081832420220316143339.png) 通过运行结果可以看到,我们没有给student_name单独赋值,但是经过Mybatis解析以后,单独给student_name赋值了一个值,可以推断出mybatis在解析SQL并对变量赋值的时候是有问题的,初步猜测是foreach循环中的变量的值带到了foreach外边,导致SQL解析出现异常,下面通过源码进行分析验证一下。 ### 2 Mybatis查询原理 #### 2.1 mybatis架构 ##### 2.1.1 架构图 我们先简单来看一下mybatis整体上的架构模型,从整体上看Mybatis主要分为四大模块 ![](//img1.jcloudcs.com/developer.jdcloud.com/dd6317a7-c9b8-4bed-9d1a-727eb52131cc20220316143411.png) <center>图片来自于网络</center> 每个模块的主要作用为: - 接口层:主要作用就是和数据库打交道 - 数据处理层:数据处理层可以说是MyBatis 的核心,它要完成两个功能: - 通过传入参数构建动态SQL语句; - SQL语句的执行以及封装查询结果集成List<E> - 框架支撑层:主要有事务管理、连接池管理、缓存机制和SQL语句的配置方式 - 引导层:引导层是配置和启动MyBatis 配置信息的方式。MyBatis 提供两种方式来引导MyBatis :基于XML配置文件的方式和基于Java API 的方式 ##### 2.1.2 mybatis四大对象 贯穿Mybatis整个框架的有四大核心对象,ParameterHandler、ResultSetHandler、StatementHandler和Executor,四大对象贯穿了整个框架的执行过程,四大对象的主要作用为: - ParameterHandler:设置预编译参数 - ResultSetHandler:处理SQL的返回结果集 - StatementHandler:处理sql语句预编译,设置参数等相关工作 - Executor:MyBatis的执行器,用于执行增删改查操作 #### 2.2 从源码解读一下mybatis的一次查询过程 首先给出复现问题的代码以及相应的准备过程 ##### 2.2.1 数据准备 ```sql CREATE TABLE `student` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `student_name` varchar(255) NULL DEFAULT NULL, `age` int(11) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1; -- ---------------------------- -- Records of student -- ---------------------------- INSERT INTO `student` VALUES (1, 'lct', 1); INSERT INTO `student` VALUES (2, 'lct2', 2); ``` ##### 2.2.2 代码准备 ###### 1.mapper配置文件 ```xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="mybatis.StudentDao"> <!-- 映射关系 --> <resultMap id="resultMap" type="mybatis.Student"> <id column="id" property="id" jdbcType="BIGINT" /> <result column="student_name" property="studentName" jdbcType="VARCHAR" /> <result column="age" property="age" jdbcType="INTEGER" /> </resultMap> <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap"> select * from student <where> <if test="studentNames != null and studentNames.size > 0 "> AND student_name IN <foreach collection="studentNames" item="studentName" open="(" separator="," close=")"> #{studentName, jdbcType=VARCHAR} </foreach> </if> <if test="studentName != null and studentName != '' "> AND student_name = #{studentName, jdbcType=VARCHAR} </if> </where> </select> </mapper> ``` ###### 2.示例代码 ```java public static void main(String[] args) throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); //1.获取SqlSessionFactory对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //2.获取对象 SqlSession sqlSession = sqlSessionFactory.openSession(); //3.获取接口的代理类对象 StudentDao mapper = sqlSession.getMapper(StudentDao.class); StudentCondition condition = new StudentCondition(); List<String> studentNames = new LinkedList<>(); studentNames.add("lct"); studentNames.add("lct2"); condition.setStudentNames(studentNames); //执行方法 List<Student> students = mapper.queryStudents(condition); } ``` ##### 2.2.3 查询过程分析 ###### 1.SqlSessionFactory的构建 我们先看SqlSessionFactory的对象的创建过程 ```java //1.获取SqlSessionFactory对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); ``` 代码中首先通过调用SqlSessionFactoryBuilder中的build方法来获取对象,进入build方法 ![](//img1.jcloudcs.com/developer.jdcloud.com/f3f9f35e-f7d6-4b8c-9a0c-e8136102444e20220316143752.png) 调用自身的build方法 ![](//img1.jcloudcs.com/developer.jdcloud.com/b32225af-e035-4576-9412-1ab4b62dc66920220316143807.png) 在这个方法里会创建一个XMLConfigBuilder的对象,用来解析我们传入的mybatis的配置文件,然后调用parse方法进行解析 ![](//img1.jcloudcs.com/developer.jdcloud.com/d32c1ea3-b19f-45c7-a654-5a27c8a56aad20220316143824.png) 在这个方法中,会从mybatis的配置文件的根目录中获取xml的内容,其中parser这个对象是一个XPathParser的对象,这个是专门用来解析xml文件的,具体怎么从xml文件中获取到各个节点这里不再进行讲解。这里可以看到解析配置文件是从configuration这个节点开始的,在mybatis的配置文件中这个节点也是根节点 ![](//img1.jcloudcs.com/developer.jdcloud.com/a6d3a154-be13-461e-b14a-077fd463297020220316143844.png) 然后将解析好的xml文件传入parseConfiguration方法中,在这个方法中会获取在配置文件中的各个节点的配置 ![](//img1.jcloudcs.com/developer.jdcloud.com/72f5ec2b-ca87-4de2-987a-2aeb29a7deea20220316143858.png) 以获取mappers节点的配置来看一下具体的解析过程 ![](//img1.jcloudcs.com/developer.jdcloud.com/6324cb94-0a8c-40b3-9496-e5269312721f20220316143915.png) 进入mapperElement方法 ![](//img1.jcloudcs.com/developer.jdcloud.com/5d81aadd-97cc-4af5-924a-e72ee3bf537820220316143940.png) ![](//img1.jcloudcs.com/developer.jdcloud.com/5a3a6e18-3cdb-4677-827b-38abf05327e420220316143958.png) 我们看到mybatis还是通过创建一个XMLMapperBuilder对象来对mappers节点进行解析,在parse方法中 ![](//img1.jcloudcs.com/developer.jdcloud.com/937a1e79-1d08-4c8b-ab7b-ae85c1f072cb20220316144018.png) 通过调用configurationElement方法来解析配置的每一个mapper文件 ![](//img1.jcloudcs.com/developer.jdcloud.com/6bb2b8d1-13f2-4a65-85fe-d70e139ea6d220220316144041.png) 我们以解析mapper中的增删改查的标签来看一下是如何解析一个mapper文件的 进入buildStatementFromContext方法 ![](//img1.jcloudcs.com/developer.jdcloud.com/7e0785af-3b7b-4719-8503-5256ab4a267220220316144103.png) 可以看到mybatis还是通过创建一个XMLStatementBuilder对象来对增删改查节点进行解析,通过调用这个对象的parseStatementNode方法,在这个方法里会获取到配置在这个标签下的所有配置信息,然后进行设置 ![](//img1.jcloudcs.com/developer.jdcloud.com/0059b6a0-7c70-41bc-baec-d68cad523fb820220316144120.png) 解析完成以后,通过方法addMappedStatement将所有的配置都添加到一个MappedStatement中去,然后再将mappedstatement添加到configuration中去 ![](//img1.jcloudcs.com/developer.jdcloud.com/cf05fe49-44fa-4761-94dd-85c0b9d5849120220316144140.png) ![](//img1.jcloudcs.com/developer.jdcloud.com/47e1d638-0d00-4795-a8f6-3e3cc8826b8420220316144149.png) 可以看到一个mappedstatement中包含了一个增删改查标签的详细信息 ![](//img1.jcloudcs.com/developer.jdcloud.com/3fac6f92-56a4-4ec8-be22-2efabfe10a4c20220316144209.png) 而一个configuration就包含了所有的配置信息,其中mapperRegistertry和mappedStatements ![](//img1.jcloudcs.com/developer.jdcloud.com/f3e1e63b-e57f-4bb4-a73c-817a6794252a20220316144309.png) 具体的流程为 ![](//img1.jcloudcs.com/developer.jdcloud.com/d8507881-00ce-41d9-8591-aea355dcc7db20220316144326.png) ###### 2.SqlSession的创建过程 SqlSessionFactory创建完成以后,接下来看一下SqlSession的创建过程 ```java SqlSession sqlSession = sqlSessionFactory.openSession(); ``` 首先会调用DefaultSqlSessionFactory的openSessionFromDataSource方法, ![](//img1.jcloudcs.com/developer.jdcloud.com/22c77b91-1d9a-49dd-aa71-1347f67b037620220316144420.png) 在这个方法中,首先会从configuration中获取DataSource等属性组成对象Environment,利用Environment内的属性构建一个事务对象TransactionFactory ![](//img1.jcloudcs.com/developer.jdcloud.com/1e7c13dd-9f80-4c0a-9f5a-6c902cbf34cd20220316144439.png) 事务创建完成以后开始创建Executor对象,Executor对象的创建是根据 executorType创建的,默认是SIMPLE类型的,没有配置的情况下创建了SimpleExecutor,如果开启二级缓存的话,则会创建CachingExecutor, ![](//img1.jcloudcs.com/developer.jdcloud.com/f7a5854c-449e-47c4-98ac-8ec7578a31c720220316144457.png) 创建executor以后,会执行executor = (Executor) interceptorChain.pluginAll(executor)方法,这个方法对应的含义是使用每一个拦截器包装并返回executor,最后调用DefaultSqlSession方法创建SqlSession ![](//img1.jcloudcs.com/developer.jdcloud.com/5ebd2b29-3756-4889-9bd5-67f1918d072c20220316144514.png) ###### 3.Mapper的获取过程 有了SqlSessionFactory和SqlSession以后,就需要获取对应的Mapper,并执行mapper中的方法 ```java StudentDao mapper = sqlSession.getMapper(StudentDao.class); ``` 我们在第一步中知道所有的mapper都放在MapperRegistry这个对象中,因此我们通过调用org.apache.ibatis.binding.MapperRegistry#getMapper方法来获取对应的mapper ![](//img1.jcloudcs.com/developer.jdcloud.com/a446a629-5295-4cff-a44c-46f0dec0469120220316144623.png) 在Mybatis中,所有的mapper对应的都是一个代理类,获取到mapper对应的代理类以后执行newInstance方法,获取到对应的实例,这样我们就可以通过这个实例进行方法的调用 ![](//img1.jcloudcs.com/developer.jdcloud.com/432fa162-7452-40cd-9bf5-1e5f901d74b420220316144642.png) 获取mapper的流程为 ![](//img1.jcloudcs.com/developer.jdcloud.com/1aae006f-51f1-435e-9628-9bffcce6e06620220316144704.png) ###### 4.查询过程 获取到mapper以后,就可以调用具体的方法 ```java //执行方法 List<Student> students = mapper.queryStudents(condition); ``` 首先会调用org.apache.ibatis.binding.MapperProxy#invoke的方法,在这个方法中,会调用org.apache.ibatis.binding.MapperMethod#execute ![](//img1.jcloudcs.com/developer.jdcloud.com/4f1d9f76-7150-415b-9068-1bc745e2f07e20220316144746.png) 首先根据SQL的类型增删改查决定执行哪个方法,我们执行的是SELECT方法,在SELECT中根据方法的返回值类型决定执行哪个方法,可以看到在select中没有selectone单独方法,都是通过selectList方法,通过调用org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object)方法来获取到数据 ![](//img1.jcloudcs.com/developer.jdcloud.com/c4405f17-5850-4053-a210-5c10bcf23da120220316144804.png) 在selectList中,首先从configuration对象中获取MappedStatement,在statement中包含了Mapper的相关信息,然后调用org.apache.ibatis.executor.CachingExecutor#query()方法 ![](//img1.jcloudcs.com/developer.jdcloud.com/3dd628bb-b87d-43de-a902-25f62918bce820220316144819.png) 在这个方法中,首先对SQL进行解析根据入参和原始SQL,对SQL进行拼接 ![](//img1.jcloudcs.com/developer.jdcloud.com/9f3247de-1b2d-4a0e-98cf-897c19ef387020220316144839.png) 调用MapperedStatement里的getBoundSql最终解析出来的SQL为 ![](//img1.jcloudcs.com/developer.jdcloud.com/f27bad33-5410-4a9f-acc9-5f61d415d15d20220316144856.png) 接下来调用org.apache.ibatis.parsing.GenericTokenParser#parse对解析出来的SQL进行解析 ![](//img1.jcloudcs.com/developer.jdcloud.com/7ba872d3-5338-44d9-a43a-ac23bbdb3c2120220316144920.png) 最终解析的结果为 ![](//img1.jcloudcs.com/developer.jdcloud.com/cab3b020-dfa6-4118-af0c-cc598b1d2d8a20220316144938.png) 最后会调用SimpleExecutor中的doQuery方法,在这个方法中,会获取StatementHandler,然后调用org.apache.ibatis.executor.statement.PreparedStatementHandler#parameterize这个方法进行参数和SQL的处理,最后调用statement的execute方法获取到结果集,然后 利用resultHandler对结进行处理 ![](//img1.jcloudcs.com/developer.jdcloud.com/e09b1767-f627-4377-8a55-eb6ab61293f420220316144955.png) 查询的主要流程为 ![](//img1.jcloudcs.com/developer.jdcloud.com/1345fe1d-7dbf-4b84-8249-aad0f1ad4b0820220316145013.png) ###### 5.查询流程总结 总结一下整个查询流程如下 ![](//img1.jcloudcs.com/developer.jdcloud.com/f1f690d1-5214-4617-8894-1462b49f839820220316145047.png) #### 2.3 场景问题原因及解决方案 ##### 2.3.1个人排查 这个问bug出现的地方在于绑定SQL参数的时候再源码中位置为 ![](//img1.jcloudcs.com/developer.jdcloud.com/5697e399-d966-4d81-ab6c-f4c98f683ac620220316145117.png) 由于我们所写的SQL是一个动态绑定参数的SQL,因此最终会走到org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql这个方法中去 ![](//img1.jcloudcs.com/developer.jdcloud.com/0eed2ef6-48f1-4c5f-a0c1-22758f9343f220220316145128.png) 在这个方法中,会调用 rootSqlNode.apply(context)方法,由于这个标签是一个foreach标签,因此这个apply方法会调用到org.apache.ibatis.scripting.xmltags.ForEachSqlNode#apply这个方法中去 ![](//img1.jcloudcs.com/developer.jdcloud.com/9fe89281-38c2-45cd-824f-fea9ecf6327920220316145146.png) 当调用appItm方法的时候将参数进行绑定,参数的变量问题都会存在bindings这个参数中区 ![](//img1.jcloudcs.com/developer.jdcloud.com/2a530722-0d52-40b0-a35b-ae437844539420220316145203.png) 进行绑定参数的时候,绑定完成foreach的方法的时候,我们可以看到bindings中不止绑定了foreach中的两个参数还额外有一个参数名字studentName->lct2,也就是说最后一个参数也是会出现在bindings这个参数中的, ![](//img1.jcloudcs.com/developer.jdcloud.com/be207d1e-8399-486d-aec2-80ae423f390520220316145219.png) ![](//img1.jcloudcs.com/developer.jdcloud.com/c37724e0-7aa7-4e54-8e3f-60494cbd135820220316145233.png) 最后判定 org.apache.ibatis.scripting.xmltags.IfSqlNode#apply ![](//img1.jcloudcs.com/developer.jdcloud.com/fe9cbf59-378b-4ff8-8bb2-86a346f2306e20220316145250.png) 我们可以看到在调用evaluateBoolean方法的时候会把context.getBindings()就是我们前边提到的bindings参数传入进去,因为现在这个参数中有一个studentName,因此在使用Ognl表达式的时候,判定为这个if标签是有值的因此将这个标签进行了解析 ![](//img1.jcloudcs.com/developer.jdcloud.com/44b7dea9-b28b-4852-9255-4e8cfb51b76420220316145307.png) 最终绑定的结果为 ![](//img1.jcloudcs.com/developer.jdcloud.com/7b88f313-0112-46b3-a313-1d9c1b83904120220316145335.png) 因此这个地方绑定参数的地方是有问题的,至此找出了问题的所在 ##### 2.3.2 官方解释 翻阅Mybatis官方文档进行求证,发现在3.4.5版本发行中bug fixes中有这样一句 ![](//img1.jcloudcs.com/developer.jdcloud.com/f714a39c-aebc-4d01-ae17-4c57f1ca7ffe20220316145359.png) 修复了foreach版本中对于全局变量context的修改的bug issue地址为https://github.com/mybatis/mybatis-3/pull/966 修复方案为https://github.com/mybatis/mybatis-3/pull/966/commits/84513f915a9dcb97fc1d602e0c06e11a1eef4d6a 可以看到官方给出的修改方案,重新定义了一个对象,分别存储全局变量和局部变量,这样就会解决foreach会改变全局变量的问题 ![](//img1.jcloudcs.com/developer.jdcloud.com/05d7fe0b-4560-4571-9aeb-3ede968026c020220316145416.png) ##### 2.3.3 修复方案 升级mybatis版本至3.4.5以上 如果保持版本不变的话,在foreach中定义的变量名不要和外部的一致 ### 3 源码阅读过程总结 Mybatis源代码的目录是比较清晰的,基本上每个相同功能的模块都在一起,但是如果直接去阅读源码的话,可能还是有一定的难度,没法理解它的运行过程,本次通过一个简单的查询流程从头到尾跟下来,可以看到Mybatis的设计以及处理流程,例如其中用到的设计模式: ![](//img1.jcloudcs.com/developer.jdcloud.com/717ec97c-3692-44c3-a9c8-5dfff3e333d020220316145439.png) - 组合模式:如ChooseSqlNode,IfSqlNode等 - 模板方法模式:例如BaseExecutor和SimpleExecutor,还有BaseTypeHandler和所有的子类例如IntegerTypeHandler; - Builder模式:例如 SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder; - 工厂模式:例如SqlSessionFactory、ObjectFactory、MapperProxyFactory; - 代理模式:Mybatis实现的核心,比如MapperProxy、ConnectionLogger; ##### 参考文档 https://mybatis.org/mybatis-3/zh/index.html ------------ ###### 自猿其说Tech-京东物流技术与数据智能部 ###### 作者:李春廷
原创文章,需联系作者,授权转载
上一篇:记录一次数据库CPU被打满的排查过程
下一篇:故障恢复及定位
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号