MyBatis源码分析

前言

本文分析mybatis源码,主要是梳理思路,对mybatis有一个全局的认知:

  1. 原生mybatis的核心组件;
  2. 原生mybatis的执行流程;
  3. mybatis的几个特性,涉及驱动层的部分以mysql为例;
  4. 与spring集成的原理;

版本:

  1. mybatis:3.5.6;
  2. mybatis-spring:2.0.6;
  3. mybatis-spring-boot-starter:2.1.4;
  4. mysql-connector-java:8.0.21;

一、案例

传统mybatis的使用方式,分三个步骤:

  1. 写业务代码(Mapper&Mapper.xml);
  2. 配置SqlSessionFactory;
  3. 使用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有两种方式。

  1. 通过xml配置,xml如何编写参考官方文档,不细看;
ini 复制代码
String resource = "org/apache/ibatis/builder/MapperConfig.xml";
Reader reader = Resources.getResourceAsReader(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
  1. 通过编码配置;
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有两种方式:

  1. 通过Mapper映射器执行;
  2. 通过语句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包含:

  1. DataSource:数据源;
  2. TransactionFactory :事务工厂,mybatis提供了JdbcTransactionFactory由mybatis自己管理事务,如果和spring集成,需要让spring来管理事务;
  3. Environment:包含DataSource和TransactionFactory;
  4. Mapper:业务Mapper;
  5. Configuration:包含所有配置项,至少包含Environment和Mapper,还包含其他扩展,如Interceptor、TypeHandler等;
  6. SqlSessionFactory :启动阶段的产物,通过Configuration构造,往往是单例
  7. 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的双向转换

  1. 当处理PreparedStatement时,javaType需要转jdbcType;
  2. 当处理ResultSet时,jdbcType需要转javaType;

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

TypeAliasRegistry

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

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

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

LanguageDriverRegistry

LanguageDriverRegistry用于管理不同LanguageDriver实现。

LanguageDriver有两个作用:

  1. createSqlSource ,启动阶段,根据sql脚本,创建SqlSource对象;
  2. 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):核心方法

  1. knownMappers:存储Mapper类与Mapper代理工厂MapperProxyFactory
  2. parser.parse:解析Mapper类,得到sql配置(MappedStatement)注入Configuration;

MapperAnnotationBuilder#parse:对于Mapper映射器,sql配置来源可以有两处

  1. xml sql配置:主动找Mapper类全路径.xml作为sql配置,如com.x.y.ZMapper,找com/x/y/ZMapper.xml;
  2. 注解 sql配置:循环Mapper类的所有方法,解析注解sql配置,如Select注解;

解析sql配置包含众多属性(比如cache、resultMap等等),这里只分析主干逻辑(注解和xml都一样):

  1. 解析SqlSource;
  2. 组合众多属性和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配置无关):

  1. DynamicSqlSource:如果使用了${}占位符,或者使用了mybatis的xml标签(如foreach、if、where等),会解析为动态SqlSource;
  2. 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包含众多属性,其中比较重要的是:

  1. id:在Configuration中的唯一标识;
  2. SqlSource:上面提到了,通过LanguageDriver解析sql脚本得到的结果,运行时通过SqlSource获取BoundSql;
  3. ParameterMap:入参映射;
  4. 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有两种方式:

  1. 加载xml配置构造,实际和第二种一样,会解析为Configuration;
  2. 编码Configuration构造;

SqlSessionFactory 用于创建SqlSession

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

三、创建SqlSession

1、SqlSession

在mybatis中SqlSession的实现只有DefaultSqlSession一种。

除了final成员变量,还包含两个状态:

  1. dirty:是否发生更新,且还未提交/回滚;
  2. 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需要几个组件:

  1. Configuration:包含众多组件;
  2. Transaction:事务;
  3. Executor:执行器;

3、Transaction

TransactionFactory事务工厂创建Transaction事务对象。

事务工厂一般有三种实现:

  1. JdbcTransactionFactory:mybatis管理事务;
  2. ManagedTransactionFactory:由外部容器管理事务,简单了解;
  3. 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实例

  1. SimpleExecutor:默认Executor;
  2. BatchExecutor :批处理Executor,配合jdbc参数rewriteBatchedStatements=true使用;
  3. ReuseExecutor:预处理Statement复用Executor;
  4. 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

  1. wrapToMapIfCollection:如果没有Param注解且参数列表大小为1,如果入参是Collection或Array,会被封装为一个Map(Map的key可以是array、list、collection,所以mapper.xml里可以用),否则不变,返回一个原始入参对象(如Author);
  2. 否则,根据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"
  1. arg0:id没有设置Param注解,解析入参名得到,这里取决于编译时是否把入参名编译到class文件中。比如javac直接编译,这里只能拿到入参名=arg0,而javac -parameters编译,这里能拿到入参名=id,总的来说加Param注解更稳;
  2. param1、param2:mybatis按照入参顺序自动生成;
  3. 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]
  1. arg0,逻辑同案例1;
  2. collection:入参是Collection类型,固定key=collection;
  3. 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等)

  1. 决定PreparedStatement的sql;
  2. SqlSourceBuilder#parse:构造StaticSqlSource
  3. 像foreach这类标签,还会有mybatis生成的占位(*frch{item}*{index}),加入StaticSqlSource的扩展参数;

注:RawSqlSource静态sql,在启动阶段就完成了StaticSqlSource解析,运行时只需要执行StaticSqlSource#getBoundSql。

StaticSqlSource构造BoundSql会包含:

  1. configuration:全局配置;
  2. sql:最终调用PreparedStatement的sql
  3. parameterMappings:参数映射关系;
  4. parameterObject:最初SqlSession传入的入参;
  5. metaParameters:扩展参数和对应值,比如foreach生成的__frch_{item}_{index}和对应值;

ParameterMapping包含每个配置sql中#{}字段配置的属性。

  1. property:占位属性;
  2. javaType:java类型;
  3. jdbcType:jdbc类型;
  4. TypeHandler:类型转换器;

比如下面property=id,jdbcType指定BIGINT。

bash 复制代码
id = #{id,jdbcType=BIGINT}

对于RawSqlSource,解析ParameterMapping在启动阶段就完成了,而DynamicSqlSource需要每次跑sql都重新解析。

查询主流程

BaseExecutor#doQuery:具体查询逻辑不同Executor实现不同,以SimpleExecutor为例。

SimpleExecutor#doQuery:

  1. 构造StatementHandler,支持Interceptor扩展;
  2. 构造Statement;
  3. StatementHandler执行查询,构造返回结果;

SimpleExecutor#prepareStatement:构造Statement又分成三步

  1. 获取数据库连接;
  2. 使用StatementHandler创建Statement;
  3. 使用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)。

针对属性找到对应的属性值,有四种情况:

  1. mybatis生成的扩展属性,如foreach属性__frch_{item}_{index},从扩展属性值中取;
  2. 入参为空,属性值为空;
  3. 入参类型有对应TypeHandler,将入参作为属性值;
  4. 兜底,通过入参反射获取属性值;

最终交给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处理。

  1. 循环读取ResultSet;
  2. getRowValue:解析为java对象;
  3. storeObject:调用DefaultResultHandler#handleResult将解析后的java对象存储下来;

DefaultResultSetHandler#getRowValue:处理ResultSet的核心方法

  1. createResultObject,先构造对象实例,一般都是无参构造,当然ResultMap也支持constructor指定构造;
  2. hasTypeHandlerForResultObject,如果返回的是简单类型,如Integer、String,这里一定有TypeHandler,直接就可以返回rowValue了;
  3. MetaObject,封装反射调用逻辑,忽略;
  4. shouldApplyAutomaticMappings,是否能够自动映射,默认针对非嵌套情况都支持;
  5. applyAutomaticMappings ,针对ResultSet未被ResultMap column匹配的字段,尝试执行自动映射,设置属性值;
  6. applyPropertyMappings,使用ResultMap匹配成功的字段,设置属性值;
  7. 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:

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

ResultMap映射

DefaultResultSetHandler#applyPropertyMappings:

  1. 和自动映射相反,找到ResultMap的column与ResultSet中匹配的字段;
  2. 循环ResultMap中每个ResultMapping
  3. 获取属性值;
  4. 反射设置属性值;

ResultMapping和入参ParameterMapping类似。

主要包含

  1. 属性:property;
  2. java类型:javaType;
  3. jdbc类型:jdbcType;
  4. TypeHandler:类型转换,这里需要将jdbcType转换为javaType;
  5. 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:批量插入

  1. 请求server,创建批量插入Statement;
  2. 占位符填充;
  3. 请求server,传递占位符参数;

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

BatchExecutor原理

BatchExecutor缓存了:

  1. 当前SqlSession中所有待执行的Statement;
  2. 当前正在处理的sql及其对应MappedStatement;

BatchExecutor#doUpdate:

每次执行update,判断currentSql与本次update的sql是否一致:

  1. 如果不一致,创建新的Statement加入statementList;
  2. 如果一致,复用缓存的上一个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,配置基础依赖。

  1. DataSource;
  2. 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有两个非常重要的作用

  1. 实现SqlSession接口,特点是保证了线程安全
  2. 事务管理交给spring

线程安全

MapperFactoryBean#getObject:创建Mapper代理。

第一点非常重要的原因是,Mapper都由一个SqlSession创建。

这里如果还是传统mybatis的DefaultSqlSession是无法实现线程安全的,也就无法在ioc容器中使用单例Mapper。

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

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

比如selectList都交给sqlSessionProxy执行。

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

SqlSessionUtils#getSqlSession:实现SqlSession线程安全的核心方法。

  1. TransactionSynchronizationManager(spring-tx),获取SqlSessionFactory对应SqlSessionHolder;
  2. SqlSessionHolder,获取SqlSession;
  3. 如果SqlSession非空,返回;
  4. SqlSession为空,SqlSessionFactory开启新SqlSession,注意传入ExecutorType是SqlSessionTemplate的ExecutorType,如果没设置,取Configuration的默认ExecutorType,即Simple;
  5. 将新的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来组装,如:

  1. Interceptor:mybatis切面,支持StatementHandler、ParameterHandler、ResultSetHandler、Executor切面处理;
  2. TypeHandler:类型转换器,javaType和jdbcType转换;
  3. 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,遵循约定大于配置:

  1. AutoConfigurationPackages.get(this.beanFactory):使用启动类包路径
  2. Mapper接口需要加上Mapper注解

总结

配置阶段,mybatis解析配置为Configuration ,构造SqlSessionFactory

运行阶段,用户用SqlSessionFactory 创建SqlSession,执行sql,其中SqlSession非线程安全。

1、配置阶段

Configuration包含所有运行阶段需要的组件:

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

MappedStatement代表一个完整的sql配置,主要包含:

  1. id:唯一id,namespace(一般是Mapper的完全限定类名)+sql.id(xml sql元素的id属性)
  2. sqlSource:通过LanguageDriver 解析sql脚本得到,如果包含${}占位符或包含mybatis特殊标签(如foreach、if)会解析为DynamicSqlSource ,需要在每次跑sql时确定真实SqlSourceStaticSqlSource );除此以外的场景,上图中的sql脚本会解析为RawSqlSource ,在启动阶段就能确定真实SqlSource
  3. resultMaps:出参映射,一般只有一个,比如配置resultType或resultMap属性;
  4. parameterMap:入参映射;

无论使用xml配置还是编码配置,最终都通过Configuration创建SqlSessionFactory

DefaultSqlSessionFactory默认实现,持有Configuration。

2、创建SqlSession

SqlSessionFactory 创建SqlSession有两种方式:

1-通过Environment的DataSource创建;2-通过Connection创建,一般使用前者。

  1. TransactionFactory创建Transaction,Transaction持有DataSource,Transaction中的Connection此时还未打开,需要执行sql时获取;
  2. SqlSessionFactory创建Executor,Executor持有Transaction和Configuration;
  3. SqlSessionFactory创建SqlSession,SqlSession持有Executor和Configuration;

SqlSession实现是DefaultSqlSession在原生mybatis下非线程安全

3、执行sql

创建Mapper代理

执行sql有两种方式:1-通过Mapper映射器执行;2-SqlSession执行。

本质上Mapper映射器使用的也是SqlSession,以SqlSession#selectList为例,Mapper映射器需要解决几个问题:

  1. Mapper接口,需要创建代理,才能实际执行SqlSession方法;
  2. Mapper接口方法,需要找到MappedStatement(statement);
  3. Mapper接口方法入参,需要转换为SqlSession能接受的入参(parameter);

SqlSession#getMapper(Class):SqlSession创建Mapper代理逻辑

  1. 配置阶段,Configuration#addMapper,注册Mapper.class对应MapperProxyFactory
  2. 构造Mapper代理 ,使用jdk动态代理创建MapperProxy。MapperProxy持有SqlSession、Method对应MapperMethodInvoker,MapperMethodInvoker可以在运行时执行SqlSession方法;
  3. 执行Mapper方法 ,首次执行Method创建MapperMethodInvoker,匹配MappedStatement为一个SqlCommand(Mapper完全限定类名+方法名=mapper.xml的namespace+sql.id解析Method方法为MethodSignature用于后续反射调用(可以解析出SqlSession的入参parameter)
  4. 后续虽然Mapper动态代理需要每次创建,但是Method和MapperMethodInvoker会缓存在MapperProxyFactory中;

创建BoundSql

SqlSession执行sql前需要先创建BoundSql。(实际上select和update都需要先创建BoundSql)

BoundSql包含:创建Statement的sql入参映射查询条件等。

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

SqlSource在启动阶段会解析为两种:

  1. 动态sql,DynamicSqlSource,包含${}占位符或mybatis标签(foreach、if、where等);
  2. 静态sql,RawSqlSource;

静态sql和动态sql都会委派给StaticSqlSource创建BoundSql,

静态sql ,构造时创建StaticSqlSource,运行时不需要解析

动态sql每次执行sql,都需要解析配置和入参构造StaticSqlSource

执行sql

所有sql执行都交给SqlSession的Executor。

  1. Transaction获取数据库连接;(DataSource#getConnection)
  2. StatementHandler 使用BoundSql中的sql,创建Statement;(Connection#prepareStatement)
  3. StatementHandler 使用ParameterHandler ,结合BoundSql中的ParameterMapping 入参映射+查询条件+TypeHandler,填充Statement;(PreparedStatement.setXXX)
  4. StatementHandler执行sql拿到ResultSet;(PreparedStatement#execute)
  5. (查询)StatementHandler 使用ResultSetHandler ,结合MappedStatement 对应的ResultMap 出参映射+ResultSet+TypeHandler,解析java对象返回;(ResultSet#getXXX)

4、特性

Cursor

Cursor游标查询,防止查询结果集全部加载到jvm内存,以迭代方式从ResultSet一条一条读取结果对象。

Cursor需要jdbc驱动支持,以mysql驱动为例:

  1. mybatis设置fetchSize ,如果fetchSize=Integer.MIN_VALUE ,使用流式ResultSet ;如果fetchSize>0,使用游标ResultSet
  2. 游标ResultSet ,url参数开启useCursorFetch=true;

Mybatis层,使用SqlSession返回DefaultCursor,DefaultCursor迭代器CursorIterator使用特殊ResultHandler(ObjectWrapperResultHandler)读取ResultSet,每读一行就停下返回。

BatchExecutor

BatchExecutor用于批量插入/更新。

BatchExecutor需要jdbc驱动支持,以mysql为例,url开启rewriteBatchedStatements=true。

驱动层,通过调用Statement#addBatchStatement#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提供切面能力。

使用:

  1. 实现Interceptor 拦截逻辑(需要保证线程安全),使用Intercepts+Signature注解定义切点;
  2. Configuration注册Interceptor;

原理:

  1. 创建上述4个组件时,循环所有Interceptor,匹配注解声明切点class,为目标组件创建jdk动态代理;
  2. 运行上述4个组件时,代理逻辑走Interceptor,匹配注解声明切点method,判断是否走Interceptor;

5、spring

mybatis-spring:

  1. 实现SqlSessionFactoryBean ,用于创建SqlSessionFactory,mapperLocations支持批量扫描mapper.xml;
  2. 实现MapperFactoryBean,用于创建单例Mapper;
  3. 实现MapperScan,支持批量注册MapperFactoryBean;
  4. SqlSessionTemplate ,实现线程安全SqlSession(代理+ThreadLocal) ,与Spring事务集成(ThreadLocal)

mybatis-spring-boot-starter:

  1. 前提只有一个DataSource;
  2. 支持直接定义mybatis组件(如Interceptor)在ioc容器中,自动装配到Configuration;
  3. 自动配置SqlSessionFactory;
  4. 自动配置SqlSessionTemplate,业务可以直接拿到线程安全SqlSession使用;
  5. 自动扫描Mapper(启动类子路径+Mapper注解接口),前提是没有使用mybatis-spring配置Mapper(如MapperScan和MapperFactoryBean);
相关推荐
hqxstudying19 分钟前
java依赖注入方法
java·spring·log4j·ioc·依赖
新world21 分钟前
mybatis-plus从入门到入土(二):单元测试
单元测试·log4j·mybatis
·云扬·27 分钟前
【Java源码阅读系列37】深度解读Java BufferedReader 源码
java·开发语言
martinzh1 小时前
Spring AI 项目介绍
后端
Bug退退退1231 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
小皮侠2 小时前
nginx的使用
java·运维·服务器·前端·git·nginx·github
前端付豪2 小时前
20、用 Python + API 打造终端天气预报工具(支持城市查询、天气图标、美化输出🧊
后端·python
爱学习的小学渣2 小时前
关系型数据库
后端
武子康2 小时前
大数据-33 HBase 整体架构 HMaster HRegion
大数据·后端·hbase
前端付豪2 小时前
19、用 Python + OpenAI 构建一个命令行 AI 问答助手
后端·python