系列文章目录
【源码系列】MyBatis与Spring整合原理源码
文章目录
- 系列文章目录
- 前言
- 一、简介
-
- 原生Jdbc查询代码
- Mybatis查询代码
- 二、MyBatis中重要组件
- 三、Mybatis原理源码
-
- 1.准备工作
-
- 1.创建Configuration对象
-
- 1.1 Configuration对象源码总结
- 1.2 Configuration对象源码分析
- 2.获取SqlSessionFactory对象
- 3.获取SqlSession对象
- 4.获取Mapper接口代理对象
- 4.准备阶段总结
- 2. 执行查询过程中源码
-
- 1.插件调用相关代码
- 2.缓存相关代码
- 3.原生Jdbc查询代码
- 4. 结果集的处理
- 5.执行查询过程中总结
- 四、总结
前言
Mybatis作为使用最为广泛的ORM框架,懂得如何使用是必备技能,为了我们使用的可以更加得心应手,我们有必要知道它是如何工作的,为什么我们只需要指定一个接口,它就能将数据查询回来?以及它是怎么一步步将数据查询回来的。
一、简介
我们先看下原生的Jdbc代码,相信每个同学之前都写过这段代码吧,相信大家用了MyBatis框架以后,都没再写过这些代码了吧。我们只需要通过调用Mapper接口的方法并且在mapper.xml中编写对应的sql语句来完成Jdbl的工作, 框架只是让我们使用起来更加简单,万变不离其中,核心还是这几句代码,只不过MyBatis在Jdbc的基础上进行了一些封装,让我们无需关心Connect对象的创建、Statement对象的创建、参数的设置、结果集处理等等。Mybatis将这些步骤都封装成了一个个组件去完成对应的工作。
原生Jdbc查询代码
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.jdbc.Driver");
// 获取数据库连接对象
Connection conn= DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test","root","123456");
String sql="select * from emp where empno = ?";
// 创建Statement对象
PreparedStatement stm = conn.prepareStatement(sql);
// 参数设置
stm.setInt(1, 7369);
// 执行查询,结果返回到ResultSet
ResultSet rs = stm.executeQuery();
// 处理结果集
while(rs.next()){
System.out.println(rs.getInt("empno"));
System.out.println(rs.getString("ename"));
}
conn.close();
}
Mybatis查询代码
public static void main(String[] args) throws Exception {
// 配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 生成SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 打开回话
SqlSession sqlSession = sqlSessionFactory.openSession();
// 获取代理类
EmpDao mapper = sqlSession.getMapper(EmpDao.class);
// 通过代理类调用查询方法
Emp empByEmpno = mapper.findEmpByEmpno(7369);
// 关闭会话
sqlSession.close();
}
二、MyBatis中重要组件
这是我之前画的mybatis源码图中关于组件及其作用的一张图,先让这些组件和大家见个面,避免后面源码分析的时候,大家一脸懵逼。
三、Mybatis原理源码
1.准备工作
1.创建Configuration对象
Configuration对象作为全局的配置对象,还是挺重要的,通过上面的组件图我们可以知道SqlSessionFactory、SqlSession都包含了该对象,可以说该整个查询过程中都可能会用到该对象。
我列举一下该类中比较重要的一些属性。
1.1 Configuration对象源码总结
这一块代码量也比较多,代码其实挺简单,就是看起来比较繁琐,我先总结一下,不想深入的同学可以跳过这里的源码。
- 解析给定的配置文件,加载配置文件中的配置属性设置到Configuration中
- 解析环境对象标签,设置到environment属性中,Environment对象包含了包含了TransactionFactory、DataSource
- 解析日志相关标签并设置logImpl实现类
- Configuration对象如果不指定执行器,默认为简单的执行器SimpleExecutor
- 解析配置的mappers包,并将每个Mapper接口生成一个MapperProxyFactory并放到mapperRegistry 属性中,方便下次用时获取。
- 第五步的同时,它会去解析每个mapper.xml文件的各个增删改查标签,每个语句都会构建成一个MappedStatement对象,并缓存到mappedStatements集合中。
这里涉及到了Executor、MapperProxyFactory、MappedStatement等组件,大家可以去组件图中再看下他们的作用,多熟悉熟悉。
1.2 Configuration对象源码分析
public class Configuration {
//环境对象,包含了TransactionFactory、DataSource
protected Environment environment;
// 日志对象,我们平常配置的日志类实际就是配置这个属性
protected Class<? extends Log> logImpl;
//默认为简单执行器
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
// 所有的Mapper接口生成的MapperProxyFactory
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
// 自定义插件,说一个大家都熟悉的 PageHelper分页插件
protected final InterceptorChain interceptorChain = new InterceptorChain();
// mapper.xml每个增删改查标签都会生成一个MappedStatement
protected final Map<String, MappedStatement> mappedStatements;
}
我们先看下Mybatis配置文件的内容
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置文件 -->
<properties resource="db.properties" ></properties>
<settings>
<!-- 日志实现类配置 -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
<!-- 下划线映射成驼峰 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<!-- 别名 -->
<typeAliases>
<package name="com.demo.bean"/>
</typeAliases>
<!-- 插件 -->
<plugins>
<plugin interceptor="com.demo.MyInterceptor"></plugin>
</plugins>
<!-- 环境对象 -->
<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>
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="SQL Server" value="sqlserver"/>
<property name="Oracle" value="oracle"/>
</databaseIdProvider>
<!-- mapper接口包路径 -->
<mappers>
<package name="com.demo.dao"/>
</mappers>
</configuration>
创建Configuration对象核心方法org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
我们从这里开始分析
解析mappers节点核心方法
通过解析标签中各个子属性,通过构建者模式,各个属性构建出一个MappedStatement对象
// 解析语句(select|insert|update|delete) 和里面各个属性标签,通过这些值去生成MappedStatement
public void parseStatementNode() {
// 获取SQL节点的id以及databaseId属性
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
// 省略部分代码
// 根据SQL节点的名称决定其SqlCommandType
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//是否要缓存select结果
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
//仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组了,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。
//这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// 在解析SQL语句之前,先处理其中的include节点
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
//参数类型
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
//脚本语言,mybatis3.2的新功能
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
//解析之前先解析<selectKey>
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 新增操作返回主键生成器
KeyGenerator keyGenerator;
// 获取selectKey节点对应的SelectKeyGenerator的id
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
// SQL节点下存在SelectKey节点
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
// 根据SQL节点的useGeneratedKeys属性值、mybatis-config.xml中全局的useGeneratedKeys配置,以及是否为insert语句,决定使用的KeyGenerator接口实现
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
//解析成SqlSource,一般是DynamicSqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
//语句类型, STATEMENT|PREPARED|CALLABLE 的一种
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
// 获取每次批量返回的结果行数
Integer fetchSize = context.getIntAttribute("fetchSize");
// 获取超时时间
Integer timeout = context.getIntAttribute("timeout");
// 引用外部parameterMap
String parameterMap = context.getStringAttribute("parameterMap");
// 结果类型
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
// 引用外部的resultMap
String resultMap = context.getStringAttribute("resultMap");
// 结果集类型,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一种
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
//(仅对 insert 有用) 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值
String keyProperty = context.getStringAttribute("keyProperty");
//(仅对 insert 有用) 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
// 通过MapperBuilderAssistant创建MappedStatement对象,并添加到mappedStatements集合中保存
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
public MappedStatement addMappedStatement(String id,SqlSource sqlSource,StatementType statementType,SqlCommandType sqlCommandType,Integer fetchSize,Integer timeout,String parameterMap, Class<?> parameterType,String resultMap, Class<?> resultType, ResultSetType resultSetType,boolean flushCache,boolean useCache,boolean resultOrdered, KeyGenerator keyGenerator,String keyProperty,String keyColumn,String databaseId,LanguageDriver lang,String resultSets) {
// ...
// 为id加上namespace前缀
id = applyCurrentNamespace(id, false);
// 是否是select语句
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 建造者模式
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource).fetchSize(fetchSize).timeout(timeout).statementType(statementType).keyGenerator(keyGenerator).keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId) .lang(lang).resultOrdered(resultOrdered).resultSets(resultSets).resultMaps(getStatementResultMaps(resultMap, resultType, id)).resultSetType(resultSetType).flushCacheRequired(valueOrDefault(flushCache, !isSelect)).useCache(valueOrDefault(useCache, isSelect)).cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
// 添加到configuration的mappedStatements集合中
configuration.addMappedStatement(statement);
return statement;
}
到这里,创建Configuration基本完成了,我没有把所有代码都拉出来,我把我们平常用到的一些重要的东西代码拉了出来。
2.获取SqlSessionFactory对象
创建SqlSessionFactory过程就比较简单,就是将得到的Configuration对象设置给了SqlSessionFactory的属性中,方便后续用时,直接从这里拿。
这里涉及到了SqlSessionFactory组件,大家可以去组件图中再看下他们的作用,多熟悉熟悉。
3.获取SqlSession对象
// 获取数据库的会话,创建出数据库连接的会话对象(事务工厂,事务对象,执行器,如果有插件的话会进行插件的解析)
SqlSession sqlSession = sqlSessionFactory.openSession();
public SqlSession openSession() {
// 获取默认的执行器类型
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
通过Configuration配置的执行器类型、隔离级别、事务自动提交来实例化SqlSession对象
创建Executor方法,我们可以看到关于插件的代码,插件放在interceptorChain的集合中,有多个会对Executor进行多次动态代理,具体插件代码需要在执行之前调用,后面调用到时我会框出来。
插件最后会调到该方法中,这里我们就能清楚的看到Jdk动态代理的代码啦
这里涉及到了SqlSession、Transaction、Environment、Executor组件,大家可以去组件图中再看下他们的作用,多熟悉熟悉。
4.获取Mapper接口代理对象
EmpDao mapper = sqlSession.getMapper(EmpDao.class);
通过前面的代码,我们知道再准备Configuration对象的时候,会为EmpDao创建一个MapperProxyFactory对象并缓存起来。
我们知道MapperProxyFactory是用来生成MapperProxy,MapperProxy类实现了InvocationHandler接口,我们知道它的invoke方法是用来执行代理的逻辑,所以它的invoke方法就很重要了,算是和数据库打交道的入口。后续我们会跟这个invoke方法。
4.准备阶段总结
- 首先创建了Configuration对象,该对象包含了整个MyBatis运行过程中需要用到的配置
- 准备好Configuration对象以后,开始创建SqlSessionFactory并将Configuration对象作为参数传到SqlSessionFactory中
- 准备好SqlSessionFactory后,开始创建SqlSession对象,我们知道所有的增删改查的交互工作都是由SqlSession来完成的,它包含了Configuration对象和Executor对象,通过组件图我们知道交付工作都是交给Executor来完成的,SqlSession只是一个装饰而已。
- 这些都准备好以后,开始创建代理对象,代理对象的InvocationHandler为MapperProxy类,所以后续调用代理对象的方法肯定会到MapperProxy#invoke方法中。
- 到这里所有准备工作都做完了
2. 执行查询过程中源码
// 具体查询代码
Emp empByEmpno = mapper.findEmpByEmpno(7369);
通过上面的讲解,该方法调用肯定会调到MapperProxy#invoke方法,我们直接进去,
通过源码我们知道进入invoke方法以后,会将方法包装成组件MapperMethod,然后再生成MapperInvoker对象。
组件MapperMethod它包含了sql类型、方法的入参、结果类型等,后面MapperMethod执行execute方法会根据sql类型来执行SqlSession对应的处理方法。
我们的语句是查询单个结果的操作,只要类型这种懂了别的类型也是一样的,就不分析了,毕竟各位同学也是很棒,相信能吃透。
这里我们遇到了组件MapperMethod,我们可以理解为用mapper的查询方法的一些属性信息组装成了MapperMethod对象,与之相对相应的mapper.xml中的增删改查标签被组装成了MappedStatement
下面我们要分析SqlSession#selectOne方法,虽然这里写着是selectOne,后续实际还是调用的selectList方法,只是最后对结果做了个判断,我们来看代码,一下就能理解了,还有我们经常遇到那个存在多个结果集的异常就是在这里抛出去的
1.插件调用相关代码
说到插件,有些同学不理解它是个啥,相信大家都用过PageHelper分页插件吧,它就是用户自定义的插件,只是别人给我们封装好了插件的逻辑,我们只需要使用就行,可能很多同学平常用的时候非常好奇,为啥它能实现我们的分页功能,实际上它拦截了所有的查询操作,执行查询语句之前,加上了limit 参数。
查询之前先通过方法名称获取了方法对应的MappedStatement,然后带着参数通过Executor去执行操作操作,如果有符合要求的插件,就会先执行插件的逻辑
插件调用会先进入下面这个方法
插件逻辑执行完成后通过反射调用Executor的query方法
2.缓存相关代码
查询前,会先生成一二级缓存的key,然后判断是否开启二级缓存,开了的话先从二级缓存中获取数据,没有再去一级缓存中取,还是没有再执行数据库的查询操作。
Mybatis缓存相关知识,我直接贴一篇博客,我觉得讲的挺好的:https://www.cnblogs.com/happyflyingpig/p/7739749.html
二级缓存代码
一级缓存代码
查询数据库代码
3.原生Jdbc查询代码
4. 结果集的处理
- 获取结果集ResultMap对象,根据ResultMap中定义的映射规则对ResultSet进行映射
- 根据ResultMap类型创建一个目标结果对象,通过目标对象创建MetaObject对象
- 拿到目标对象属性和数据库表中字段对应的映射集合
- 遍历映射集合,通过类型转换器将值转换成属性目标类型
- 通过MetaObject设置结果对象的该属性值
- 将结果对象添加到ResulHandler的结果集合中
5.执行查询过程中总结
- 调用mapper的查询方法,代理类会调到MapperProxy#invoke(这个就是动态代理传入的InvocationHandler对象)方法,这里是查询过程的入口
- 执行的方法会被包装成
MapperMethod
组件,它包含了Sql类型、方法的返回值,入参类型等信息,再将得到的MapperMethod包装成了MapperMethodInvoker - 根据MapperMethod中的Sql类型和返回值类型调用对应的SqlSession的增删改查方法(实际干活的是Exector)
- 调用查询方法之前,如果有插件的话,会先执行插件代码,插件实现原理也是通过JDK动态代理来实现的,多个插件就代理多次来实现
- 查询方法会先判断是否开启了二级缓存(默认关闭,作用范围SqlSessionFactory,通过装饰器模式来实现二级缓存的),先从二级缓存中取数据,没有再去一级缓存(作用范围:SqlSession)中取,还是没有的话就去数据库中查
- 查询数据库得到的结果,通过ResultHandler组件对结果集合目标结果对象进行值映射
- 将结果存入一级缓存中,开了二级缓存,也放入二级缓存
- 整个执行过程结束
四、总结
现在我们知道,Mybatis的核心是Jdk的动态代理和各个组件,整个运行可以分为两大步骤:
1. 准备阶段
1. 解析配置文件获取全局Configuration
对象,该过程中为每个Mapper接口创建MapperProxyFactory
对象,
2. 有了Configuration
对象以后通过构建者模式构建SqlSessionFactory
3. 通过SqlSessionFactory
创建SqlSession
对象,SqlSession
包含了Executor
(Sql的执行器),如果有插件,Executor
会织入插件逻辑(通过动态代理实现)
4. 通过SqlSession
对象加上动态代理,获取Mapper的代理对象(InvocationHandler是MapperProxy
),MapperProxy#invoke
作为执行查询时的入口
2. 查询阶段
- 执行的方法会被包装成
MappedMethod
(包含了方法的出入参和DB操作类型),再包装成MapperMethodInvoker
- 根据
MappedMethod
中的DB类型执行SqlSession对应的增删改查方法(实际调用时Executor
对象的方法) - 执行
Executor
织入的插件代码 - 开始查询的具体逻辑,有二级缓存先从二级缓存中取,没有从一级缓存中取,还是没有最终再查询数据库
- 得到查询结果后通过
ResulHanler
来完成结果集和目标对象的映射
终于写完了,写了两天,累死哥了。如果有写的不对的,请大佬指出,并给出意见,如果有说错了的我会修改的,最后附上我自己画的流程图,希望对大家对MyBatis源码理解有所帮助,其实MyBatis的源码还是比较好理解的,记住这些组件及功能,多看几遍它的运行流程,基本就能吃透Mybatis啦
Mybatis执行流程图:https://processon.com/view/61e6699507912906af03f2e6?fromnew=1