在过去程序员使用 JDBC 连接数据库,总会带来诸多不便。MyBatis 是一款优秀的持久层框架,可以替代 JDBC 帮助我们更好的进行开发。要了解 MyBatis 的实现原理,首先我们要明白 MyBatis 的大致操作步骤。



数据库源告诉我们连接哪个数据库,获得要执行的SQL语句,再进行操作,这点者缺一不可。接下来要看的就是这三点在底层如何实现。

MyBatis 如何获取数据库源?

使用 Mybatis 第一步肯定是要写好配置文件。官方给出的指导文档告诉我们,XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源。

<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<!-- 我们要获取的数据库源信息在这里 -->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
</configuration>

想要连接数据库,必然要获得数据源信息。既然上述配置文件有数据库源信息,那我们只要进行解析就好了。

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

由代码可见,我们要的是一个 SqlSessionFactory 实例,SqlSessionFactory 里面就有我们所需的数据库源信息。通过new SqlSessionFactoryBuilder().build(inputStream) 返回SqlSessionFactory 实例,进入 build 方法。



build 方法返回值就是是 SqlSessionFactory,注意到有 return build(parser.parse()),关注点在 parser.parse(),进入 parse 方法。



parse 方法返回 Configuration 。parsed 是一个布尔类型成员变量,默认值是 false,作判断的目的是为了防止多线程情况下该方法被二次调用。这个方法返回一个 Configuration 类型的实例,Configuration 是 BaseBuilder 类的一个成员变量,Configuration 其实保存了配置文件所有的信息,只是现在还是一张白纸,需要再操作一番。进入 parseConfiguration 方法。



上一张图的 parse.evalNode 方法将配置文件中 configuration 标签下的内容进行解析,封装到一个对象,这个对象作为参数传入 parseConfiguration 方法中。在 parseConfiguration 方法我们见到了很多熟悉的字样,诸如 properties、typeAliases 之类的配置信息,但我们的目的是要拿到数据库源信息,因此我们把目标放在包裹了数据库源信息的 environments 标签上,进入 environmentsElement 方法。



作为参数的 context 对象与之前一样,封装了 environments 标签中的内容,我们还需要进一步解析 dataSource 标签,关注 DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")) 这段代码,进入dataSourceElement 方法。



到这里 context 对象只有 dataSource 里的内容了。发现 type 的值为 POOLED(默认值),props 保存最终的数据库配置信息。DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance() 这一段代码,进入 resolveClass 方法,最终再跳转 resolveAlias 方法中。



注意 value = (Class) typeAliases.get(key),typeAliases 实际上是一个 HashMap,将 POOLED 作为 key 得到了保存的对应的 Class 类型。回到 dataSourceElement 方法。



得到返回的 Class 返回值,利用反射 newInstance 创建对应的 DataSourceFactory 对象,set 方法保存 props ,回到 environmentsElement 方法。



继续执行后面的方法,最终数据库源信息封装到一个 Environment 类型的实例,这个实例又通过 set 方法保存到了 configuration 。configuration 已经处理就绪,被 parse 方法返回。回到之前的 build 方法,将 configuration 作为参数传入至另一个重载的 build 方法。



SqlSessionFactory 本身是一个接口,DefaultSqlSessionFactory 则是实现了 SqlSessionFactory 的实现类,保存好 configuration 之后返回,就得到了我们开头需要的 SqlSessionFactory 实例。

MyBatis 如何获取 sql 语句?

与获取数据库源类似,只要解析 Mapper 配置文件中的对应标签,就可以获得对应的 sql 语句。之前我们讲过,SqlSessionFactory 中的 configuration 属性保存数据库源信息,事实上这个 configuration 属性将整个配置文件的信息都给封装成一个类来保存了。解析的前半部分与之前一样,分歧点在之前提到的 parseConfiguration 方法,其中在 environmentsElement 方法下面还有一个 mapperElement 方法。



配置文件中 mappers 标签加载mapper文件的方式共有四种:resource、url、class、package。代码中的 if-else 语句块分别判断四种不同的加载方式,可见 package 的优先级最高。parent 是配置文件中 mappers 标签中的信息,通过外层的循环一个一个读取多个 Mapper 文件。这里使用的方式是 resource ,所以会执行光标所在行的代码块,进入 mapperParser.parse() 方法。



我们要的是 mapper 标签的内容,因此我们关注 configurationElement(parser.evalNode("/mapper")) 这一句,进入 configurationElement 方法。



context 就是我们解析整个 Mapper 文件 mapper 标签中的内容,既然现在得到了内容,那只需再找到对应的标签就能获得sql语句了。注意 buildStatementFromContext(context.evalNodes("select|insert|update|delete")),我们看到了熟悉的 select、insert、update、delete,这些标签里就有我们写 sql 语句。进入 buildStatementFromContext 方法。



list 保存了我们在 Mapper 文件中写的所有含有 sql 语句的标签元素,用一个循环遍历 list 的每一个元素,分别将每一个元素的信息保存到 statementParser 中。进入 parseStatementNode 方法。





这个方法代码内容很多,仅摘出节选,里面定义了很多局部变量,这些变量用来保存sql语句标签(例如)的参数信息(例如缓存 useCache)。再把所有参数传到 addMappedStatement 中。进入 addMappedStatement 方法。



MappedStatement statement = statementBuilder.build(),使用 build 方法得到 MappedStatement 实例,这个类封装了每一个含有sql语句标签中所有的信息,再是 configuration.addMappedStatement(statement),保存到 configuration 中。

MyBatis 如何执行 sql 语句?

既然有了 SqlSessionFactory,我们可以从中获得 SqlSession 的实例。开启 session 的语句是 SqlSession session = sessionFactory.openSession(),进入 openSession 方法。



最终会执行 openSessionFromDataSource 方法。在之前 environment 已经有了数据库源信息,调用 configuration.newExecutor 方法。



Executor 叫做执行器,Mybatis 一共有三种执行器,用一个枚举类 ExecutorType 保存,分别是 SIMPLE,REUSE,BATCH,默认就是 SIMPLE。if-else 语句判断对应的类型,创建不同的执行器。在代码末端处有个 if 判断语句,如果 cacheEnabled 为 true,则会创建缓存执行器,默认是为 true,即默认开启一级缓存。

回到 openSessionFromDataSource 方法,最终返回一个 DefaultSqlSession 实例。得到 session 我们就可以执行 sql 语句了。SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句,以 selectOne 方法为例,进入该方法后发现,最终会调用到 selectList 方法。



configuration.getMappedStatement(statement) 得到了我们之前保存的 MappedStatement 对象,再调用 executor.query 方法,调用 query 方法之前会执行 wrapCollection 方法,保存 sql 语句中用户传入的参数。进入 query 方法。



boundSql 里面就有我们要执行的 sql 语句,CacheKey 是用来开启缓存的。执行父类 BaseExecutor 中的 createCacheKey 方法,通过 id,offsetid,limited,sql 组成一个唯一的 key,调用下一个 query 方法。



Cache cache = ms.getCache() 是二级缓存,二级缓存为空,直接调用 query 方法。



list = resultHandler == null ? (List) localCache.getObject(key) : null 传入 key 值在本地查询,如果有返回证明 key 已经缓存到本地,直接从本地缓存获取结果。否则 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql),去数据库查询。



localCache.putObject(key, EXECUTION_PLACEHOLDER) 首先将 key 缓存至本地,下一次查询就能找到这个 key 了。进入 doQuery 方法。



stmt = prepareStatement(handler, ms.getStatementLog()),得到一个 Statement。进入 prepareStatement 方法。



我们看到了一个熟悉的 Connection 对象,这个就是原生 JDBC 的实例对象。回到 doQuery 方法,进入 handler.query(stmt, resultHandler) 方法。



statement 强转型为 PreparedStatement 类型,这下我们又得到了 PreparedStatement 的类型实例了,调用 execute 方法,这个方法也是属于原生 JDBC。执行完成后 return resultSetHandler.handleCursorResultSets(ps),进入 handleCursorResultSets 方法。



ResultSetWrapper rsw = getFirstResultSet(stmt),看到 getFirstResultSet 方法中的 ResultSet rs = stmt.getResultSet(),在这里我们得到了 ResultSet 实例对象,最终 return rs != null ? new ResultSetWrapper(rs, configuration) : null,返回最终结果集。

MyBatis 如何实现不同类型数据之间的转换?

进入上一张图中 ResultSetWrapper 中可以看到,其中包含三个成员变量 columnNames、classNames、jdbcTypes,三者都是 ArrayList 集合。看一下构造方法。



final ResultSetMetaData metaData = rs.getMetaData(),metaData 就是数据库相关的数据,getColumnCount 统计有多少个字段,循环加入到 columnNames、jdbcTypes、classNames。columnNames 保存的就是实体类中的属性名,jdbcTypes 保存的是字段在数据库中的数据类型,classNames 保存的是字段在 Java 中的数据类型,比如 Java 的 String 与数据库 VARCHAR,MyBatis 充当一个中介完成转换,真正实现 ORM 的核心思想。

学习 MyBatis 的一点小总结 —— 底层源码初步分析的更多相关文章

  1. Java线程池及其底层源码实现分析

    1.相关类 Executors  ExecutorService   Callable   ThreadPool     Future 2.相关接口 Executor Executor接口的使用: p ...

  2. vue-style-loader源码初步分析

    背景: 首先声明一下,我只是个菜鸡,为了解决问题才去看的源码,解决完问题之后也就没有兴趣看其他部分代码了,所以这篇文章是一次很低层次的解读,角度也相当片面,想必会有很多喷点吧. 事情的经过是这样,今年 ...

  3. 从底层源码浅析Mybatis的SqlSessionFactory初始化过程

    目录 搭建源码环境 POM依赖 测试SQL Mybatis全局配置文件 UserMapper接口 UserMapper配置 User实体 Main方法 快速进入Debug跟踪 源码分析准备 源码分析 ...

  4. 【小盘子看源码-MyBatis-1】MyBatis配置文件的加载流程

    众所周知,Mybatis有一个全局的配置,在程序启动时会加载XML配置文件,将配置信息映射到org.apache.ibatis.session.Configuration类中,例如如下配置文件. &l ...

  5. Android开发之漫漫长途 Ⅵ——图解Android事件分发机制(深入底层源码)

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...

  6. 全方位深度剖析PHP7底层源码(已完结)

    第1章 课程介绍本章主要介绍课程要讲的知识点,以及课程要求等. 第2章 PHP7的新特性本章主要介绍PHP7的新特性,做基准测试,与PHP5对比验证PHP7的性能提升程度,引出对PHP7源码学习的必要 ...

  7. BAT资深工程师 由浅入深分析 Tp5&Tp6底层源码 - 分享

    BAT资深工程师由浅入深分析Tp5&Tp6底层源码 第1章 课程简介 本章主要让大家知道本套课程的主线, 导学内容,如何学习源码等,看完本章要让小伙伴觉得这个是必须要掌握的,并且对加薪有很大的 ...

  8. BAT资深工程师由浅入深分析Tp5&Tp6底层源码☆

    第1章 课程简介 本章主要让大家知道本套课程的主线, 导学内容,如何学习源码等,看完本章要让小伙伴觉得这个是必须要掌握的,并且对加薪有很大的帮助. 第2章 [TP5灵魂]自动加载Loader 深度分析 ...

  9. Java并发包源码学习系列:JDK1.8的ConcurrentHashMap源码解析

    目录 为什么要使用ConcurrentHashMap? ConcurrentHashMap的结构特点 Java8之前 Java8之后 基本常量 重要成员变量 构造方法 tableSizeFor put ...

随机推荐

  1. Day06 - Fetch、filter、正则表达式实现快速古诗匹配

    Day06 - Fetch.filter.正则表达式实现快速古诗匹配 作者:©liyuechun 简介:JavaScript30 是 Wes Bos 推出的一个 30 天挑战.项目免费提供了 30 个 ...

  2. vuex源码阅读分析

    这几天忙啊,有绝地求生要上分,英雄联盟新赛季需要上分,就懒着什么也没写,很惭愧.这个vuex,vue-router,vue的源码我半个月前就看的差不多了,但是懒,哈哈.下面是vuex的源码分析在分析源 ...

  3. 关于Spring注解@Component、@Repository、@Service、@Controller @Resource、@Autowired、@Qualifier 解析

    1.Spring 2.5 中除了提供 @Component 注释外,还定义了几个拥有特殊语义的注释,它们分别是:@Repository.@Service和 @Controller 其实这三个跟@Com ...

  4. python笔记28(TCP,UDP,socket协议)

    今日内容 1.TCP协议 协议的特点:三次握手,四次挥手: 2.UDP协议 3.OSI七层模型:每层的物理设备,每一层协议. 4.代码部分: ①介绍socket: ②使用socket完成tcp协议的w ...

  5. Nginx 推流 拉流 --- 点播直播

    1. 准备环境 安装操作系统Cenos 配置yum源 yum:https://developer.aliyun.com/mirror/ Nginx依赖 gcc-c++ zlib pcre openss ...

  6. 【11】openlayers 地图交互

    地图交互interaction 关于map的方法: //添加地图交互 map.addInteraction(interaction) //删除地图交互 map.removeInteraction(in ...

  7. ggplot2(5) 工具箱

    5.1 简介 ggplot2的图层化架构鼓励我们以一种结构化的方式来设计和构建图形.本章旨在概述可用的几何对象和统计变换,在文中逐个详述.每一节都解决一个特定的作图问题. 5.2 图层叠加的总体策略 ...

  8. C++ 结构体sturct练习

    #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> struct Student { ];// 姓名 int id; //id int a ...

  9. drf认证组件(介绍)、权限组件(介绍)、jwt认证、签发、jwt框架使用

    目录 一.注册接口 urls.py views.py serializers.py 二.登录接口 三.用户中心接口(权限校验) urls.py views.py serializers.py 四.图书 ...

  10. 【JAVA进阶架构师指南】之二:JVM篇

    前言   谈到JAVA,就不得不提JVM---JAVA程序员绕不开的话题.也许有童鞋会说,我不懂JVM,但是我一样可以写出JAVA代码,我相信说这种话的童鞋,往往是只有1-3年的初级开发人员,对JAV ...