前言
本文分析mybatis源码,主要是梳理思路,对mybatis有一个全局的认知:
- 原生mybatis的核心组件;
- 原生mybatis的执行流程;
- mybatis的几个特性,涉及驱动层的部分以mysql为例;
- 与spring集成的原理;
版本:
- mybatis:3.5.6;
- mybatis-spring:2.0.6;
- mybatis-spring-boot-starter:2.1.4;
- mysql-connector-java:8.0.21;
一、案例
传统mybatis的使用方式,分三个步骤:
- 写业务代码(Mapper&Mapper.xml);
- 配置SqlSessionFactory;
- 使用SqlSessionFactory创建SqlSession执行sql;
1、业务Mapper
映射器Mapper,支持直接注解配置sql。
java
public interface MyAuthorMapper {
@Select("select * from author where id = #{id}")
Author selectAuthorByAnnotation(int id);
Author selectAuthorClassic(int id);
}
传统mybatis,支持在Mapper包路径下找到类名.xml,通过xml配置sql。

xml
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.apache.ibatis.my.MyAuthorMapper">
<select id="selectAuthorClassic" resultType="org.apache.ibatis.domain.blog.Author">
select * from author where id = #{id}
</select>
</mapper>
注:找xml的路径都可以通过多种方式调整,如xml配置SqlSessionFactory、集成spring配置mapperLocations,但是这都是细节,不是关键,不重要。
2、配置SqlSessionFactory
配置SqlSessionFactory有两种方式。
- 通过xml配置,xml如何编写参考官方文档,不细看;
ini
String resource = "org/apache/ibatis/builder/MapperConfig.xml";
Reader reader = Resources.getResourceAsReader(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
- 通过编码配置;
ini
// 创建数据源
DataSource dataSource = createDataSource();
// 创建事务工厂
TransactionFactory transactionFactory = new JdbcTransactionFactory();
// 数据源 + 事务工厂 -> Environment
Environment environment = new Environment("development", transactionFactory, dataSource);
// Environment + Mapper -> Configuration
Configuration configuration = new Configuration(environment);
configuration.addMapper(MyAuthorMapper.class);
// Configuration -> SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
3、执行sql
获取SqlSession执行sql有两种方式:
- 通过Mapper映射器执行;
- 通过语句id执行;
本质上都属于第二种。
ini
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
// 1. 通过Mapper执行
MyAuthorMapper mapper = sqlSession.getMapper(MyAuthorMapper.class);
System.out.println(mapper.selectAuthorByAnnotation(101));
System.out.println(mapper.selectAuthorClassic(101));
// 2. 使用语句id执行
Author author =
sqlSession.selectOne("org.apache.ibatis.my.MyAuthorMapper.selectAuthorClassic", 101);
}
二、配置阶段
1、组件概览

从最外层来看,mybatis包含:
- DataSource:数据源;
- TransactionFactory :事务工厂,mybatis提供了JdbcTransactionFactory由mybatis自己管理事务,如果和spring集成,需要让spring来管理事务;
- Environment:包含DataSource和TransactionFactory;
- Mapper:业务Mapper;
- Configuration:包含所有配置项,至少包含Environment和Mapper,还包含其他扩展,如Interceptor、TypeHandler等;
- SqlSessionFactory :启动阶段的产物,通过Configuration构造,往往是单例;
- SqlSession :运行阶段暴露给用户的api,通过SqlSessionFactory构造,需要注意的是,在未与spring集成的情况下,单纯mybatis的SqlSession实现是非线程安全的;
Configuration
Configuration包含mybatis的所有配置,在业务运行前需要装配完成。
Configuration包含各种kv配置。

除了普通kv配置,Configuration包含很多组件。

MapperRegistry
MapperRegistry管理所有Mapper类,所有注册的Mapper类都存放在knownMappers中。
每个Mapper类对应一个MapperProxyFactory ,用于运行时构造Mapper代理MapperProxy。

InterceptorChain
InterceptorChain管理所有Interceptor,可用于为StatementHandler、ParameterHandler、ResultSetHandler、Executor提供切面能力。

TypeHandlerRegistry
TypeHandlerRegistry管理所有TypeHandler类型转换器。

TypeHandler实现javaType和jdbcType的双向转换:
- 当处理PreparedStatement时,javaType需要转jdbcType;
- 当处理ResultSet时,jdbcType需要转javaType;

TypeHandlerRegistry在构造时会注入内置的n种TypeHandler。

TypeAliasRegistry
TypeAliasRegistry管理java类型的别名,构造时内置了许多别名。

这个别名可以用于很多地方,比如parameterType=list,会解析为java.util.List。

又比如二级缓存的实现,也可以通过TypeAliasRegistry来找到对应实现,类似于SPI、IOC的简单实现。

LanguageDriverRegistry
LanguageDriverRegistry用于管理不同LanguageDriver实现。

LanguageDriver有两个作用:
- createSqlSource ,启动阶段,根据sql脚本,创建SqlSource对象;
- createParameterHandler ,运行阶段,根据MappedStatement、入参、sql脚本,创建ParameterHandler对象;

LanguageDriver的默认实现是XMLLanguageDriver,基于xml脚本解析sql语句。
注:RawLanguageDriver可以忽略,其作用已经完全被XMLLanguageDriver实现,用于向后兼容。

2、加载Mapper
Mapper在mybatis中其实也不是必须的,比如通过xml配置Configuration。

通过sql.id直接调用查询Author。

一般情况下,我们都使用Mapper映射器来执行sql,所以按照这个api来分析。
ini
Configuration configuration = new Configuration(environment);
configuration.addMapper(MyAuthorMapper.class);
注:通过Mapper映射器来管理sql,也包含mapper.xml配置解析逻辑,都是一种形式,底层都一样。
MapperRegistry
MapperRegistry 管理所有Mapper类,所有注册的Mapper类都存放在knownMappers中。

MapperRegistry注册Mapper有多个重载方法,比如根据包路径、根据包路径+基类扫描Mapper类。

MapperRegistry#addMapper(Class):核心方法
- knownMappers:存储Mapper类与Mapper代理工厂MapperProxyFactory;
- parser.parse:解析Mapper类,得到sql配置(MappedStatement)注入Configuration;

MapperAnnotationBuilder#parse:对于Mapper映射器,sql配置来源可以有两处
- xml sql配置:主动找Mapper类全路径.xml作为sql配置,如com.x.y.ZMapper,找com/x/y/ZMapper.xml;
- 注解 sql配置:循环Mapper类的所有方法,解析注解sql配置,如Select注解;

解析sql配置包含众多属性(比如cache、resultMap等等),这里只分析主干逻辑(注解和xml都一样):
- 解析SqlSource;
- 组合众多属性和SqlSource,构造MappedStatement;
解析SqlSource
SqlSource
无论注解配置sql还是xml配置sql,最终每个sql脚本都会对应一个SqlSource。
XMLStatementBuilder#parseStatementNode:xml配置sql解析为SqlSource。

MapperAnnotationBuilder#buildSqlSource:注解配置sql解析为SqlSource。

解析SqlSource都交给Configuration中的LanguageDriver ,一般都使用XMLLanguageDriver。

SqlSource的作用是在运行时构造一个BoundSql。

SqlSource在配置阶段有两种实现(与注解还是xml配置无关):
- DynamicSqlSource:如果使用了${}占位符,或者使用了mybatis的xml标签(如foreach、if、where等),会解析为动态SqlSource;
- RawSqlSource:除了上面的情况,都属于RawSqlSource;
这两个SqlSource主要区别在于什么时候解析原始sql脚本。
RawSqlSource
RawSqlSource,在构造阶段解析sql脚本配置,后续反复使用。
比如sql=select * from author where id = #{id},这里会解析成select * from author where id = ?,即PreparedStatement支持的格式。

DynamicSqlSource
DynamicSqlSource,动态sql只会将原始配置sql缓存下来,需要在每次实际跑sql时解析sql脚本,创建BoundSql。

比如包含if的语句。

DynamicSqlSource只能缓存解析后的SqlNode,需要运行时判断PreparedStatement的占位符个数。

StaticSqlSource
SqlSourceBuilder#parse:
无论使用哪种SqlSource,最终跑sql时只会是一个StaticSqlSource。这个在后面再看。

构造MappedStatement
MappedStatement代表一个完整的sql配置。
每个MappedStatement包含众多属性,其中比较重要的是:
- id:在Configuration中的唯一标识;
- SqlSource:上面提到了,通过LanguageDriver解析sql脚本得到的结果,运行时通过SqlSource获取BoundSql;
- ParameterMap:入参映射;
- ResultMap:出参映射,一般只有一个,比如配置resultType或resultMap属性;

MapperBuilderAssistant#addMappedStatement:
最终所有sql配置组合各种特性后成为MappedStatement ,存放在Configuration中,至此sql解析完成。

Configuration:mappedStatements存放所有MappedStatement。

其中key是MappedStatement的唯一标识,即 {namespace}.{sql的id} ,如a.b.c.XMapper.selectById。
注:这个Map是mybatis的特殊实现,会同时包含namespace+id 和仅id (如上面的selectById)两种key,见Configuration.StrictMap,不过一般使用namespace+id的方式获取MappedStatement。
3、构造SqlSessionFactory
SqlSessionFactoryBuilder构造SqlSessionFactory有两种方式:
- 加载xml配置构造,实际和第二种一样,会解析为Configuration;
- 编码Configuration构造;

SqlSessionFactory 用于创建SqlSession。

DefaultSqlSessionFactory是SqlSessionFactory的一般实现,根据Configuration创建SqlSession。

三、创建SqlSession
1、SqlSession
在mybatis中SqlSession的实现只有DefaultSqlSession一种。
除了final成员变量,还包含两个状态:
- dirty:是否发生更新,且还未提交/回滚;
- cursorList:游标查询相关;

2、创建SqlSession的两种方式
DefaultSqlSessionFactory#openSessionFromDataSource:
使用Environment 中的DataSource,根据入参的事务隔离级别(level)+是否自动提交(autocommit)构造SqlSession。

DefaultSqlSessionFactory#openSessionFromConnection:
不使用DataSource,使用自己管理的数据库连接Connection,构建SqlSession,一般不会用这种。

SqlSessionFactory无参openSession方法:
- 使用DataSource;
- level=null,使用默认事务隔离级别;
- autocommit=false,非自动提交;
- 使用默认Executor(Simple);

总之,构造SqlSession需要几个组件:
- Configuration:包含众多组件;
- Transaction:事务;
- Executor:执行器;
3、Transaction
TransactionFactory事务工厂创建Transaction事务对象。

事务工厂一般有三种实现:
- JdbcTransactionFactory:mybatis管理事务;
- ManagedTransactionFactory:由外部容器管理事务,简单了解;
- SpringManagedTransactionFactory(mybatis-spring提供):由外部容器管理事务,这个容器特指spring,这个放在mybatis-spring部分来看;

JdbcTransactionFactory
JdbcTransactionFactory创建事务实现是JdbcTransaction。

getConnection:从DataSource取Connection;
commit:Connection#commit;
rollback:Connection#rollback;

ManagedTransactionFactory
ManagedTransactionFactory创建ManagedTransaction。

ManagedTransaction代表所有事务操作由外部容器管理。
mybatis仅仅使用Connection,commit和rollback都不在mybatis里处理。

4、Executor
Executor实际负责执行sql。

Configuration#newExecutor:每个SqlSession都对应一个Executor实例
- SimpleExecutor:默认Executor;
- BatchExecutor :批处理Executor,配合jdbc参数rewriteBatchedStatements=true使用;
- ReuseExecutor:预处理Statement复用Executor;
- CachingExecutor:二级缓存Executor,包装上述三种基础Executor,忽略;

四、执行sql
1、执行sql的两种方式
执行sql有两种方式:通过Mapper执行、使用MappedStatement的id执行。
ini
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
// 1. 通过Mapper执行
MyAuthorMapper mapper = sqlSession.getMapper(MyAuthorMapper.class);
System.out.println(mapper.selectAuthorByAnnotation(101));
System.out.println(mapper.selectAuthorClassic(101));
// 2. 使用语句id执行
Author author =
sqlSession.selectOne("org.apache.ibatis.my.MyAuthorMapper.selectAuthorClassic", 101);
}
2、Mapper执行sql
获取Mapper代理
DefaultSqlSession#getMapper:
执行sql的其中一种方式,是通过SqlSession#getMapper(用户Mapper接口)执行。

最终委派给MapperRegistry 中Mapper类对应的MapperProxyFactory创建。

MapperProxyFactory使用jdk动态代理,创建Mapper类的代理对象,代理逻辑都在MapperProxy对象(InvocationHandler)中。
注意,每次通过getMapper获取Mapper代理,都会创建一次MapperProxy代理对象,因为SqlSession非线程安全,没办法直接缓存Mapper代理。

执行Mapper方法
MapperProxy#invoke:执行Mapper方法。
虽然MapperProxy代理无法缓存,但是MapperProxyFactory.methodCache缓存了Mapper接口方法对应的目标执行逻辑MapperMethodInvoker。
一般MapperMethodInvoker的实现就是PlainMethodInvoker。

MapperMethod负责执行目标mapper方法。

MapperMethod有两个成员变量,MethodSignature会对Method提前做好解析,用于反射调用。

SqlCommand ,构造时从Configuration中定位对应MappedStatement ,如果定位不到抛出BindingException。

SqlCommand#resolveMappedStatement:这里将Mapper接口与Mapper.xml对应上。
MappedStatement的id=Mapper接口+方法名=xml配置namespace+sql的id。

MapperMethod#execute:执行sql,实际还是操作SqlSession的api。
比如insert,传入MappedStatement的id(command.getName)和入参param。

确定SqlSession入参
MapperMethod#convertArgsToSqlCommandParam方法,决定了传入SqlSession的入参。
这个入参将用于后续PreparedStatement占位符替换。

ParamNameResolver#getNamedParams:
- wrapToMapIfCollection:如果没有Param注解且参数列表大小为1,如果入参是Collection或Array,会被封装为一个Map(Map的key可以是array、list、collection,所以mapper.xml里可以用),否则不变,返回一个原始入参对象(如Author);
- 否则,根据Param注解或fieldName+自动生成的key,组装一个ParamMap返回;


案例1
less
@Select("select * from author where id = #{param1} and username = #{username}")
Author selectAuthorByIdAndName(Integer id, @Param("username") String name);
参数Map如下:
rust
"arg0" -> 101
"param1" -> 101
"username" -> "aaa"
"param2" -> "aaa"
- arg0:id没有设置Param注解,解析入参名得到,这里取决于编译时是否把入参名编译到class文件中。比如javac直接编译,这里只能拿到入参名=arg0,而javac -parameters编译,这里能拿到入参名=id,总的来说加Param注解更稳;
- param1、param2:mybatis按照入参顺序自动生成;
- username:Param注解解析得到;
案例2
swift
@Select("<script>select *\n" +
" from author\n" +
" where id in\n" +
" <foreach collection="collection" item="i" open="(" close=")" separator=",">\n" +
" #{i}\n" +
" </foreach></script>")
List<Author> selectByIds(List<Integer> ids);
参数Map如下:
rust
"arg0" -> [1,2]
"collection" -> [1,2]
"list" -> [1,2]
- arg0,逻辑同案例1;
- collection:入参是Collection类型,固定key=collection;
- list:入参是List类型,固定key=list;
3、SqlSession执行sql
上面看到,通过Mapper接口执行sql,本质上还是执行SqlSession的方法,一般需要传入MappedStatement的id和入参,入参往往是一个Map(MapperMethod.ParamMap)。
SqlSession#selectOne:
单个查询,复用批量查询。

SqlSession#selectList:
批量查询,根据MappedStatement的id找到MappedStatement,调用Executor#query执行。
注:Mapper定义的方法支持两个特殊类型入参,RowBounds-逻辑分页(用处不大忽略),ResultHandler-自定义结果处理器;

SqlSession#insert/update:
插入/更新,设置dirty标志为true,调用Executor#update执行。

SqlSession#commit/rollback:事务操作也交给Executor执行。

4、SimpleExecutor执行查询sql
获取BoundSql
BaseExecutor#query:首先调用MappedStatement#getBoundSql。

MappedStatement#getBoundSql:委派SqlSource执行。

DynamicSqlSource#getBoundSql:以动态sql为例(包含mybatis标签,如if/where等)
- 决定PreparedStatement的sql;
- SqlSourceBuilder#parse:构造StaticSqlSource;
- 像foreach这类标签,还会有mybatis生成的占位(*frch{item}*{index}),加入StaticSqlSource的扩展参数;
注:RawSqlSource静态sql,在启动阶段就完成了StaticSqlSource解析,运行时只需要执行StaticSqlSource#getBoundSql。

StaticSqlSource构造BoundSql会包含:
- configuration:全局配置;
- sql:最终调用PreparedStatement的sql;
- parameterMappings:参数映射关系;
- parameterObject:最初SqlSession传入的入参;
- metaParameters:扩展参数和对应值,比如foreach生成的__frch_{item}_{index}和对应值;


ParameterMapping包含每个配置sql中#{}字段配置的属性。
- property:占位属性;
- javaType:java类型;
- jdbcType:jdbc类型;
- TypeHandler:类型转换器;

比如下面property=id,jdbcType指定BIGINT。
bash
id = #{id,jdbcType=BIGINT}
对于RawSqlSource,解析ParameterMapping在启动阶段就完成了,而DynamicSqlSource需要每次跑sql都重新解析。
查询主流程
BaseExecutor#doQuery:具体查询逻辑不同Executor实现不同,以SimpleExecutor为例。

SimpleExecutor#doQuery:
- 构造StatementHandler,支持Interceptor扩展;
- 构造Statement;
- StatementHandler执行查询,构造返回结果;

SimpleExecutor#prepareStatement:构造Statement又分成三步
- 获取数据库连接;
- 使用StatementHandler创建Statement;
- 使用StatementHandler填充Statement参数;

创建StatementHandler
Configuration#newStatementHandler:StatementHandler实现是RoutingStatementHandler。

RoutingStatementHandler根据语句类型,最终会将逻辑委派给底层StatementHandler。

创建Connection
获取数据库连接都由Transaction处理。

这里使用mybatis自带的事务工厂创建的事务对象JdbcTransaction。
JdbcTransaction#getConnection:
如果当前SqlSession还未获取数据库连接,从DataSource获取Connection,
设置事务隔离级别,设置是否自动提交。
所以原生mybatis一般在SqlSession执行第一条sql时(当然如果你直接SqlSession#getConnection也会直接触发),才会打开数据库连接,如果有事务会开启事务。
注:这里在spring里的行为肯定会发生变化。

创建Statement
BaseStatementHandler#prepare:子类实现创建Statement。

PreparedStatementHandler#instantiateStatement:
对于PreparedStatement来说,就是Connection#prepareStataement传入BoundSql中的sql。

填充参数
PreparedStatementHandler#parameterize:交给ParameterHandler。

每个StatementHandler构造时都会创建一个ParameterHandler。

Configuration#newParameterHandler:
ParameterHandler由LanguageDriver创建 ,默认是DefaultParameterHandler,支持Interceptor。

DefaultParameterHandler#setParameters:
遍历BoundSql的所有ParameterMapping(#{}解析到的结构化对象,见上面获取BoundSql)。
针对属性找到对应的属性值,有四种情况:
- mybatis生成的扩展属性,如foreach属性__frch_{item}_{index},从扩展属性值中取;
- 入参为空,属性值为空;
- 入参类型有对应TypeHandler,将入参作为属性值;
- 兜底,通过入参反射获取属性值;
最终交给TypeHandler设置PreparedStatement的占位符参数,如果TypeHandler执行异常,比如数据转换异常,抛出TypeException。

TypeHandler在BoundSql构造时解析#{}占位符就确定了。
针对每个占位符,可以设置TypeHandler,比如显示指定id属性的TypeHandler是IntegerTypeHandler。
bash
where id = #{id,typeHandler=org.apache.ibatis.type.IntegerTypeHandler}
IntegerTypeHandler调用PreparedStatement#setInt设置占位符参数。

但是往往用户不会显示指定typeHandler,所以框架会推断TypeHandler。
ParameterMapping.Builder#resolveTypeHandler:
ParameterMapping会根据属性的java类型和jdbc类型推断TypeHandler,不深入分析。

执行
PreparedStatementHandler#query:
执行查询,调用PreparedStatemnet#exeucte。

读取ResultSet
主流程
读ResultSet由ResultSetHandler处理。

同样StatementHandler都会在构造时创建一个ResultSetHandler。

Configuration#newResultSetHandler:ResultSetHandler实现是DefaultResultSetHandler,支持Interceptor。

DefaultResultSetHandler#handleResultSets:将ResultSet封装为ResultSetWrapper。
注意,如果是存储过程,ResultSet是会存在多个的,这里只讨论普通PreparedStatement。

DefaultResultSetHandler#handleResultSet:默认使用DefaultResultHandler 解析ResultSet,传入MappedStatement中的ResultMap。

注:之前提到过,Mapper方法支持特殊入参ResultHandler,在这里resultHandler就非空,就能实现自定义ResultSet处理逻辑。比如:
arduino
Author selectById(int id, MyResultHandler handler);
DefaultResultSetHandler#handleRowValuesForSimpleResultMap:
ResultMap支持嵌套特性,这里不做深入分析,直接看简单ResultMap处理。
- 循环读取ResultSet;
- getRowValue:解析为java对象;
- storeObject:调用DefaultResultHandler#handleResult将解析后的java对象存储下来;

DefaultResultSetHandler#getRowValue:处理ResultSet的核心方法
- createResultObject,先构造对象实例,一般都是无参构造,当然ResultMap也支持constructor指定构造;
- hasTypeHandlerForResultObject,如果返回的是简单类型,如Integer、String,这里一定有TypeHandler,直接就可以返回rowValue了;
- MetaObject,封装反射调用逻辑,忽略;
- shouldApplyAutomaticMappings,是否能够自动映射,默认针对非嵌套情况都支持;
- applyAutomaticMappings ,针对ResultSet未被ResultMap column匹配的字段,尝试执行自动映射,设置属性值;
- applyPropertyMappings,使用ResultMap匹配成功的字段,设置属性值;
- isReturnInstanceForEmptyRow,这个配置比较有意思,如果一个对象填充任何一个属性值(自动映射或ResultMap),默认会返回null,比如selectList的集合中就会是n个null,这个比较坑;

自动映射
自动映射常用于设置了resultType,未设置resultMap的情况。
bash
<select id="xxx" resultType="org.apache.ibatis.domain.blog.Author">
select id, username from author where id = #{id}
</select>
DefaultResultSetHandler#applyAutomaticMappings:
构建自动映射,执行TypeHandler转换java对象,反射设置属性。

DefaultResultSetHandler#createAutomaticMappings:
- ResultSetWrapper#getUnmappedColumnNames:比对ResultSet中的数据库字段与ResultMap中的column,得到db中存在但ResultMap不存在的字段;
- MetaObject#findProperty:找column对应属性,注意mapUnderscoreToCamelCase的用处在这里体现,数据库字段下划线转驼峰,默认关闭;
- 每个自动映射成功的字段,封装为UnMappedColumnAutoMapping,包含对应TypeHandler;
- 未自动映射成功的情况,走AutoMappingUnknownColumnBehavior处理,默认NONE什么都不做;

ResultMap映射
DefaultResultSetHandler#applyPropertyMappings:
- 和自动映射相反,找到ResultMap的column与ResultSet中匹配的字段;
- 循环ResultMap中每个ResultMapping;
- 获取属性值;
- 反射设置属性值;

ResultMapping和入参ParameterMapping类似。

主要包含
- 属性:property;
- java类型:javaType;
- jdbc类型:jdbcType;
- TypeHandler:类型转换,这里需要将jdbcType转换为javaType;
- column:用于与ResultSet中字段匹配;

DefaultResultSetHandler#getPropertyMappingValue:
使用ResultMapping中的TypeHandler解析ResultSet中对应列名的属性;

IntegerTypeHandler#getNullableResult(ResultSet, String):
比如Integer类型转换,调用ResultSet#getInt获取属性值。

5、SimpleExecutor执行更新sql
BaseExecutor#update:清空一级缓存,不同Executor执行doUpdate逻辑。

SimpleExecutor#doUpdate:主流程和查询类似,区别在于BoundSql到此时还未构造。

StatementHandler构造时,会处理BoundSql为null的情况,主要是为了处理insert语句的selectKey逻辑(主键生成)。

SelectKeyGenerator#processGeneratedKeys:
如果使用selectKey+order=before,这里将执行selectKey查询(就是一个普通的查询,只不过走新Executor),反射设置主键。
注意,这里使用Transaction和外部SqlSession一致,即同一个连接,同一个事务。

比如下面这种sql:
sql
<insert id="insert" parameterType="org.apache.ibatis.domain.blog.Author">
<selectKey resultType="java.lang.Integer" keyProperty="id" order="BEFORE">
select max(id + 1) as id from author
</selectKey>
insert into author (id,username,password,email,bio)
values (#{id},#{username},#{password},#{email},#{bio})
</insert>
PreparedStatementHandler#update:
sql执行后,不需要执行ResultSet读取,同样要处理selectKey查询,当order=AFTER。

五、其他特性
1、Cursor
使用方式
Cursor游标查询普通使用方式如下:
kotlin
class AuthorMapper {
@Select("select * from author")
Cursor<Author> selectCursor();
}
AuthorMapper mapper = sqlSession.getMapper(AuthorMapper.class);
try(Cursor<Author> authors = mapper.selectCursor()) {
for (Author author : authors) {
System.out.println(author);
}
}
因为Cursor实现了Iterable,所以可以用增强for循环遍历,本质是执行内部的Iterator迭代器。
每次遍历一个元素,相当于从ResultSet解析一个对象。

默认情况下,这种使用游标查询的效果如下。
假设查询20条数据,数据库会将20条数据全量返回,ResultSet实际上已经缓存了20条数据,Cursor只是延迟解析java对象。

真正的游标查询需要数据库驱动配合,目的是防止查询结果集全部加载到jvm内存。
以mysql为例,在连接url上需要设置useCursorFetch=true。
bash
url=jdbc:mysql://localhost:3306/test?useCursorFetch=true
在mapper方法上需要配置fetchSize,比如配置为10。
less
@Select("select * from author")
@Options(fetchSize = 10)
Cursor<Author> selectCursor();
效果大致如下,ResultSet如果next拿不到数据,会再次去数据库拉,每次拉10条。
但是对于用户使用是无感知的,只需要遍历Cursor即可。

驱动层原理
mysql驱动共有三种ResultSet实现。
普通ResultSet
ResultsetRowsStatic最普通的ResultSet实现,直接读取数据库返回的所有记录缓存,运行时只需要按顺序遍历即可。

游标ResultSet
ResultsetRowsCursor是游标查询的ResultSet实现。

但是ResultsetRowsCursor在构造时并没有传入所有记录,而是与Server端的连接。

ResultsetRowsCursor#hasNext:每次遍历,如果超出了fetchSize,会调用server端再拉取更多数据。

流式ResultSet
ResultsetRowsStreaming 是流式查询的ResultSet实现。

流式查询的效果大致如下,一次查询db就会将所有数据返回给客户端。
在驱动层,ResultSet每次遍历会从InputStream读取一条数据。


要使用这种流式查询,只需要将fetchSize配置为Integer.MIN_VALUE。
Cursor原理
DefaultSqlSession#selectCursor:SqlSession层和普通查询类似,这里将调用Executor#queryCursor。

BaseExecutor#queryCursor:先构造BoundSql,执行子类doQueryCursor。

SimpleExecutor#doQueryCursor:区别在StatementHandler层。

PreparedStatementHandler#queryCursor:最终区别在ResultSet的处理上。

DefaultResultSetHandler#handleCursorResultSets:对于游标查询,ResultSet被缓存在返回的Cursor中。

DefaultCursor实现Iterable,实际迭代器Iterator是CursorIterator。

CursorIterator迭代器,每次next调用fetchNextUsingRowBound。

DefaultCursor#fetchNextObjectFromDatabase:
每次next 走DefaultResultSetHandler,解析ResultSet,只不过传入的ResultHandler是Cursor实现的内部类。

ObjectWrapperResultHandler 不同于默认实现DefaultResultHandler,除了存储解析后的java对象外,主要区别在于context.stop停止了ResultSet的遍历,从而达成迭代器懒加载的目的。

2、BatchExecutor
使用方式
开启jdbc参数rewriteBatchedStatements =true,openSession指定ExecutorType=Batch。
ini
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
MyAuthorMapper mapper = sqlSession.getMapper(MyAuthorMapper.class);
// 批量插入
for (int i = 1; i <= 10; i++) {
Author author = new Author(i, "username" + i);
mapper.insert(author);
}
// 批量更新
for (int i = 1; i <= 10; i++) {
Author author = new Author(i);
author.setUsername("xxx");
mapper.updateOne(author);
}
// 执行所有sql,并提交事务
sqlSession.commit();
}
驱动层原理
以MySQL驱动为例,使用传统jdbc方式,上面的逻辑等同于:
ini
// 获取db连接
Connection connection = dataSource.getConnection();
// 开启事务
connection.setAutoCommit(false);
// 1. 准备批量插入
PreparedStatement statement = connection.prepareStatement("insert into author (id,username) values (?,?)");
for (int i = 1; i <= 10; i++) {
statement.setInt(1, i);
statement.setString(2, "username" + i);
statement.addBatch();
}
// 2. 准备批量更新
PreparedStatement statement2 = connection.prepareStatement("update author set username = ? where id = ?");
for (int i = 1; i <= 10; i++) {
statement2.setString(1, "xxx");
statement2.setInt(2, i);
statement2.addBatch();
}
// 3. 执行批量插入
int[] i = statement.executeBatch();
// 4. 执行批量更新
int[] i1 = statement2.executeBatch();
// 5. 提交事务
connection.commit();
最核心的两个api是Statement#addBatch 和Statement#executeBatch。
打开profileSQL=true,最终在执行executeBatch时,驱动层将insert和update sql进行了拼接。
sql
// prepare插入sql
Fri Feb 09 10:22:50 CST 2024 INFO: [PREPARE] insert into author (id,username) values (?,?) [Created on: Fri Feb 09 10:22:50 CST 2024, duration: 3, connection-id: 15, statement-id: 0, resultset-id: -1, at org.apache.ibatis.my.MyTest.testJdbcBatch(MyTest.java:223)]
// prepare更新sql
Fri Feb 09 10:22:50 CST 2024 INFO: [PREPARE] update author set username = ? where id = ? [Created on: Fri Feb 09 10:22:50 CST 2024, duration: 1, connection-id: 15, statement-id: 0, resultset-id: -1, at org.apache.ibatis.my.MyTest.testJdbcBatch(MyTest.java:238)]
// 插入executeBatch
Fri Feb 09 10:22:50 CST 2024 INFO: [PREPARE] insert into author (id,username) values (?,?),(?,?),(?,?),(?,?),(?,?),(?,?),(?,?),(?,?),(?,?),(?,?) [Created on: Fri Feb 09 10:22:50 CST 2024, duration: 1, connection-id: 15, statement-id: 0, resultset-id: -1, at org.apache.ibatis.my.MyTest.testJdbcBatch(MyTest.java:245)]
Fri Feb 09 10:22:50 CST 2024 INFO: [EXECUTE] insert into author (id,username) values (1,username1),(2,username2),(3,username3),(4,username4),(5,username5),(6,username6),(7,username7),(8,username8),(9,username9),(10,username10) [Created on: Fri Feb 09 10:22:50 CST 2024, duration: 1, connection-id: 15, statement-id: 0, resultset-id: -1, at org.apache.ibatis.my.MyTest.testJdbcBatch(MyTest.java:245)]
Fri Feb 09 10:22:50 CST 2024 INFO: [FETCH] [Created on: Fri Feb 09 10:22:50 CST 2024, duration: 0, connection-id: 15, statement-id: 0, resultset-id: 0, at org.apache.ibatis.my.MyTest.testJdbcBatch(MyTest.java:245)]
// 更新executeBatch
Fri Feb 09 10:22:50 CST 2024 INFO: [QUERY] update author set username = 'xxx' where id = 1;update author set username = 'xxx' where id = 2;update author set username = 'xxx' where id = 3;update author set username = 'xxx' where id = 4;update author set username = 'xxx' where id = 5;update author set username = 'xxx' where id = 6;update author set username = 'xxx' where id = 7;update author set username = 'xxx' where id = 8;update author set username = 'xxx' where id = 9;update author set username = 'xxx' where id = 10 [Created on: Fri Feb 09 10:22:50 CST 2024, duration: 1, connection-id: 15, statement-id: 0, resultset-id: 0, at org.apache.ibatis.my.MyTest.testJdbcBatch(MyTest.java:247)]
Fri Feb 09 10:22:50 CST 2024 INFO: [FETCH] [Created on: Fri Feb 09 10:22:50 CST 2024, duration: 2, connection-id: 15, statement-id: 0, resultset-id: 0, at org.apache.ibatis.my.MyTest.testJdbcBatch(MyTest.java:247)]
以批量插入为例,实际执行executeBatch才是最关键的部分,分为两步。
第一步,
客户端,基于单个插入sql和addBatch得到的参数列表,解析得到批量插入sql的PreparedStatement,请求服务端。
服务端,返回一个Statement ID=3,用于后续提交参数列表。


第二步,
客户端,使用StatementId=3,将addBatch缓存的所有参数,发送给服务端。
服务端,返回执行成功。


所以,在纯批量场景下,第一个PreparedStatement并未起到决定性作用。
因为驱动层在executeBatch阶段,没有使用第一个PreparedStatement,比如下面这个Statement ID=1的sql。


回到代码层面。
com.mysql.cj.AbstractQuery#addBatch:addBatch将入参缓存。

com.mysql.cj.jdbc.ClientPreparedStatement#executeBatchedInserts:批量插入
- 请求server,创建批量插入Statement;
- 占位符填充;
- 请求server,传递占位符参数;

ClientPreparedStatement#executePreparedBatchAsMultiStatement:批量更新和批量插入类似。

BatchExecutor原理
BatchExecutor缓存了:
- 当前SqlSession中所有待执行的Statement;
- 当前正在处理的sql及其对应MappedStatement;

BatchExecutor#doUpdate:
每次执行update,判断currentSql与本次update的sql是否一致:
- 如果不一致,创建新的Statement加入statementList;
- 如果一致,复用缓存的上一个Statement;
注:批处理过程中,谨防sql切换导致批处理失效。

PreparedStatementHandler#batch:执行Statement#addBatch,将本次占位符参数缓存。

BatchExecutor#doFlushStatements:在多种场景下,会触发flush操作,循环缓存的所有Statement,真正执行Statement#executeBatch执行sql。

场景一:提交事务。
BaseExecutor#commit:

场景二:执行查询sql。
BatchExecutor#doQuery:

场景三:手动flush。
BaseExecutor#flushStatements():

3、ReuseExecutor
使用方式
openSession指定ExecutorType=REUSE。
ini
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE, false)) {
MyAuthorMapper mapper = sqlSession.getMapper(MyAuthorMapper.class);
for (int i = 1; i <= 3; i++) {
Author author = new Author(i, "username" + i);
mapper.insert(author);
}
sqlSession.commit();
}
驱动层原理
Statement复用,一个Statement使用不同参数执行多次update。
比如上面案例一个执行了3次insert,只需要4次请求,而如果使用普通Executor则需要6次。

ReuseExecutor原理
ReuseExecutor缓存了sql对应Statement。

ReuseExecutor#doUpdate:以update为例。

ReuseExecutor#prepareStatement:只要缓存中存在sql对应可用Statement,就复用Statement。
注:和batch缓存有区别,batch要求连续相同sql,而reuse则不需要。

ReuseExecutor#doFlushStatements:
如果发生事务提交回滚 /查询 /手动flush,缓存Statement将关闭清空。

4、ResultHandler
使用方式
假设批量查询需要返回一个Map,id对应Author。
Mapper映射器。
less
public interface MyAuthorMapper {
@Select("<script>select *\n" +
" from author\n" +
" where id in\n" +
" <foreach collection="ids" item="i" open="(" close=")" separator=",">\n" +
" #{i}\n" +
" </foreach></script>")
@ResultType(Author.class)
void selectMap(@Param("ids") List<Integer> ids, ResultHandler<Author> resultHandler);
}
自定义ResultHandler缓存ResultSet每行结果。
typescript
public class AuthorNameResultHandler implements ResultHandler<Author> {
private final Map<Integer, String> id2Name = new HashMap<>();
@Override
public void handleResult(ResultContext<? extends Author> resultContext) {
Author value = resultContext.getResultObject();
id2Name.put(value.getId(), value.getUsername());
}
public String getName(Integer id) {
return id2Name.get(id);
}
}
查询传入自定义ResultHandler。
ini
AuthorNameResultHandler handler = new AuthorNameResultHandler();
try (SqlSession session = sqlSessionFactory.openSession()) {
MyAuthorMapper mapper = session.getMapper(MyAuthorMapper.class);
mapper.selectMap(Arrays.asList(1,2,3), handler);
}
System.out.println(handler.getName(1));
自定义ResultHandler原理
MapperMethod#execute:
如果方法返回void,且参数列表包含ResultHandler,走SqlSession#select,传入ResultHandler。


DefaultResultSetHandler#handleResultSet:
在处理每一行ResultSet时,区分是否有自定义ResultHandler。

DefaultResultSetHandler#callResultHandler:
每行解析结果,会被设置到ResultContext中,调用ResultHandler#handleResult处理。

所以通过自定义ResultHandler在handleResult方法中可定义结果集如何收集。
默认DefaultResultHandler将每一行ResultSet解析结果缓存,最终返回一个list。

selectMap
mybatis提供了MapKey注解,可实现与案例中类似的作用。
less
@Select({ "SELECT * FROM author"})
@MapKey("id")
Map<Integer,Author> selectMap();
底层是SqlSession提供了通用selectMap方法,实现上述功能。

DefaultSqlSession#selectMap:在selectList后,使用特殊ResultHandler实现DefaultMapResultHandler,再次遍历结果集,组装为Map。
与自定义ResultHandler不同的是,框架提供的selectMap在ResultSet全部处理完成后,二次收集得到Map。

DefaultMapResultHandler用反射获取key,Map缓存结果集,实现更通用的selectMap能力。

5、Interceptor
使用方式
Interceptor用于扩展mybatis的切面逻辑。
Step1,实现Interceptor 拦截逻辑,使用Intercepts+Signature注解定义切点。
less
@Intercepts({
@Signature(type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
)
})
public class UpdateInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object parameter = invocation.getArgs()[1];
if (parameter instanceof XXX) {
XXX x = (XXX) parameter;
// 设置更新时间
x.setUpdateTime(LocalDateTime.now());
}
return invocation.proceed();
}
}
Step2,在Configuration中加入Interceptor。
即Interceptor在SqlSessionFactory范围内单例,需要保证线程安全。
scss
// 创建数据源
DataSource dataSource = createDataSource();
// 创建事务工厂
TransactionFactory transactionFactory = new JdbcTransactionFactory();
// 数据源 + 事务工厂 -> Environment
Environment environment = new Environment("development", transactionFactory, dataSource);
// Environment + Mapper -> Configuration
Configuration configuration = new Configuration(environment);
configuration.addMapper(MyAuthorMapper.class);
configuration.addInterceptor(new UpdateInterceptor()); // 配置Interceptor
// Configuration -> SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
原理
Configuration用InterceptorChain存储所有Interceptor。

InterceptorChain 在运行期间,在构造为四个组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler )时用Interceptor#plugin包装。

Interceptor#plugin:Interceptor默认实现。

Plugin#wrap:
解析Signature注解,得到切点。
用目标对象实现接口(如Executor)匹配切点class,得到需要代理的interfaces。
如果需要代理接口数量大于0,创建动态代理返回。

Plugin#invoke:代理逻辑,匹配切点method,封装Invocation,调用Interceptor。

Invocation提供proceed方法,执行目标对象方法。

六、mybatis-spring
1、案例
Step1,配置基础依赖。
- DataSource;
- PlatformTransactionManager:Spring事务管理器;
less
@Configuration
@EnableTransactionManagement
public class MybatisConfig {
@Bean
public DataSource dataSource() {
// ...
}
@Bean
public PlatformTransactionManager transactionalManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
Step2,配置SqlSessionFactory。
java
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) throws IOException {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("mapper/*.xml"));
return bean;
}
Step3,配置Mapper映射器,可选,其实通过SqlSessionFactory已经可以执行sql。
方式一:使用SqlSessionTemplate构造。
typescript
@Bean
public MyUserMapper userMapper(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory).getMapper(MyUserMapper.class);
}
方式二:使用MapperFactoryBean构造。
ini
@Bean
public MapperFactoryBean<MyUserMapper> userMapperWithFactory(SqlSessionFactory sqlSessionFactory) {
MapperFactoryBean<MyUserMapper> mapperFactoryBean = new MapperFactoryBean<>();
mapperFactoryBean.setMapperInterface(MyUserMapper.class);
mapperFactoryBean.setSqlSessionFactory(sqlSessionFactory);
return mapperFactoryBean;
}
方式三:使用MapperScan扫描。
less
@Configuration
@MapperScan("org.mybatis.my.mapper")
public class MybatisConfig {
}
2、SqlSessionFactoryBean
SqlSessionFactoryBean内部包含众多mybatis组件。
ini
public class SqlSessionFactoryBean
implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
private Resource configLocation;
private Configuration configuration;
private Resource[] mapperLocations;
private DataSource dataSource;
private TransactionFactory transactionFactory;
private Properties configurationProperties;
private SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
private SqlSessionFactory sqlSessionFactory;
private String environment = SqlSessionFactoryBean.class.getSimpleName();
private boolean failFast;
private Interceptor[] plugins;
private TypeHandler<?>[] typeHandlers;
private String typeHandlersPackage;
@SuppressWarnings("rawtypes")
private Class<? extends TypeHandler> defaultEnumTypeHandler;
private Class<?>[] typeAliases;
private String typeAliasesPackage;
// ...
}
SqlSessionFactoryBean是个FactoryBean 用于构造SqlSessionFactory。


如果未配置configLocation,默认使用无参构造默认Configuration。

Environment 使用用户指定DataSource,而事务工厂默认使用SpringManagedTransactionFactory,由mybatis-spring提供实现,由spring控制事务。

通过指定mapperLocations ,扫描Mapper.xml,注册为MappedStatement。
注:mapper.xml在传统mybatis里只能通过两种方式查找,1-Mapper接口完全限定类名.xml,2-configuration.xml配置mappers,两种都是单个查找。批量扫描是mybatis-spring提供的方式。

SqlSessionFactoryBuilder使用Configuration构造SqlSessionFactory。

SqlSessionFactoryBean没什么特别的,利用用户配置和mybatis核心api,构造SqlSessionFactory。
3、MapperFactoryBean
MapperFactoryBean用于构造Mapper映射器。

MapperFactoryBean注入SqlSessionFactory 实际会构造一个SqlSessionTemplate。

MapperFactoryBean#checkDaoConfig:在afterPropertiesSet阶段将Mapper加入 MapperRegistry。

MapperFactoryBean#getObject:FactoryBean最终利用底层SqlSessionTemplate创建Mapper代理。


使用MapperFactoryBean和直接使用SqlSessionTemplate创建Mapper的唯一区别在于,MapperFactoryBean会在初始化阶段,将Mapper加入MapperRegistry。
即,使用MapperFactoryBean构造的Mapper,可以不依赖主动扫描mapper.xml。
(MapperRegistry#addMapper可解析注解为MappedStatement,也支持根据完全限定类名找mapper.xml,见第二部分加载Mapper)
java
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) throws IOException {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// 不需要mapper.xml
// bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("mapper/*.xml"));
return bean;
}
@Bean
public MapperFactoryBean<MyUserMapper> userMapperWithFactory(SqlSessionFactory sqlSessionFactory) {
MapperFactoryBean<MyUserMapper> mapperFactoryBean = new MapperFactoryBean<>();
mapperFactoryBean.setMapperInterface(MyUserMapper.class);
mapperFactoryBean.setSqlSessionFactory(sqlSessionFactory);
return mapperFactoryBean;
}
4、MapperScan
MapperScan是配置Mapper映射器的批量方式,支持xml配置(mybatis:scan)也支持注解驱动(MapperScan注解),这里分析注解驱动。

MapperScan注解放在ConfigurationClass上,Import了MapperScannerRegistrar。

MapperScannerRegistrar实现ImportBeanDefinitionRegistrar,在ConfigurationClass解析时,通过registerBeanDefinitions钩子注册BeanDefinition。

MapperScannerRegistrar#registerBeanDefinitions:
利用MapperScan注解属性,注册MapperScannerConfigurer的BeanDefinition。

MapperScannerConfigurer#postProcessBeanDefinitionRegistry:
MapperScannerConfigurer实现BeanDefinitionRegistryPostProcessor注册Mapper的BeanDefinition。

ClassPathMapperScanner 继承spring提供的ClassPathBeanDefinitionScanner。

ClassPathMapperScanner在扫描注册BeanDefinition后,二次处理了BeanDefinition。

第一,
设置beanClass从用户Mapper替换为MapperFactoryBean ,所以实际注册的是MapperFactoryBean。
MapperFactoryBean构造方法传入用户Mapper的class。

第二,如果MapperScan未显示配置SqlSessionFactory(比如多数据源需要配置多个SqlSessionFactory),则MapperFactoryBean 会按照类型注入SqlSessionFactory。
至此,完成了用户手动构造MapperFactoryBean的逻辑。

5、SqlSessionTemplate
前面的分析都是描述如何将一个mybatis组件注入ioc容器。
其实和spring集成都大同小异,都是描述如何构造一个bean。
而SqlSessionTemplate是mybatis与spring集成的非常重要的一个组件。

SqlSessionTemplate有两个非常重要的作用:
- 实现SqlSession接口,特点是保证了线程安全;
- 事务管理交给spring;
线程安全
MapperFactoryBean#getObject:创建Mapper代理。
第一点非常重要的原因是,Mapper都由一个SqlSession创建。
这里如果还是传统mybatis的DefaultSqlSession是无法实现线程安全的,也就无法在ioc容器中使用单例Mapper。
所以MapperFactoryBean注入SqlSessionFactory 实际会构造一个SqlSessionTemplate。

SqlSessionTemplate在构造时会创建SqlSession的代理对象,用于后续实际执行SqlSession方法。


比如selectList都交给sqlSessionProxy执行。

SqlSessionInterceptor 代理逻辑,静态方法获取SqlSession,执行实际目标SqlSession的目标方法。

SqlSessionUtils#getSqlSession:实现SqlSession线程安全的核心方法。
- 从TransactionSynchronizationManager(spring-tx),获取SqlSessionFactory对应SqlSessionHolder;
- 从SqlSessionHolder,获取SqlSession;
- 如果SqlSession非空,返回;
- SqlSession为空,SqlSessionFactory开启新SqlSession,注意传入ExecutorType是SqlSessionTemplate的ExecutorType,如果没设置,取Configuration的默认ExecutorType,即Simple;
- 将新的SqlSession封装为SqlSessionHolder ,放入TransactionSynchronizationManager;

TransactionSynchronizationManager由spring-tx提供,用ThreadLocal存储当前线程使用SqlSessionFactory (Key)对应的SqlSessionHolder(Value)。
所以每个线程拥有独立的SqlSession,从而解决了线程安全问题。

spring事务
mybatis与spring事务集成,需要拿到spring开启事务的Connection,作为后续执行sql的Connection。

SqlSessionUtils#getSqlSession如果当前线程没有SqlSession,SqlSessionFactory#openSession创建SqlSession。

DefaultSqlSessionFactory#openSessionFromDataSource:
创建SqlSession需要事务工厂创建Transaction对象,mybatis.jar只提供了mybatis自己管理事务的JdbcTransactionFactory实现。

mybatis-spring提供了对于spring管理事务的实现。

SpringManagedTransaction #getConnection:当执行sql时,mybatis获取数据库连接,通过spring-jdbc 提供的DataSourceUtils#getConnection方法获取。

DataSourceUtils#doGetConnection:
本质上也是利用spring-tx提供的TransactionSynchronizationManager。
Step1,获取当前线程的DataSource对应Connection;
Step2,如果当前线程还未获取Connection,则创建新的Connection返回;

DataSourceTransactionManager#doBegin:
而spring事务管理器DataSourceTransactionManager 会在开启事务的时候,绑定当前线程的DataSource和Connection关系。所以,mybatis的数据源必须和spring事务管理器的数据源一致。

SpringManagedTransaction提交事务和回滚事务。
只有isConnectionTransactional=false,非spring管理事务的情况(比如事务管理器数据源与mybatis数据源不一致),才会主动执行事务操作,否则都交给Spring处理。

对于spring事务还有很多细节,不深入分析。
总得来说mybatis利用spring-tx提供的TransactionSynchronizationManager,根据当前线程获取对应Connection,解决了与spring事务集成的问题。
七、mybatis-spring-boot-starter
有了mybatis-spring后,mybatis-spring-boot-starter的工作不多。
无非是把描述bean构造的工作,交给了自动配置。
1、自动配置前提
MybatisAutoConfiguration是mybatis的自动配置类,只有一个DataSource的时候才能启用(或多数据源通过primary指定)。

2、mybatis相关组件
Mybatis的一些组件可以直接定义在Spring容器中,通过MybatisAutoConfiguration来组装,如:
- Interceptor:mybatis切面,支持StatementHandler、ParameterHandler、ResultSetHandler、Executor切面处理;
- TypeHandler:类型转换器,javaType和jdbcType转换;
- LanguageDriver:sql脚本驱动,一般使用xml写sql,支持thymeleaf、freemarker、velocity等模板引擎脚本;

3、SqlSessionFactory
使用mybatis-spring提供的SqlSessionFactoryBean构造SqlSessionFactory。
可以用到ioc容器里用户定义的mybatis扩展组件,如Interceptor等。

4、SqlSessionTemplate
在springboot中,SqlSessionTemplate被自动注入了,所以业务代码可以直接拿到线程安全的SqlSession。

5、AutoConfiguredMapperScannerRegistrar
如果使用传统mybatis-spring方式注册Mapper,不会导入这个Bean。
注:单独注册MapperFactoryBean、使用MapperScan批量注册MapperFactoryBean(MapperScannerConfigurer )

AutoConfiguredMapperScannerRegistrar 的作用是代替用户写MapperScan注解 ,本质上还是利用MapperScannerConfigurer,遵循约定大于配置:
- AutoConfigurationPackages.get(this.beanFactory):使用启动类包路径;
- Mapper接口需要加上Mapper注解;

总结
配置阶段,mybatis解析配置为Configuration ,构造SqlSessionFactory。
运行阶段,用户用SqlSessionFactory 创建SqlSession,执行sql,其中SqlSession非线程安全。

1、配置阶段
Configuration包含所有运行阶段需要的组件:
- Environment :环境,包含DataSource 数据源和TransactionFactory事务工厂;
- MapperRegistry :管理所有用户Mapper ,每个Mapper类对应一个MapperProxyFactory ,用于运行时构造Mapper代理MapperProxy;
- InterceptorChain :管理所有Interceptor,可用于为StatementHandler、ParameterHandler、ResultSetHandler、Executor提供切面能力;
- TypeHandlerRegistry :管理所有TypeHandler 类型转换器,TypeHandler实现javaType和jdbcType的双向转换:当处理PreparedStatement时,javaType转jdbcType;当处理ResultSet时,jdbcType转javaType;
- TypeAliasRegistry :管理java类型的别名,比如list字符串对应java.util.List,在部分地方可实现简单spi能力;
- LanguageDriverRegistry :管理LanguageDriver 实现,LanguageDriver用于解析sql脚本为SqlSource ,默认采用XMLLanguageDriver解析xml脚本,还支持其他模板引擎如thymeleaf、freemarker、velocity等;
- MappedStatement:重要,所有sql配置(mapper.xml或Mapper方法注解),最终会解析为MappedStatement存放在Configuration中;

MappedStatement代表一个完整的sql配置,主要包含:
- id:唯一id,namespace(一般是Mapper的完全限定类名)+sql.id(xml sql元素的id属性) ;
- sqlSource:通过LanguageDriver 解析sql脚本得到,如果包含${}占位符或包含mybatis特殊标签(如foreach、if)会解析为DynamicSqlSource ,需要在每次跑sql时确定真实SqlSource (StaticSqlSource );除此以外的场景,上图中的sql脚本会解析为RawSqlSource ,在启动阶段就能确定真实SqlSource;
- resultMaps:出参映射,一般只有一个,比如配置resultType或resultMap属性;
- parameterMap:入参映射;
无论使用xml配置还是编码配置,最终都通过Configuration创建SqlSessionFactory。
DefaultSqlSessionFactory默认实现,持有Configuration。
2、创建SqlSession
SqlSessionFactory 创建SqlSession有两种方式:
1-通过Environment的DataSource创建;2-通过Connection创建,一般使用前者。

- TransactionFactory创建Transaction,Transaction持有DataSource,Transaction中的Connection此时还未打开,需要执行sql时获取;
- SqlSessionFactory创建Executor,Executor持有Transaction和Configuration;
- SqlSessionFactory创建SqlSession,SqlSession持有Executor和Configuration;
SqlSession实现是DefaultSqlSession ,在原生mybatis下非线程安全。
3、执行sql
创建Mapper代理
执行sql有两种方式:1-通过Mapper映射器执行;2-SqlSession执行。
本质上Mapper映射器使用的也是SqlSession,以SqlSession#selectList为例,Mapper映射器需要解决几个问题:

- Mapper接口,需要创建代理,才能实际执行SqlSession方法;
- Mapper接口方法,需要找到MappedStatement(statement);
- Mapper接口方法入参,需要转换为SqlSession能接受的入参(parameter);
SqlSession#getMapper(Class):SqlSession创建Mapper代理逻辑

- 配置阶段,Configuration#addMapper,注册Mapper.class对应MapperProxyFactory;
- 构造Mapper代理 ,使用jdk动态代理创建MapperProxy。MapperProxy持有SqlSession、Method对应MapperMethodInvoker,MapperMethodInvoker可以在运行时执行SqlSession方法;
- 执行Mapper方法 ,首次执行Method创建MapperMethodInvoker,匹配MappedStatement为一个SqlCommand(Mapper完全限定类名+方法名=mapper.xml的namespace+sql.id) ,解析Method方法为MethodSignature用于后续反射调用(可以解析出SqlSession的入参parameter) ;
- 后续虽然Mapper动态代理需要每次创建,但是Method和MapperMethodInvoker会缓存在MapperProxyFactory中;
创建BoundSql
SqlSession执行sql前需要先创建BoundSql。(实际上select和update都需要先创建BoundSql)
BoundSql包含:创建Statement的sql ,入参映射 ,查询条件等。

MappedStatement#getBoundSql,创建BoundSql交给匹配的MappedStatement ,MappedStatement使用配置阶段拿到的SqlSource创建BoundSql。

SqlSource在启动阶段会解析为两种:
- 动态sql,DynamicSqlSource,包含${}占位符或mybatis标签(foreach、if、where等);
- 静态sql,RawSqlSource;
静态sql和动态sql都会委派给StaticSqlSource创建BoundSql,
静态sql ,构造时创建StaticSqlSource,运行时不需要解析;
动态sql ,每次执行sql,都需要解析配置和入参构造StaticSqlSource。
执行sql
所有sql执行都交给SqlSession的Executor。
- Transaction获取数据库连接;(DataSource#getConnection)
- StatementHandler 使用BoundSql中的sql,创建Statement;(Connection#prepareStatement)
- StatementHandler 使用ParameterHandler ,结合BoundSql中的ParameterMapping 入参映射+查询条件+TypeHandler,填充Statement;(PreparedStatement.setXXX)
- StatementHandler执行sql拿到ResultSet;(PreparedStatement#execute)
- (查询)StatementHandler 使用ResultSetHandler ,结合MappedStatement 对应的ResultMap 出参映射+ResultSet+TypeHandler,解析java对象返回;(ResultSet#getXXX)

4、特性
Cursor
Cursor游标查询,防止查询结果集全部加载到jvm内存,以迭代方式从ResultSet一条一条读取结果对象。
Cursor需要jdbc驱动支持,以mysql驱动为例:
- mybatis设置fetchSize ,如果fetchSize=Integer.MIN_VALUE ,使用流式ResultSet ;如果fetchSize>0,使用游标ResultSet;
- 游标ResultSet ,url参数开启useCursorFetch=true;
Mybatis层,使用SqlSession返回DefaultCursor,DefaultCursor迭代器CursorIterator使用特殊ResultHandler(ObjectWrapperResultHandler)读取ResultSet,每读一行就停下返回。
BatchExecutor
BatchExecutor用于批量插入/更新。
BatchExecutor需要jdbc驱动支持,以mysql为例,url开启rewriteBatchedStatements=true。
驱动层,通过调用Statement#addBatch 和Statement#executeBatch实现。
mybatis层:
BatchExecutor缓存所有执行的Statement;
每次执行sql,如果上一个statement与当前statement一致 ,可以持续Statement#addBatch;
flush阶段,循环所有Statement,执行Statement#executeBatch,清空缓存Statement;
三种触发flush方式:1-手动调用;2-事务提交;3-查询。
ReuseExecutor
ReuseExecutor用于Statement复用,减少远程调用次数。
驱动层,同一个Statement(id)可以反复设置参数执行。
mybatis层:
ReuseExecutor缓存所有执行的Statement;
每次执行sql,优先使用缓存中的Statement(和Batch不同,与执行顺序无关);
flush阶段,清空缓存Statement;
三种触发flush方式:1-手动调用;2-事务提交/回滚;3-查询。
ResultHandler
无论是Mapper还是SqlSession,都支持用户传入自定义ResultHandler收集ResultSet结果。
对于Mapper方法,如果返回void 且参数列表包含ResultHandler,走SqlSession#select,传入自定义ResultHandler。
如果用户未提供ResultHandler,默认DefaultResultHandler将每一行ResultSet解析结果缓存,最终返回一个list;如果用户提供ResultHandler,直接走用户ResultHandler。
mybatis提供了MapKey 注解(SqlSession#selectMap),用于SqlSession#selectList返回结果集后,二次收集结果 ,返回一个Map(key由MapKey指定,value是list中的对象)。selectMap虽然是对结果集的二次收集,也是通过实现特殊ResultHandler实现,即DefaultMapResultHandler。
Interceptor
作用:为四个组件Executor、StatementHandler、ParameterHandler、ResultSetHandler提供切面能力。
使用:
- 实现Interceptor 拦截逻辑(需要保证线程安全),使用Intercepts+Signature注解定义切点;
- Configuration注册Interceptor;
原理:
- 创建上述4个组件时,循环所有Interceptor,匹配注解声明切点class,为目标组件创建jdk动态代理;
- 运行上述4个组件时,代理逻辑走Interceptor,匹配注解声明切点method,判断是否走Interceptor;
5、spring
mybatis-spring:
- 实现SqlSessionFactoryBean ,用于创建SqlSessionFactory,mapperLocations支持批量扫描mapper.xml;
- 实现MapperFactoryBean,用于创建单例Mapper;
- 实现MapperScan,支持批量注册MapperFactoryBean;
- SqlSessionTemplate ,实现线程安全SqlSession(代理+ThreadLocal) ,与Spring事务集成(ThreadLocal) ;
mybatis-spring-boot-starter:
- 前提只有一个DataSource;
- 支持直接定义mybatis组件(如Interceptor)在ioc容器中,自动装配到Configuration;
- 自动配置SqlSessionFactory;
- 自动配置SqlSessionTemplate,业务可以直接拿到线程安全SqlSession使用;
- 自动扫描Mapper(启动类子路径+Mapper注解接口),前提是没有使用mybatis-spring配置Mapper(如MapperScan和MapperFactoryBean);