转自:https://juejin.cn/post/6983853041686577189
mybatis是当今Java
项目使用最为广泛的ORM框架,免除了几乎所有的JDBC
代码以及设置参数和获取结果集的工作。本文将会带大家从源码角度分析mybatis核心工作流程,知其然,更知其所以然。
源码地址:
寻找入口
要想理解完整的工作流程,肯定要从mybatis原始API
入手,下面是使用原始API
使用mybatis的示例代码(MyBatisTest.java
):
@Test
public void testSelect() throws IOException {
String resource = "config/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession(); // ExecutorType.BATCH
try {
// 通过sqlSession先获取到Mapper接口对象
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlogById(1688);
System.out.println(blog);
} finally {
session.close();
}
}
复制代码
可以明显看出来,入口代码就是SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
。
配置解析
mybatis有两种配置文件,一种是全局配置文件mybatis-config.xml
,另一种就是各个具体的Mapper文件xxxMapper.xml
。在上面的代码中,核心就是基于mybatis-config.xml
配置文件构建出了sqlSessionFactory
对象。跟进SqlSessionFactoryBuilder
的build()
方法:
上面代码的关键是调用了parser
的parse()
方法,它会返回一个Configuration
类。
明显可以看出来,解析配置是从configuration根节点开始解析的。具体解析过程如下:
解析配置完整方法上所示,主要的解析过程包含属性、类型别名、插件、对象工厂、对象包装工厂、设置、环境、类型处理器、映射器等等。下面挑选最关键的部分具体讲解。
属性 propertiesElement()
第一个是解析<properties>
标签,读取我们引入的外部配置文件。解析的最终结果就是我们会把所有的配置信息放到名为defaults
的Properties
对象里面,最后把XPathParser
和Configuration
的Properties
属性都设置成填充后的Properties
对象。
类型别名 typeAliasesElement()
类型别名是解析<typeAliases>
标签,它有两种定义方式,一种是直接定义一个类的别名,另一种是指定一个包,此时这个package
下面所有的类的名字就会成为这个类全路径的别名。类的别名和类的关系,放在一个TypeAliasRegistry
对象里面。
插件 pluginElement()
插件是解析<plugins>
标签。<plugins>
标签里面只有<plugin>
标签,<plugin>
标签里面只有<property>
标 签。标签解析完以后,会生成一个Interceptor
对象,并且添加到Configuration
的 InterceptorChain
属性里面,它是一个List
。
对象工厂 objectFactoryElement()
、objectWrapperFactoryElement()
这两个标签是用来实例化对象用的,分别解析的是<objectFactory>
和<objectWrapperFactory>
这两个标签,对应生成ObjectFactory
、ObjectWrapperFactory
对象,同样设置到Configuration
的属性里面。
设置 settingsElement()
设置是解析<settings>
标签,首先将所有子标签全部转换成了Properties
对象,然后再将相应的属性设置到Configuration
中。
二级标签里面有很多的配置,比如二级缓存,延迟加载,自动生成主键这些。需要注意的是,所有的默认值都是在这里赋值的。如果我们不知道这个属性的值是什么,也可以到这一步来确认一下。 所有的值,都会赋值到Configuration
的属性中。
环境 environmentsElement()
一个environment
就是对应一个数据源,所以在这里我们会根据配置的<transactionManager>
创建一个事务工厂,根据<dataSource>
标签创建一个数据源,最后把这两个对象设置成Environment
对象的属性,放到 Configuration
里面。
类型处理器 typeHandlerElement()
跟TypeAlias
一样,TypeHandler
有两种配置方式,一种是单独配置一个类,一种是指定一个package
。最后我们得到的是JavaType
和JdbcType
,以及用来做相互映射的TypeHandler
,并将其保存在 TypeHandlerRegistry
对象里面。
映射器 mapperElement()
映射器支持4种配置方式,包括类路径、绝对url路径、Java类名和自动扫描包方式。下面以自动扫描包为例,具体说明。查看configuration.addMappers(mapperPackage)
实现:
查找指定包下所有的mapperClass
,并注册。继续跟进addMapper(mapperClass)
实现:
添加映射就是将Mapper
类型和对应的MapperProxyFactory
关联,放到一个Map
容器中。并且在这里还会去解析Mapper
接口方法上的注解。具体来说是创建了一个MapperAnnotationBuilder
,我们点进去看一下 parse()
方法。
parseCache()
和parseCacheRef()
方法其实是对@CacheNamespace
和@CacheNamespaceRef
这两个注解的处理。parseStatement()
方法里面的各种getAnnotation()
,都是对注解的解析,比如@Options
,@SelectKey
,@ResultMap
等等。
最后同样会解析成MappedStatement
对象,也就是说在XML
中配置,和使用注解配置,最后起到一样的效果。
小结
经过上述步骤,我们主要完成了config
配置文件、Mapper
文件、Mapper
接口上的注解的解析。我们得到了一个最重要的对象Configuration
,这里面存放了全部的配置信息,它在属性里面还有各种各样的容器。最后,返回了一个DefaultSqlSessionFactory
,里面持有了Configuration
的实例。
创建会话
在构建好SqlSessionFactory
对象,接下来创建会话了,也就是执行SqlSession session = sqlSessionFactory.openSession()
,获取会话对象。具体源码如下:
首先从Configuration
里面拿到Enviroment
,再通过Enviroment
获取事务工厂TransactionFactory
。接下里,通过事务工厂来产生一个事务,再生成一个执行器(事务包含在执行器里),然后生成DefaultSqlSession
。
创建 Transaction
如果配置的是JDBC
,则会使用Connection
对象的commit()
、rollback()
、close()
管理事务。
如果是Spring + MyBatis
,则没有必要配置,因为会直接使用applicationContext.xml
里面配置数据源和事务管理器,覆盖MyBatis的配置。
创建 Executor
Executor
的基本类型有三种:SIMPLE
、BATCH
、REUSE
,默认是SIMPLE
(settingsElement()
读取默认值),他们都继承了抽象类BaseExecutor
。
Executor
三种类型的区别是什么?
SimpleExecutor
:每执行一次update
或select
,就开启一个Statement
对象,用完立刻关闭Statement
对象。ReuseExecutor
:执行update
或select
,以 sql 作为key
查找Statement
对象,存在就使用,不存在就创建。用完后,不关闭Statement
对象,而是放置于Map
内, 供下一次使用。简言之,就是重复使用Statement
对象。BatchExecutor
:执行update
(没有select
,JDBC 批处理不支持select
),将所有sql都添加到批处理中(addBatch()
),等待统一执行(executeBatch()
),它缓存了多个Statement
对象,每个Statement
对象都是addBatch()
完毕后,等待逐一执行executeBatch()
批处理。与 JDBC 批处理相同。
如果配置了cacheEnabled=ture
,会用装饰器模式对executor
进行包装:new CachingExecutor(executor)
。
最后调用Executor
的插件,执行对应的插件逻辑。(插件原理后续再讲)
小结
创建会话的过程,主要是获得了一个DefaultSqlSession
,里面包含了一个Executor
,它是SQL的执行者。
获取Mapper
对象
在创建好SqlSession
对象之后,接下来就是获取Mapper
对象了。Mapper.xml
文件与Mapper
类型通过namespace
进行关联,Statement ID
与方法名进行了关联。因此,调用Mapper
的方法就能执行相应的SQL了。
namespace
等于类的全路径名,Statement ID
等于方法名。
最后调用MapperRegistry
的getMapper()
方法。在前面配置解析阶段,我们讲过添加映射就是将Mapper
类型和对应的MapperProxyFactory
关联,放到一个Map
容器中。而这里就是根据Mapper
类型,得到对应的MapperProxyFactory
,接下来通过代理工厂就可以创建一个Mapper
代理对象了。
执行SQL
由于所有的Mapper
都是MapperProxy
代理对象,所以任意的方法都是执行MapperProxy
的invoke()
方法。
invoke()
方法的执行步骤主要有2步,第一步是查找MapperMethod,第二步是执行方法。
查找MapperMethod
优先从缓存中获取MapperMethod
,缓存中没有则创建一个。
执行方法
执行方法就是调用MapperMethod
的execute()
方法。
可以看到执行时就是4种情况,insert|update|delete|select
,分别调用SqlSession
的4大类方法。调用 convertArgsToSqlCommandParam()
将参数转换为SQL的参数。
接下来,我们以查询单行记录为例,最终会执行DefaultSqlSession.selectOne()
方法。
可以看到,selectOne()
最终也是调用了selectList()
。
在SelectList()
中,我们先根据Statement ID
从Configuration
中拿到 MappedStatement
,这个ms
上面有我们在 xml中配置的所有属性,包括id
、statementType
、sqlSource
、useCache
、入参、出参等等。
查询
在获取到MappedStatement
之后,接下就是调用执行器的query()
方法了。查询是最复杂的sql处理,接下来详细分析查询的执行流程。
前面我们说到了Executor
有三种基本类型,SIMPLE/REUSE/BATCH
。另外还有一种包装类型CachingExecutor
。
如果启用了二级缓存,就会先调用CachingExecutor
的query()
方法,里面有缓存相关的操作,然后才是再调用基本类型的执行器,比如默认的SimpleExecutor
。最终会调用BaseExecutor.query()
方法:
主要包含获取绑定sql、创建CacheKey和执行查询。
获取绑定sql
根据输入参数,获取绑定sql
创建CacheKey
MyBatis 对于其 Key 的生成采取规则为:[mappedStementId + offset + limit + SQL + queryParams + environment]
生成一个哈希码
执行查询
优先从缓存中查询,如果没有,则从数据库查询:queryFromDatabase()
。
数据库查询
先向缓存用占位符占位。执行查询后,移除占位符,放入数据。执行查询调用的是doQuery()
方法。
执行查询 doQuery()
主要包含创建StatementHandler
、准备语句和查询三步。
创建StatementHandler
在configuration.newStatementHandler()
中,创建了一个 StatementHandler
,先得到 RoutingStatementHandler
。RoutingStatementHandler
里面没有任何的实现,它是用来创建基本的 StatementHandler
的。这里会根据MappedStatement
里面的statementType
决定StatementHandler
的类型 。 默认是PREPARED
。 接下来以预处理语句处理器(PREPARED
)为例进行分析。
这里直接调用了父类BaseStatementHandler
的构造方法。
在构造方法里面,重点是生成了处理参数的ParameterHandler
和处理结果集的ResultSetHandler
。它们都可以被插件拦截。所以在创建之后都要用拦截器包装。
准备语句
调用prepareStatement()
方法对语句进行预处理。主要是根据连接准备语句和参数化处理。
查询
RoutingStatementHandler
的query()
方法最终委派给PreparedStatementHandler
执行。
在这里,调用了PreparedStatement
的execute()
方法,它的底层就是是调用了JDBC的PreparedStatement
进行执行,这里就不展开讲了。我们的重点是结果集的处理。
首先我们会先拿到第一个结果集,如果没有配置一个查询返回多个结果集的情况,一般只有一个结果集。也就是下面的这个while
循环只会执行一次,然后会调用handleResultSet()
方法。
如果没有配置结果处理器,则会使用默认的结果处理器DefaultResultHandler
,否则使用配置的结果处理器进行处理。
小结
总之,调用Mapper
对象的方法就是执行sql。首先,它会根据方法和参数得到绑定的sql,然后创建语句处理器、准备好语句等等,之后就会通过JDBC执行sql,最后处理结果集,将结果转化为Mapper
方法的返回类型返回。
总结
总的来说,mybatis核心工作流程是非常清晰的。
- 第一步是配置解析,这一步的重点就是根据配置文件生成
Configuration
,基于它然后构建DefaultSqlSessionFactory
对象。 - 第二步是创建会话,主要是获得了一个
DefaultSqlSession
对象,里面包含了一个Executor
,它是SQL的执行者。 - 第三步是获取
Mapper
对象,这一步主要是根据第一步注册号的Mapper
,创建好MapperProxyFactory
代理对象。后续所有的方法调用都会调用MapperProxyFactory
的invoke()
方法。 - 第四步是执行方法,在这里会创建语句处理器、准备好语句等等,之后就会通过JDBC执行sql,最后处理结果集,将结果转化为
Mapper
方法的返回类型返回。