Mybatis源码深入学习

Mybatis 源码深入学习

Mybatis 介绍

MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github。 MyBatis是一个优秀的持久层框架,它对jdbc的操作数据库的过程进行封装,使开发者只需要关注 SQL 本身,而不需要花费精力去处理例如注册驱动、创建connection、创建statement、手动设置参数、结果集检索等jdbc繁杂的过程代码。
Mybatis通过xml或注解的方式将要执行的各种statement(statement、preparedStatemnt、CallableStatement)配置起来,并通过java对象和statement中的sql进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射成java对象并返回。

原生JDBC问题

jdbc编程步骤

1、 加载数据库驱动
2、 创建并获取数据库链接
3、 创建jdbc statement对象
4、 设置sql语句
5、 设置sql语句中的参数(使用preparedStatement)
6、 通过statement执行sql并获取结果
7、 对sql执行结果进行解析处理
8、 释放资源(resultSet、preparedstatement、connection)
代码:

public static void main(String[] args) {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
//加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
//通过驱动管理类获取数据库链接
connection =  DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8", "root", "123456");
//定义sql语句 ?表示占位符
String sql = "select * from user where username = ?";
//获取预处理statement
preparedStatement = connection.prepareStatement(sql);
//设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
preparedStatement.setString(1, "王五");
//向数据库发出sql执行查询,查询出结果集
resultSet =  preparedStatement.executeQuery();
//遍历查询结果集
while(resultSet.next()){
System.out.println(resultSet.getString("id")+"  "+resultSet.getString("username"));
}
} catch (Exception e) {
e.printStackTrace();
}finally{
//释放资源
if(resultSet!=null){
try {
resultSet.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(preparedStatement!=null){
try {
preparedStatement.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
 
}
 
}
jdbc问题总结

1、 数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接池可解决此问题。
2、 Sql语句在代码中硬编码,造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。
3、 使用preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不一定,可能多也可能少,修改sql还要修改代码,系统不易维护。
4、 对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据库记录封装成pojo对象解析比较方便。

Mybatis框架

架构图如下:
在这里插入图片描述
执行流程
1、 mybatis配置
SqlMapConfig.xml,此文件作为mybatis的全局配置文件,配置了mybatis的运行环境等信息。mapper.xml文件即sql映射文件,文件中配置了操作数据库的sql语句。此文件需要在SqlMapConfig.xml中加载。
2、 通过mybatis环境等配置信息构造SqlSessionFactory即会话工厂。
3、 由会话工厂创建sqlSession即会话,操作数据库需要通过sqlSession进行。
4、 mybatis底层自定义了Executor执行器接口操作数据库,Executor接口有两个实现,一个是基本执行器、一个是缓存执行器。
5、 Mapped Statement也是mybatis一个底层封装对象,它包装了mybatis配置信息及sql映射信息等。mapper.xml文件中一个sql对应一个Mapped Statement对象,sql的id即是Mapped statement的id。
6、 Mapped Statement对sql执行输入参数进行定义,包括HashMap、基本类型、pojo,Executor通过Mapped Statement在执行sql前将输入的java对象映射至sql中,输入参数映射就是jdbc编程中对preparedStatement设置参数。
7、 Mapped Statement对sql执行输出结果进行定义,包括HashMap、基本类型、pojo,Executor通过Mapped Statement在执行sql后将输出结果映射至java对象中,输出结果映射过程相当于jdbc编程中对结果的解析处理过程。

简单使用过程

主要针对源码说明,所以依赖就不详说,主要列出核心配置文件SqlMapConfig.xml

<?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>
<!-- 和spring整合后 environments配置将废除-->
<environments default="development">
<environment id="development">
<!-- 使用jdbc事务管理-->
<transactionManager type="JDBC" />
<!-- 数据库连接池-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8" />
<property name="username" value="root" />
<property name="password" value="123456" />
</dataSource>
</environment>
</environments>
</configuration>

测试程序

public class Mybatis_first {
//会话工厂
private SqlSessionFactory sqlSessionFactory;
 
@Before
public void createSqlSessionFactory() throws IOException {
// 配置文件
String resource = "SqlMapConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
 
// 使用SqlSessionFactoryBuilder从xml配置文件中创建SqlSessionFactory
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(inputStream);
 
}
 
// 根据 id查询用户信息
@Test
public void testFindUserById() {
// 数据库会话实例
SqlSession sqlSession = null;
try {
// 创建数据库会话实例sqlSession
sqlSession = sqlSessionFactory.openSession();
// 查询单个记录,根据用户id查询用户信息,假设有一个UserMapper.xml
User user = sqlSession.getMapper(UserMapper.class).selectOne(1);
// 输出用户信息
System.out.println(user);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
 
}
}

源码解析

SqlSessionFactory创建

从入口sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream)开始,配置文件SqlMapConfig.xml同流的方式读取,交给SqlSessionFactoryBuilder创建,build()方法。
创建XMLConfigBuilder,交给其parse解析配置属性
SqlMapConfig.xml中配置的内容和解析顺序如下:

properties(属性)
settings(全局配置参数)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境集合属性对象)
-environment(环境子属性对象)
–transactionManager(事务管理)
–dataSource(数据源)
mappers(映射器)

在这里插入图片描述
this.build(parser.parse())—》XMLConfigBuilder#parse()—>parseConfiguration():
在这里插入图片描述
this.propertiesElement(root.evalNode(“properties”)):《properties》标签解析

例如:
在classpath下定义db.properties文件,
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8
jdbc.username=root
jdbc.password=root
SqlMapConfig.xml引用如下:
< properties resource=“db.properties”/>
将所有属性都读取,k-v方式创建properties,加入Configuration的variables
在这里插入图片描述

Properties settings = this.settingsAsProperties(root.evalNode(“settings”)):加载全局配置
加载自定义类扫描器,用于扫描所有class文件
this.loadCustomVfs(settings);
this.loadCustomLogImpl(settings);

typeAliases解析
this.typeAliasesElement(root.evalNode(“typeAliases”)):别名扫描,将别名标签扫面,并建立type-alias的映射,两种类型说明:

< package name=“cn.demo.mybatis.pojo”/>
包扫描路径配置@Alias注解的类
< typeAlias alias=“user” type=“cn.demo.mybatis.po.User”/>
单个类型配置

在这里插入图片描述
在这里插入图片描述
补充说明,构造时,会注册默认别名标签的解析,比如基本数据类型int、string、long等,如下
在这里插入图片描述

this.pluginElement(root.evalNode(“plugins”));
plugins标签解析,plugins下面的Interceptor解析
在这里插入图片描述
自定义pojo工厂类的解析
this.objectFactoryElement(root.evalNode(“objectFactory”));
this.objectWrapperFactoryElement(root.evalNode(“objectWrapperFactory”));
this.reflectorFactoryElement(root.evalNode(“reflectorFactory”));
settings标签解析,全局变量的设置,然后加入configuration中
this.settingsElement(settings)
在这里插入图片描述
this.environmentsElement(root.evalNode(“environments”));
environments标签解析,如下格式,environment内transactionManager(事务管理器)、dataSource(数据源)的解析实例化
在这里插入图片描述

environments标签格式说明:
< properties resource=“db.properties”/>
< environments default=“development”>
< environment id=“development”>
< transactionManager type=“JDBC”/>
< dataSource type=“POOLED”>
< property name=“driver” value=“$ {jdbc.driver}”/>
< property name=“url” value=“$ {jdbc.url}”/>
< property name=“username” value=“$ {jdbc.username}”/>
< property name=“password” value=“$ {jdbc.password}”/>
< /dataSource>
< /environment>
< /environments>

数据库类型解析,比如mysql、sql server 、oracle等。
this.databaseIdProviderElement(root.evalNode(“databaseIdProvider”));
在这里插入图片描述
类型转换器解析
this.typeHandlerElement(root.evalNode(“typeHandlers”));
jdbcType、javaType、handler解析
在这里插入图片描述
以package扫描为例,先扫描包下所有TypeHandler类,然后注册,判断是否有@MappedTypes注解,然后判断是否有@MappedJdbcTypes注解
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
然后建立Javatype、JdbcType、TypeHandler的映射关系
在这里插入图片描述
mapper标签解析

Mapper配置的几种方法:
1、< mapper resource=" " />
使用相对于类路径的资源
如:< mapper resource=“sqlmap/User.xml” />
2、 < mapper class=" " />
使用mapper接口类路径
如:< mapper class=“cn.demo.mybatis.mapper.UserMapper”/>
注意:此种方法要求mapper接口名称和mapper映射文件名称相同,且放在同一个目录中。
3、 < package name=“”/>
注册指定包下的所有mapper接口
如:< package name=“cn.demo.mybatis.mapper”/>
注意:此种方法要求mapper接口名称和mapper映射文件名称相同,且放在同一个目录中。

在这里插入图片描述
this.mapperElement(root.evalNode(“mappers”)),优先resource,以resource为例,创建XMLMapperBuilder解析器,进行解析。parse()—>configrationElement()
在这里插入图片描述
在这里插入图片描述
configurationElement(),进行标签解析
1、设置命名空间namespace
this.builderAssistant.setCurrentNamespace(namespace);
2、其他标签解析
cache相关
this.cacheRefElement(context.evalNode(“cache-ref”));
this.cacheElement(context.evalNode(“cache”));
parameterMapping解析,封装为ParameterMapping对象
this.parameterMapElement(context.evalNodes(“/mapper/parameterMap”));
ResultMapping解析,封装为ResultMapping对象
this.resultMapElements(context.evalNodes(“/mapper/resultMap”));
Sql标签解析
this.sqlElement(context.evalNodes(“/mapper/sql”));
3、select、insert、update、delete标签解析
this.buildStatementFromContext(context.evalNodes(“select|insert|update|delete”));
—》XMLStatementBuilder#parseStatementNode()
在这里插入图片描述
3.1、判断databaseId与配置的是否一致,不一致则什么都不做,既不解析;
3.2、include标签解析
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(this.configuration, this.builderAssistant);
includeParser.applyIncludes(this.context.getNode());
3.3、selectkey标签解析
this.processSelectKeyNodes(id, parameterTypeClass, langDriver);
3.4、select、insert、update、delete子标签解析,if、where、trim、set、foreach、choose、when、bind、otherwise等。
在这里插入图片描述

解析思路说明:
标签会涉及循环嵌套的的用法,比如
在这里插入图片描述
解析思路则是以树的结构嵌套,相同层级的标签为兄弟节点、如果为文本,则为叶子节点,这样递归解析封装,每个节点称之为SqlNode,每个SqlNode下有一个List< SqlNode> contents属性,存储下一级的节点列表,这样每个标签就变成一颗SqlNode树结构,包装成SqlSource对象

SqlSource sqlSource = langDriver.createSqlSource(this.configuration, this.context, parameterTypeClass);---->XMLLanguageDriver#createSqlSource()---->XMLScriptBuilder#parseScriptNode()—>parserDynamicTags()
在这里插入图片描述
在这里插入图片描述
判断是不是非文本标签,非文本标签则handler.handleNode(child, contents),需要继续解析;若是文本标签,则判断是否是动态标签(其实就是判断文本是否有${}),非静态直接升级为纯文本标签StaticTextSqlNode;
handler.handleNode(child, contents),以策略模式,进行递归解析以where标签为例,
在这里插入图片描述
parseDynamicTags又回到上面,每个都封装为MixedSqlNode的树形节点结构。
获取并设置属性比如parameterMap、resultType、resultMap等等。
在这里插入图片描述
3.4、构建MappedStatement对象(每一个SQL标签都会封装成一个MappedStatement对象)
设置ParameterMap、sqlSource、ResultMap等等属性
在这里插入图片描述
在这里插入图片描述
到这里就完成了配置的解析,然后bindMapperForNamespace()绑定映射
在这里插入图片描述
建立mapper类与代理工厂的映射关系
在这里插入图片描述
创建MapperAnnotationBuilder进行mapper方法上的注解解析(@select、@insert、@update等),其实也就是将注解信息解析包装成标签类型的结构,然后进行解析。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最后build()创建DefaultSqlSessionFactory对象
在这里插入图片描述
SqlSessionFactory构成完成。

开启SqlSession

sqlSession = sqlSessionFactory.openSession()—>openSessionFromDataSource()
在这里插入图片描述
1、获取环境对象Environment(datasource、transactionFactory)和事务工厂
Environment environment = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
在这里插入图片描述
2、通过事务工厂获取事务对象
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit)
默认创建JdbcTransaction
在这里插入图片描述
在这里插入图片描述
3、获取执行器对象Executor(mybatis提供四种执行器BatchExecutor、ReuseExecutor、SimpleExecutor默认、CachingExecutor)
在这里插入图片描述注意:interceptorChain.pluginAll(executor)这里会生成代理,既拦截器的拓展

4、创建DefaultSqlSession
在这里插入图片描述

获取Mapper代理对象

sqlSession.getMapper(xxxMapper.class),通过解析配置时映射关系找到MapperProxyFactory并创建代理对象MapperProxy
在这里插入图片描述
在这里插入图片描述

Mapper方法调用

Mapper方法调用是通过代理对象调用的,JDK的代理原理,方法会执行到invoke()方法,如下:
在这里插入图片描述
这里有一个方法对象缓存MapperMethodInvoker,Map<Method, MapperProxy.MapperMethodInvoker> methodCache,对于同一个线程,同一个方法的反复调用为同一个代理对象。

MapperMethod信息说明:
在这里插入图片描述
SqlCommand:SQL的命令类型说明
MethodSignature:方法标签,方法返回值信息说明
在这里插入图片描述
方法标签构造时会处理诸多方法信息,比如返回结果类型、参数解析器的创建等等。
在这里插入图片描述

PlainMethodInvoker#invoke()
在这里插入图片描述

SQL执行Execute

MapperMethod#excute()执行SQL
在这里插入图片描述
这里分增删改查不同情况,但流程大同小异,以常见的查询为例;
result = this.executeForMany(sqlSession, args)
1、参数转换解析Object param = this.method.convertArgsToSqlCommandParam(args),这里建立mapper方法中的参数和真实参数的映射,这里有个前提是在MethodSignature构造时,建立mapper方法参数和@param的参数进行映射处理
在这里插入图片描述
在这里插入图片描述
2、Sql执行
在这里插入图片描述
这里在位交给Spring管理时,调用的是DefaultSqlSession#selectList(),获取MappedStatement对象,执行query操作
在这里插入图片描述
在这里插入图片描述
2.1、BoundSql获取
在这里插入图片描述BoundSql封装的完整的SQL语句(除占位符外),获取BoundSql也就是对前面SqlSource中的SqlNode树形结构进行递归解析拼接成完整的sql语句,如下
在这里插入图片描述
2.1.1、DynamicSqlSource:动态sql标签解析
在这里插入图片描述
rootSqlNode.apply(context)对SqlNode子标签进行解析,递归的遍历解析
在这里插入图片描述
比如if标签IfSqlNode中的条件判断:
在这里插入图片描述
在这里插入图片描述
复杂层次的MixedSqlNode:
在这里插入图片描述
进行SqlNode解析后,还需要进行参数替换和映射,也就是将#{xxx}替换成?sqlSourceParser.parse,封装成纯静态SqlSource-----StaticSqlSource,而StaticSqlSource包装的BoundSql就是接近完整的SQL语句(动态参数用?代替的),将#{xxx}变为?;
在这里插入图片描述
然后调用StaticSqlSource#getBoundSql()
在这里插入图片描述
RawSqlSource:其实和StaticSqlSource一样,只是对StaticSqlSource一次包装,最终还是调用StaticSqlSource#getBoundSql()

2.1.2、query执行
在这里插入图片描述
先从缓存中获取localCache,为session级缓存,只对同一个session对象的同一个方法,相同参数生效,缓存没有才走数据库
在这里插入图片描述
查询doQuery(),然后putObject()设置缓存
在这里插入图片描述
默认为SimpleExecutor#doQuery(),对jdbc的PreparedStatement封装操作
在这里插入图片描述

StatementHandler预处理

在这里插入图片描述
获取StatementHandler,默认创建RoutingStatementHandler,这里可以Interceptor自主定义扩展
在这里插入图片描述
默认创建RoutingStatementHandler方式,同时会初始化创建ParameterHandler和ResultSetHandler
在这里插入图片描述
在这里插入图片描述
this.prepareStatement(handler, ms.getStatementLog()),预处理执行
在这里插入图片描述
1、获取连接对象getConnection(),三个不同类型,JdbcConnection、SpringManegedTranscation(spring整合时创建)
在这里插入图片描述
在这里插入图片描述
同时,如果日志等级为debug,则会创建代理对象ConnectionLogger(包装连接对象)
在这里插入图片描述
在调用Connection的方法时通过代理对象调用,进行相关日志打印处理
2、预处理
来到handler.prepare(connection, this.transaction.getTimeout())
在这里插入图片描述
instantiateStatement()实例化Statement
在这里插入图片描述
这里会调用Connection对象的prepareStatement()方法
设置超时
this.setStatementTimeout(statement, transactionTimeout);
在这里插入图片描述
设置分批大小,这里就是避免大量数据没有分页或者数据量过大时,分批次取数据的阈值,优先单个配置再全局配置
this.setFetchSize(statement);
在这里插入图片描述

ParameterHandler参数设置

在这里插入图片描述
来到 handler.parameterize(stmt);
在这里插入图片描述
DefaultParameterHandler#setParameters(),将sql语句中的 ?替换成真正的参数值,会通过参数类型找到对应的处理器,然后调用PreparedStatement的setXXX()方法

在这里插入图片描述

ResultSetHandler获取结果集,结果集映射

在这里插入图片描述
在这里插入图片描述
获取sql语句、执行sql、结果集处理
在这里插入图片描述
1、获取结果集
在这里插入图片描述
结果集合映射可能有多个(比如存储过程的调用),所以需要逐一遍历处理,获取第一个结果集,也就Statement#getResultSet(),然后mybatis进行包装为ResultSetWrapper
在这里插入图片描述
ResultSetWrapper的创建过程,就是拿到元数据,然后所有的列进行列名、对应的pojo类型的收集
在这里插入图片描述
2、结果集映射处理
在这里插入图片描述
首先创建ResultHandler和ObjectFactory(默认的对象工厂),然后利用结果处理器处理每一行数据
在这里插入图片描述
获取结果集ResultSet,遍历结果集的每一行,处理每一行数据,分基本类似和object类型
在这里插入图片描述
object:
在这里插入图片描述
着重看createResultObject()方法,会创建一个空对象,既属性值没有填充
在这里插入图片描述
在这里插入图片描述
匹配参数构造,遍历处理每一个参数
在这里插入图片描述
如果又是ResultMap的需要再getRowValue()递归处理,否则根据对应的参数类型策略模式处理参数值
在这里插入图片描述
在这里插入图片描述
不管哪个参数,最终都会到TypeHandler#getResult()处理然后调用getNullableResult(),而不同的参数类型对应着一个TypeHandler,这个TypeHandler会在解析mapper标签时就建立好映射
在这里插入图片描述
在这里插入图片描述
createResultObject完成后,进行属性映射,这里有个判断自动映射的处理,比如驼峰和下划线的映射等详情看applyAutomaticMappings(),这里不重点说明,主要看applyPropertyMappings()属性映射
在这里插入图片描述
其实就是遍历列和属性,判断查找
在这里插入图片描述
再就是applyNestedResultMappings(),对参数可能是一个结构体的进行处理,比如list、map、(非基本数据类型包装类的)object等等。

核心组件应用与拓展

缓存与懒加载

缓存
一级缓存

在一次数据库会话中,执行多次查询条件完全相同的SQL时,MyBatis提供了一级缓存的方案优化,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
从源码中可以看到。SqlSessionFactory在openSession()时,会开启一次会话,而每个SqlSession中持有了Executor,每个Executor中有一个LocalCache。
在这里插入图片描述
当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户,如下:
在这里插入图片描述
看下每次查询的缓存生成大致规则,query_id、分页、sql语句、参数等。
在这里插入图片描述
优先根据缓存key查询一级缓存是否有数据,有则从缓存获取,否则从数据库获取
在这里插入图片描述
而数据库获取后,会再次设置缓存
在这里插入图片描述

二级缓存

我们知道,在一级缓存中最大的共享范围就是在一个SqlSession内部,如果需要在多个SqlSession之间,一级缓存就会失效,这时共享缓存就需要使用二级缓存。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询。
二级缓存开启后,同一个namespace下的所有SQL语句都是共用同一个Cache,即二级缓存被多个SqlSession共享。
相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
当开启缓存后,查询数据会经过二级缓存 -> 一级缓存 -> 数据库。
但在分布式中,这种缓存很容易脏数据,并且二级缓存很容易用更好的方案替代,比如redis等。
在源码中:CachingExecutor持有一个Executor和一个缓存管理器
在这里插入图片描述在查询时,会先从二级缓存中查,没有再委托给持有的Executor去做查询
在这里插入图片描述

懒加载

在mybatis中,当涉及关联标签时,并且有嵌套查询时,比如在resultMap标签中关联关系association(一对一)、collection(一对多)

fetchType为lazy,并且有select值时,就会触发懒加载
在这里插入图片描述

在查询时,有一个queryStack查询栈,记录当前会话的查询次数,当queryStack大于1时,表示有嵌套查询
在这里插入图片描述
然后通过之前的查询源码,最终到DefaultResultSetHandler#handleRowValues()方法,在遍历每一个属性映射时会判断该属性是否是一个嵌套查询,着重看handleRowValuesForNestedResultMap()方法
在这里插入图片描述
这里有一个nestedResultObjects的缓存,并且根据此次查询也创建了一个缓存key
在这里插入图片描述
如果有缓存,直接调用applyNestedResultMappings,没有则到下面的createResultObject
在这里插入图片描述
如果有嵌套并且有懒加载,则创建一个代理对象,会存入一个懒加载器lazyLoader,也就是说实际返回的resultMap是一个代理类
在这里插入图片描述
接着来到applyPropertyMappings
在这里插入图片描述
getPropertyMappingValue()判断getNestedQueryId是否嵌套查询
在这里插入图片描述
进入getNestedQueryMappingValue,先进行一系列包装处理得到MappedStatement,然后判断propertyMapping.isLazy()是懒加载,则添加lazyLoader,并且返回一个空对象 (DEFERRED),如果是非懒加载则 resultLoader.loadResult()在这里插入图片描述
进行Excutor的查询
在这里插入图片描述
而懒加载的处理则是在调用属性的get方法时再处理的,以cglib代理为例,在调用方法时是通过代理方法调用的,进到CglibProxyFactory#intercept()中可看出在这里插入图片描述
调用get属性方法时。会有一个this.lazyLoader.load(property),通过追踪load()方法,可以看到也会调到resultLoader.loadResult(),而此时的load查询和外层查询属于同一个session,如果多次嵌套查询命中一级缓存时是可以不用走数据库的。
在这里插入图片描述

总结:mybatis的懒加载原理,其实就是用的代理对象,有懒加载的resultMap返回的是代理对象,在调用懒加载属性的get方法时,才会出去该属性的loadResult去数据库查询,非懒加载时在设置resultMap属性时就会直接触发loadResult查询。

插件与分页

Mybatis的四种插件Executor、StatementHandler、ParameterHandler、ResultSetHandler。
四种插件的调用顺序分别是Executor、StatementHandler、ParameterHandler、ResultSetHandler。
Mybatis插件原理分析:
解析plugin标签时会添加所有的拦截器到InterceptorChain中
在这里插入图片描述
在创建四个对象时,会通过InterceptorChain#pluginAll()方法添加所有的Interceptor#plugin()方法
在这里插入图片描述
然后就是插件的代理生成,Plugin.wrap(target, this)就是用于插件的代理生成,如果返回的源对象表示没有做代理,所有的插件想要代理必须通过Plugin.wrap(target, this)生成代理才会生效
在这里插入图片描述
来到Plugin.wrap(target, this)
在这里插入图片描述
这里做的操作有:
1、getSignatureMap(interceptor)获取拦截器的拦截定义信息
解析@Intercepts的注解信息封装成signatureMap容器中,这里涉及到插件的定义

插件定义,以分页插件为例,其实就是解析@Intercepts里面的@Signature信息,
在这里插入图片描述
Signature中参数说明
type:接口类型,类型也就是Executor、StatementHandler、ParameterHandler、ResultSetHandler四种
method:方法名称
agrs:参数类型

2、获取当前目标类信息
3、类型匹配,匹配如果需要代理,则生成代理对象Plugin,类型匹配就是从signatureMap中的type属性匹配当前对象的类型
在这里插入图片描述
到这里就创建了代理对象,当调用方法时,会进入invoke()方法,这里会进行匹配方法,就是根据定义的method和args匹配方法,匹配到则调用intercep()方法,否则调用目标方法
在这里插入图片描述

Executor插件

代理生成时机:
SessionFactory开启session时,即就是调用openSession()方法中
在这里插入图片描述

在这里插入图片描述

调用时机:
在这里插入图片描述

调用时机与插件的方法拦截定义有关,即@Signature的信息,以分页插件的定义为例,当通过SqlSession调用到Executor的两个query方法时,也就是所有的查询时,就会被拦截器处理。
注意:此时还处于StatementHandler预处理前,也就是预处理前如果有需要拦截处理的就可以定义该类型的拦截

StatementHandler插件

代理生成时机:在Executor的方法中,以查询为例如下,从queryFromDatabase()—>doQuery()
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
调用时机:其实就是通过调用StatementHandler的方法时触发,原理都一样
在这里插入图片描述

ParameterHandler插件

代理生成时机:创建StatementHandler时,父类初始化时创建
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
调用时机:通过调用ParameterHandler的方法时触发,原理都一样
在这里插入图片描述

ResultSetHandler插件

代理生成时机:与ParameterHandler插件一致
在这里插入图片描述

调用时机:通过调用ResultSetHandler的方法时触发,原理都一样
在这里插入图片描述

插件应用

TypeHandler

TypeHandler是mybatis中对参数映射和结果映射的类型处理器,在解析标签阶段会解析对应的TypeHandler,并注册到configuration对象中。用于自定义参数类似转换,比如日期和时间的格式转换统一;
举个例子,比如我们需要将数据库的日期或时间对象在封装时直接变成字符串处理,如下:
日期:

@MappedJdbcTypes({JdbcType.DATE})
@MappedTypes({String.class})
public class DateTypeHandler extends BaseTypeHandler<String> {
    public DateTypeHandler() {
    }

    public void setNonNullParameter(PreparedStatement preparedStatement, int i, String s, JdbcType jdbcType) throws SQLException {
        if (StrUtil.isBlank(s)) {
            s = null;
        }

        preparedStatement.setString(i, s);
    }

    public String getNullableResult(ResultSet resultSet, String s) throws SQLException {
        return DateUtil.formatDate(resultSet.getDate(s));
    }

    public String getNullableResult(ResultSet resultSet, int i) throws SQLException {
        return resultSet.getString(i);
    }

    public String getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        return callableStatement.getString(i);
    }
  }

时间戳:

@MappedJdbcTypes({JdbcType.TIMESTAMP})
@MappedTypes({String.class})
public class DatetimeTypeHandler extends BaseTypeHandler<String> {
    public DatetimeTypeHandler() {
    }

    public void setNonNullParameter(PreparedStatement preparedStatement, int i, String s, JdbcType jdbcType) throws SQLException {
        if (s.contains("T") && s.contains("Z")) {
            s = s.substring(0, s.lastIndexOf(".")).replace("T", " ");
        }

        if (StrUtil.isBlank(s)) {
            s = null;
        }

        preparedStatement.setString(i, s);
    }

    public String getNullableResult(ResultSet resultSet, String s) throws SQLException {
        return DateUtil.formatDateTime(resultSet.getTimestamp(s));
    }

    public String getNullableResult(ResultSet resultSet, int i) throws SQLException {
        return resultSet.getString(i);
    }

    public String getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        return callableStatement.getString(i);
    }
}

整合Spring

1、配置注册扫描器,扫描mapper文件,并修改BeanDefinition,完成Mapper注册

整合Spring其实就是把对象交给Spring管理,以注解方式为例,版本mybatis-spring-2.0.5,入口在@MapperScan处,根据spring源码的加载机制,MapperScannerRegistrar会被Spring加载,
在这里插入图片描述
MapperScannerRegistrar,实现ImportBeanDefinitionRegistrar,所以在spring在加载创建后会调用registerBeanDefinitions()方法,这里就是获取@MapperScan的注解信息
在这里插入图片描述
这里注册MapperScannerConfigurer,用于Mapper文件扫描
在这里插入图片描述
MapperScannerConfigurer实现BeanDefinitionRegistryPostProcessor,Spring在扫描过程中会调用到
BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry()进行修改BeanDefinition
在这里插入图片描述
创建扫描器ClassPathMapperScanner,进行扫描,借助Spring扫描器扫描(super.doScan()),然后再修改BeanDifinition
在这里插入图片描述
把BeanClass修改为MapperFactoryBean,把 AutowireMode修改为byType
在这里插入图片描述
在这里插入图片描述
扫描完成后,由于每个mapper的BeanClass被修改为MapperFactoryBean,所以每个Mapper对应的就是一个MapperFactoryBean对象。

2、代理生成

MapperFactoryBean是一个FactoryBean对象,所以每次创建会调用到getObject()
在这里插入图片描述
getSqlSession(),返回的是SqlSessionTemplate
在这里插入图片描述
SqlSessionTemplate结构:sqlSessionFactory、sqlSessionProxy
在这里插入图片描述
在这里插入图片描述

getMapper()生成mapper代理,这个代理就是spring容器中的对象
在这里插入图片描述

sqlSession对象是Mybatis中的,一个sqlSession对象需要SqlSessionFactory来产生。
在mapper扫描时MapperFactoryBean的AutowireMode被修改为byType,所以Spring会自动调用set方法,有两个set 方法,一个setSqlSessionFactory,一个setSqlSessionTemplate,而这两个方法执行的前提是 根据方法参数类型能找到对应的bean,所以Spring容器中要存在SqlSessionFactory类型的 bean或者SqlSessionTemplate类型的bean

3、mapper方法调用

当调用Mapper方法时,会调用到SqlSessionTemplate方法,而SqlSessionTemplate会交给sqlSessionProxy代理对象处理,SqlSessionInterceptor#invoke()
在这里插入图片描述
3.1、SqlSessionUtils.getSqlSession()获取mybatis的SqlSession,没有的话就会创建DefaultSqlSession
在这里插入图片描述
在这里插入图片描述

先从事务管理器中拿,其实就是从当前线程池中获取,只有当前开启事务时,该地方才会有值
在这里插入图片描述
在这里插入图片描述
这里如果事务管理器中有,就说明holder不为空,表面有事务,那么就会返回事务管理器中的SqlSession对象
在这里插入图片描述
从这里就可以得出结论:
Spring整合Mybatis后,如果执行某个方法时,该方法上没有加@Transactional注解,也就是没有开启Spring事务,那么后面在执行具体sql时,每执行一个sql时都会新生成一个SqlSession 对象来执行该sql,也就是说mybatis的一级缓存失效(也就是没有使用同一个SqlSession对象),而如果开启了Spring事务,那么该Spring事务中的多个sql,在执行时会使用同一个SqlSession对象,从而一级缓存生效。

3.2、调用执行,进入Mybatis的底层方法调用SqlSession 的方法
method.invoke(sqlSession, args),但是有变化的地方,
在这里插入图片描述
这里的TransactionFactory是在SqlSessionFactoryBean初始化时放入environment的SpringManagedTransactionFactory对象
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Transaction tx = transactionFactory.newTransaction(connection)会返回SpringManagedTransaction
在这里插入图片描述
所以,后续的数据源都是从spring的事务管理器中获取的、事务管理也是如此。

3.3、判断是不是Spring事务,非Spring事务则设置commit为自动提交
在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值