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方法的返回类型返回。
原创不易,觉得文章写得不错的小伙伴,点个赞👍 鼓励一下吧~
欢迎关注我的开源项目:一款适用于SpringBoot的轻量级HTTP调用框架