扑街前言:本来dubbo结束之后,应该是要写SpringCloud相关的组件内容的,但是因为目前本人要去重新面试,所以先整理一下myBatis、Spring、SpringBoot之类的源码分析,至于SpringCloud后面有时间了再说。本次说一下mybatis的源码分析,原本计划是一篇文章就搞定的,结果发现还是不够写,那么本篇就只分析mybatis 的结构设计和传统方式调用的源码,后续的代理方式也就是Spring 结合的注解调用模式下篇文章再说。
目录
sqlSessionFactory的创建,又叫封装Configuration
解析sql配置文件,并封装到Configuration的mapperRegistry集合
封装sql对象至configuration的mappedStatements集合
PreparedStatement对象执行sql和结果集的处理
MyBatis的架构设计
老规矩看框架源码先看框架的架构设计,先上图。说明一下下面几层的大体职责,首先是接口层:对外提供增删改查接口,接着就是数据处理层:设置参数、sql解析、封装结果集等,然后就是框架支撑层:提供了一些支持上层的基础功能,最后是引导层:就是提供配置信息。那么本篇文章的重点就是数据处理层和框架支撑层的源码解析。
MyBatis的各个组件
先上图,从图来看,首先就是sqlSession 就是对外提供了一些课调用的方法,比如:selectList()等,然后就是把操作委派给了executor 做的,executor 执行器主要就是执行sql,维护两级别的缓存,然后就是StatementHandler 语句执行器,这个就是封装了jdbc statement,那么语句执行器去执行的时候,一定是要有sql 的,sql 来源就是configuration 全局配置对象,configuration 对象里面,又有一个属性对象叫做MappedStatement,它是由解析映射配置文件形成的map 集合,语句执行器真正要执行的sql 就是放在了MappedStatement 的SQLSource属性中,这里获取到的sql 还是配置文件中的sql 语句,是存在占位符的,所有要到下一层的parameterHandler 参数处理器,这里它还会用到TypeHandler 类型转换器,目的就是将sql 拼接完整,然后就是Statement 对象进行真正的sql 执行了,执行完整之后,如果有返回值的话,就会有一个返回结果集,然后就是经过resultSetHandler 结果集处理器,后面就是原来返回了。
MyBatis源码分析
大体介绍了架构和组件,我们现在就开始本篇的正题MyBatis源码的分析,首先可以将我的资源中的注释版源码下载下来,地址:myBatis源码(注释版)-Java文档类资源-CSDN下载
打开项目之后,我们可以先看到MyBatis 的测试类,首先我们看下通过xml配置的方式进行操作,看具体代码。第一个步是将配置文件通过类加载器,加载为字节输入流,然后构建sqlSessionFactory 工厂,再由工厂去获取具体的sqlSession,然后由sqlSession创建代理,再由代理调用具体的查询语句并返回接口集,然后就结束了。很简单的逻辑,我们要重点要看的是它怎么创建工厂的,代理又是怎么生成的,sql是怎么调用并返回结果集的,下面我们一步一步来。
@Test
public void test1() throws IOException {
// 1. 通过类加载器对配置文件进行加载,加载成了字节输入流,存到内存中 注意:配置文件并没有被解析
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
// 2. (1)解析了配置文件,封装configuration对象 (2)创建了DefaultSqlSessionFactory工厂对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// 3.问题:openSession()执行逻辑是什么?
// 3. (1)创建事务对象 (2)创建了执行器对象cachingExecutor (3)创建了DefaultSqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
// 4. 委派给Executor来执行,Executor执行时又会调用很多其他组件(参数设置、解析sql的获取,sql的执行、结果集的封装)
User user = sqlSession.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);
System.out.println(user);
System.out.println("MyBatis源码环境搭建成功....");
sqlSession.close();
}
sqlSessionFactory的创建,又叫封装Configuration
这里我们直接跟上面的代码内容,找到SqlSessionFactoryBuilder().build 方法,然后跟进就是下面代码。
可以看到首先是获取到XMLConfigBuilder 对象,然后就是XMLConfigBuilder 对象的parse 方法,我为什么说是parse 方法而不是build 方法呢,因为build 方法就只是一个调用DefaultSqlSessionFactory 类的有参构造方法而已,就是将Configuration 放入而已,那么这里其实就可以知道XMLConfigBuilder 对象中就是为了将Configuration 创建出来。
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// XMLConfigBuilder:用来解析XML配置文件
// 使用构建者模式:好处:降低耦合、分离复杂对象的创建
// 1.创建XPathParser解析器对象,根据inputStream解析成了document对象 2.创建全局配置对象Configuration对象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// parser.parse():使用XPATH解析XML配置文件,将配置文件封装到Configuration对象
// 返回DefaultSqlSessionFactory对象,该对象拥有Configuration对象(封装配置文件信息)
// parse():配置文件就解析完成了
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
那么我们可以继续看XMLConfigBuilder 的构建,这里是直接new 的有参构造,那么我们可以跟进去看下,代码如下。
这里其实就是再次构建了一个XPathParser 对象,这个对象的目的就是基于java的xpath 解析器,将inputStream 字节输入流解析成了配置对象,这里的解析代码我们就不看了,有兴趣的可以搜一下这都是apache 提供的一些基础东西了。
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
// XPathParser基于 Java XPath 解析器,用于解析 MyBatis中的配置文件
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
我们继续跟到this 里面,这里就是本身的构造方法,代码如下。其实也没有什么东西,真正的解析并没有放在这里,super 也只是构建了一个基础的configuration,就是为了注册一些别名,为了后面的真正解析的时候使用,这快的代码太多我就截图展示,有兴趣可以自己跟一下。
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
// 创建Configuration对象,并通过TypeAliasRegistry注册一些Mybatis内部相关类的别名
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
解析配置文件,并封装到Configuration不同属性
然后我们回到SqlSessionFactory 中,当XMLConfigBuilder 对象构建完成之后,跟着的就是parse 方法,这里我们就算不跟代码,现在已知道是就是为了生成一个configuration 对象,我们现在要看的也就是怎么生成的代码,先上代码。
这里首先会判断parsed,这是一个私有的全局布尔值对象,只有先走了XMLConfigBuilder 的构造才会赋值为false。然后跟着的就是parseConfiguration 方法,这里其实就是根据一个一个的标签解析出不同的对象,注意这里的标签应该都是configuration的下级标签,解析到不同对象后再存入configuration 对象的不同属性中,这里逻辑相似,我们就不一个一个看看,我们选两个看下。
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// parser.evalNode("/configuration"):通过XPATH解析器,解析configuration根节点
// 从configuration根节点开始解析,最终将解析出的内容封装到Configuration对象中
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
// 解析</properties>标签
propertiesElement(root.evalNode("properties"));
// 解析</settings>标签
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// 解析</typeAliases>标签
typeAliasesElement(root.evalNode("typeAliases"));
// 解析</plugins>标签
pluginElement(root.evalNode("plugins"));
// 解析</objectFactory>标签
objectFactoryElement(root.evalNode("objectFactory"));
// 解析</objectWrapperFactory>标签
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析</reflectorFactory>标签
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 解析</environments>标签
environmentsElement(root.evalNode("environments"));
// 解析</databaseIdProvider>标签
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析</typeHandlers>标签
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析</mappers>标签 加载映射文件流程主入口
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
首先是看数据源的配置标签environments,这里我们要找的方法也就是environmentsElement,先上代码。
这个还是比较简单好理解,相对dubbo来说,看这个太幸福了。说正题,这个就是将标签一级一级解析,找到每一级标签的属性,然后构建不同的对象,比如事务的事务工厂对象TransactionFactory,还有数据源工厂对象DataSourceFactory,然后这些对象会被放入Environment 对象中,这里使用的是Builder 模式,最后可以看到就是将封装好的对象存入了全局配置对象configuration 对象中。
private void environmentsElement(XNode context) throws Exception {
// 如果定义了environments
if (context != null) {
// 方法参数如何没有传递 environment,则解析sqlMapConfig.xml中的
if (environment == null) {
// <environments default="development" >
environment = context.getStringAttribute("default");
}
//遍历解析environment节点
for (XNode child : context.getChildren()) {
//获得id属性值
String id = child.getStringAttribute("id");
//判断id和environment值是否相等
if (isSpecifiedEnvironment(id)) {
/*
<transactionManager type="JDBC" />
创建事务工厂对象
*/
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
/*
<dataSource type="POOLED">
创建数据源对象
*/
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
// 将Environment存到configuraion中
configuration.setEnvironment(environmentBuilder.build());
break;
}
}
}
}
解析sql配置文件,并封装到Configuration的mapperRegistry集合
这里还需要看一个关于sql 的配置文件解析,这里我们可以找mappers 标签的解析方法mapperElement 方法,先上代码。
从代码中我们就可以知道这个标签是有两种不同的引入方式,一是:package,二是:其余方式,但是不管是哪种方式都是在configuration 对象的mapperRegistry 这个Map 集合对象属性中存入值。这里我们直接说循环里面的东西,首选判断当前标签是否是pakage 标签,如果是,则先获取标签的name 属性值,也就是引入的包名,然后以这个为key 值存入集合中,至于value 就是Object.class,这里所有存值value 目前都还只是Object.class,唯一的区别是key 的不同。
下面说下当标签不为package 时,那么可以看到是获取其3个属性,resource、url、class,而且这三个属性只能配置一个属性有值,这里有意思的是resource、url 两种情况,又会走一遍解析文件的过程,也就是意味着resource、url 这两种情况都是引入其余的配置文件,而class 就是直接得到其class 对象,然后还是都存入了configuration 对象的mapperRegistry 集合属性中。注意这里封装的是每一个配置文件。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
// 获取<mappers>标签的子标签
for (XNode child : parent.getChildren()) {
// <package>子标签
if ("package".equals(child.getName())) {
// 获取mapper接口和mapper映射文件对应的package包名
String mapperPackage = child.getStringAttribute("name");
// 将包下所有的mapper接口以及它的代理工厂对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
configuration.addMappers(mapperPackage);
} else {// <mapper>子标签
// 获取<mapper>子标签的resource属性
String resource = child.getStringAttribute("resource");
// 获取<mapper>子标签的url属性
String url = child.getStringAttribute("url");
// 获取<mapper>子标签的class属性
String mapperClass = child.getStringAttribute("class");
// 它们是互斥的
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
// 专门用来解析mapper映射文件
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 通过XMLMapperBuilder解析mapper映射文件
mapperParser.parse();
}
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
try(InputStream inputStream = Resources.getUrlAsStream(url)){
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
// 通过XMLMapperBuilder解析mapper映射文件
mapperParser.parse();
}
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
// 将指定mapper接口以及它的代理对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
我们这在具体说一下resource 有值的情况下是如果解析sql 配置文件的,首先我们可以跳过的是InputStream 和XMLMapperBuilder 的获取,这个获取的流程和上面的配置文件转换流程是一样的,我们指点看parse 方法,代码如下。
首先要确认的是所有的标签都是在mapper 标签下的,首先要判断的是这个文件是否已经解析过了,解析过了的话,就不再解析,然后就是去解析mapper 标签下的所有标签内容,跟进configurationElement 方法,代码也在下面了,可以看到跟上面的配置文件解析差不多,都是逐个逐个的标签进行解析,不过这里需要注意一点的就是它为命名空间,也就是mapper 标签的namespace 属性进行了存值。
这里的解析标签我们就不一个一个看了,我们重点看下buildStatementFromContext 方法,这就是解析增删改查的sql 位置了,这里代码太多,我就不一个一个展示了我下面截图看重点。
public void parse() {
// mapper映射文件是否已经加载过
if (!configuration.isResourceLoaded(resource)) {
// 从映射文件中的<mapper>根标签开始解析,直到完整的解析完毕
configurationElement(parser.evalNode("/mapper"));
// 标记已经解析
configuration.addLoadedResource(resource);
// 为命名空间绑定映射
bindMapperForNamespace();
}
// 解析ResultMap
parsePendingResultMaps();
// 解析缓存
parsePendingCacheRefs();
// 解析statement
parsePendingStatements();
}
/**
* 解析映射文件
* @param context 映射文件根节点<mapper>对应的XNode
*/
private void configurationElement(XNode context) {
try {
// 获取<mapper>标签的namespace值,也就是命名空间
String namespace = context.getStringAttribute("namespace");
// 命名空间不能为空
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// MapperBuilderAssistant:构建MappedStatement对象的构建助手,设置当前的命名空间为namespace的值
builderAssistant.setCurrentNamespace(namespace);
// 解析<cache-ref>子标签
cacheRefElement(context.evalNode("cache-ref"));
// 解析<cache>子标签
cacheElement(context.evalNode("cache"));
// 解析<parameterMap>子标签
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析<resultMap>子标签
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析<sql>子标签,也就是SQL片段
sqlElement(context.evalNodes("/mapper/sql"));
// 解析<select>\<insert>\<update>\<delete>子标签
// 将cache对象封装到MappedStatement中
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
构建sql对象,替换sql占位符,保存对应对象信息
我们最后是可以跟到statementParser.parseStatementNode() 的调用,这里我们可以看到解析出的入参类型,并生成对应的java对象,这段代码之前还要注意一个SqlCommandType 对象的构建,我们知道mybatis 的底层就是jdbc,而jdbc 其实大体就分两种操作executeQuery和executeUpdate,这个对象就是为了区分当前操作是属于哪种情况的。
然后下面就是跟入参类型的解析差不多,就不多说了,然后可以跟到SqlSource 对象的生成,这个是因为jdbc 可识别的占位符是“?”号,而我们当前的sql 是存在#{}或者${}这种情况的,那么SqlSource 对象就是为了将sql 中的#{}或者${}对应的值进行记录,并且将其替换为“?”号占位符。这里我们可以下面跟一下代码,代码如下。
在这里展示吧,跟进createSqlSource 方法,找到对应的XMLLanguageDriver 实现,其内容就是先构建出解析器,然后再解析动态sql,这里的解析器其实就包含了sql 中出现的if、foreach等等,其实可以发现mybatis 的工厂模式已经出现了很多次了。
我们重点看解析动态sql 的内容,跟进来我们可以看到如下代码,${}和#{}对应的值分别存入了TextSqlNode 和StaticTextSqlNode 对象中,然后下面判断的isDynamic 布尔值,注意这个值只要是存在${}情况,那么就是true,就算是#{}和${}共存,那也是true。这里我们就跟一下RawSqlSource 方法,跟进去我们看的是 方法的调用,这里面会使用到一个叫做ParameterMappingTokenHandler 的解析器对象,用于解析sql,然后将sql中的#{}替换为“?”号,最后解析出来返回的就是一个StaticSqlSource 对象,而这个对象中存在一个获取BoundSql 对象的getBoundSql 方法,BoundSql 对象中又存在getSql 的方法可以直接获取sql。所以也就是我们之前分析的使用BoundSql 对象来获取使用的sql 给到语句执行器。
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
// 初始化了动态SQL标签处理器
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
// 解析动态SQL
return builder.parseScriptNode();
}
/**
* 解析select\insert\ update\delete标签中的SQL语句,最终将解析到的SqlNode封装到MixedSqlNode中的List集合中
* @return
*/
public SqlSource parseScriptNode() {
// ****将带有${}号的SQL信息封装到TextSqlNode
// ****将带有#{}号的SQL信息封装到StaticTextSqlNode
// ****将动态SQL标签中的SQL信息分别封装到不同的SqlNode中
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
// 如果SQL中包含${}和动态SQL语句,则将SqlNode封装到DynamicSqlSource
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
// 如果SQL中包含#{},则将SqlNode封装到RawSqlSource中,并指定parameterType
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
封装sql对象至configuration的mappedStatements集合
这里结束之后,我们可以回到XMLStatementBuilder 类中,下面的解析就没有什么要关注的了,大体都是一样的,当解析完成值,就会去构建一个MappedStatement 对象,也就是存入了configuration 的mappedStatements 集合属性中。注意,这里封装的是每一个配置文件对应的所有sql集合
SqlSessionFactory的openSession
那么上述就已经结束了sqlSessionFactory 的构建,然后我们回到测试方法,测试方法的下一步就是通过工厂获取到具体的sqlSession 对象,这里的openSession 方法其实现类就是DefaultSqlSessionFactory 类,openSession 方法也就是调用openSessionFromDataSource 它有三个入参,参数1:执行器类型、参数2:事务隔离级别、参数三:指定事务是否自动提交,然后我们可以跟进这个方法,上代码。注意这个执行器我们使用的是默认执行器ExecutorType.SIMPLE。
可以看到代码中首先获取的就是configuration 的environment 属性,这个就是我们之前解析environments 后获得的属性对象,然后获取这个对象中存入的事务工厂对象,然后在继续由事务工厂获取事务对象,然后就是执行器,最后由这些来构建出最后的sqlSession对象。注意这里的事务对象,在不配置的情况下,默认选择是交给容器做事务,可以看下代码中的默认事务对象是ManagedTransaction 对象这个对于提交和回滚的实现都是null的。
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 从configuration对象中获取environment对象
final Environment environment = configuration.getEnvironment();
// 获得事务工厂对象
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 构建事务对象
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 创建执行器对象
final Executor executor = configuration.newExecutor(tx, execType);
// 创建DefaultSqlSession对象
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
这里我们重点需要看的是Executor 执行器对象的构建,首先知道的是在执行器选择的情况,mybatis 提供了三个执行器,分别是BatchExecutor 批量执行器,ReuseExecutor 重复执行器,SimpleExecutor 简单执行器,我们这里选择的是SimpleExecutor 简单执行器,然后注意这里会判断一个cacheEnabled 这个表示的是否启动缓存,默认为true,也就是说在不设置的情况下,都会走进这一个逻辑,所以最后返回的对象就是CachingExecutor 代理执行器,这个里面是将原始执行器进行了封装,后面我们获取对应执行器的是就是通过CachingExecutor 来调用其delegate 属性获取。
那么这里最后就是构建出来一个DefaultSqlSession 对象,进行返回,这个对应也是SqlSession 对象的子类。
CachingExecutor执行器的调用
进过了上面的openSession 得到了sqlSession 对象,我们再次回到测试方法,这里就走了具体sqlSession 调用,从sqlSession.selectOne 方法开始,最后跟到DefaultSqlSession 的selectList 方法,这里我们上代码看,代码如下。
这里可以看到第一步就是获取到了上面封装的configuration 的mappedStatements 集合中对应的具体mappedStatement 也就是sql对象,注意之前我们没有说到这个集合的key 值是什么,这个key 就是命名空间,也就是mapper 标签的namespace 属性值 + select、update、delete、insert 标签的id 属性值。
当获取了对应的mappedStatement 对象后,直接是执行器调用query 方法,注意我们现在的执行器是CachingExecutor。
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
// 根据传入的statementId,获取MappedStatement对象
MappedStatement ms = configuration.getMappedStatement(statement);
// 调用执行器的查询方法
// wrapCollection(parameter)是用来装饰集合或者数组参数
return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
先上代码。可以看到,上面也说过,首先通过的是mappedStatement 的getBoundSql 方法获取到BoundSql,这里的方法虽然是get 开头,但是这里并不是直接取值,而是构建出了一个BoundSql 对象,其属性值来源就是sqlSource 的属性值。
然后我们直接跟到下一步生成缓存key 的createCacheKey 方法,这里具体的代码我们可以不用看,但是要知道它是由什么来生成的,这个是调用了之前封装到CachingExecutor 中的SimpleExecutor 执行器的createCacheKey 方法,其入参是mappedStatement 对象、parameterObject 传入参数对象、分页对象、BoundSql sql对象。这个面试的时候可以能会问到。
//第一步
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取绑定的SQL语句,比如 "SELECT * FROM user WHERE id = ? "
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 生成缓存Key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
接下来就是调用query 方法,先看代码。
首先需要从二级缓存中查询数据,通过mappedStatement 对象来获得缓存对象,这里只要在sql 配置文件中开启了二级缓存,那么这里就能获取到二级缓存对象,往下走如果在sql 配置中配置了flushCache 为true,那么就需要先刷新二级缓存,然后再去二级缓存中查询数据,这里二级缓存对象是TransactionalCacheManager 对象,这是Cache 对象的一个包装对象,也就是二级缓存对象。如果能获取到值,那么直接返回,反之再去查询一级缓存,这里的一级缓存就是调用SimpleExecutor 的query 方法,当查询成功之后再将查询内容放置二级缓存中,注意这里的key 就是上面封装的CacheKey,还有目前只是存入了一个map 集合中,只有真正的提交才会进行缓存。
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 获取二级缓存
Cache cache = ms.getCache();
if (cache != null) {
// 刷新二级缓存 (存在缓存且flushCache为true时)
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 从二级缓存中查询数据
List<E> list = (List<E>) tcm.getObject(cache, key);
// 如果二级缓存中没有查询到数据,则查询一级缓存及数据库
if (list == null) {
// 委托给BaseExecutor执行
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 将查询结果 要存到二级缓存中(注意:此处只是存到map集合中,没有真正存到二级缓存中)
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 委托给BaseExecutor执行
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
我们接着跟到delegate.query 方法中,这里调用的SimpleExecutor 的父类BaseExecutor,我们直接找到具体代码,这里个方法的实现代码太多了,我就截图展示了。
首先就是一级缓存查询,这里我们可以看下一级缓存的数据结构,点进去我们可以看到就是用过map 集合的get 方法去获取对应值,而这个集合就是PerpetualCache 类cache 属性,同样二级缓存也是这个数据结构。二级缓存最后调用到了LruCache 类的getObject 方法,最后还是调用delegate.getObject 方法,跟一级缓存的取值一致。
接下来如果在这里能获取到一级缓存那么就直接处理输出结果,如果没有则还是去数据库中查询,也就是queryFromDatabase 方法的调用。这个方法也很简单,就是用doQuery 方法进行查询数据库,然后将查询到的数据放入缓存,最会返回查询到的数据即可。
我们这里要跟踪的是doQuery 方法,直接可以跟到SimpleExecutor 的doQuery 方法,代码如下。
这里可以分为4步,第一步:获取配置实例,也就是从MappedStatement 获取的;第二步就是构建一个statementHandler 实例,也就是我们上面流程图中说的语句执行器,第三步准备处理器,主要包括创建statement 以及动态配置参数的设置等,第四步就是真正的数据库操作调用。
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
// 1. 获取配置实例
Configuration configuration = ms.getConfiguration();
// 2. new一个StatementHandler实例
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 3. 准备处理器,主要包括创建statement以及动态参数的设置
stmt = prepareStatement(handler, ms.getStatementLog());
// 4. 执行真正的数据库操作调用
return handler.query(stmt, resultHandler);
} finally {
// 5. 关闭statement
closeStatement(stmt);
}
}
StatementHandler语句执行的构建
这里我们就要先一个概念了,如下图,关于statementHandler 语句处理器的不同实现类解析。这里了解了之后,我们再看上面代码中的语句执行器的构建,跟进代码,代码如下。
这里获取的是一个 RoutingStatementHandler 路由执行器,但是这个路由执行的构造方法里面可以看到,这里是通过传入不同的参数,选择出对应的不同执行器,这里我们现在debug 进来构建的是一个PreparedStatementHandler 预编译执行器,也就是说在没有配置的情况下,我们默认是预编译执行器,注意这里是可以在sql 配置文件中进行配置的,有一些情况是不需要预编译的。
这里的还有一个插件机制,这里我们等下再介绍,先有个印象。
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 创建路由功能的StatementHandler,根据MappedStatement中的StatementType
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
// 插件机制:对核心对象进行拦截
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
再回到SimpleExecutor 的doQuery 方法,在语句执行器构建成功之后,我们需要对其参数的设置,我们可以从prepareStatement 方法中的handler.parameterize 方法调用,可以跟到PreparedStatementHandler 类的parameterHandler.setParameters 方法调用,这里注意我们已经到了流程图中的parameterHandler 参数处理器层了,最后跟到DefaultParameterHandler 类的setParameters 方法,这里代码太多,就只展示重点,其余的可以自己跟代码看下。
首先获取到参数映射,然后循环迭代,去封装每一个参数,不同类型转换是通过TypeHandler 也就是流程图中的类型转换器去做的,最后将每一个参数设置到预编译对象中。
PreparedStatement对象执行sql和结果集的处理
当预编译对象封装好之后,我们再次回到SimpleExecutor 的doQuery 方法,进行下一步,也就是handler.query 方法的调用,我们这里可以跟到PreparedStatementHandler 的query 方法,这里就可以看到其实还是java jdbc的PreparedStatement 对象在执行sql,我们重点看下结果集的处理。
这里因为代码太多了,我也就图片展示先关代码,具体的还是要请大家自己跟一下,我这里就提供一个流程的思路。
首先我们先考虑如果自己做结果集的封装需要怎么做?那么首先需要拿到返回的结果集、然后获取对应的映射关系,最后通过映射关系和结果接封装相应的对象。我们看下mybatis 的源码做法。继上面的内容我们跟进resultSetHandler.handleResultSets 方法,找到DefaultResultSetHandler 类的handleResultSets 方法。
首先获取结果集,并封装成一个wrapper 对象。
然后获取相对应的映射关系,从mappedStatement 对象中获取,并循环迭代每一个映射进行封装处理,处理方法为handleResultSet 方法,
跟进方法,首先是实例化一个DefaultResultHandler 对象,然后是对结果集进行映射,转换的结果存入defaultResultHandler,然后将转换的结果集放入multipleResults 集合中。映射具体我们就不看了,大体就是通过映射关系,获取到java 对象的实例,然后通过set 方法将结果集的数据进行存入,然后返回对象即可。这里也还会用到之前说过的TypeHandler 类型转换器对象,进行类型的转换。
总结:上述基本上就是传统调用的全部过程了,总的来说其实就是跟着架构图在走,其中最重要的就是configuration 全局配置对象,基本上所有的配置信息都存在这个对象中,还有就是一、二级缓存的处理,其缓存对象就是PerpetualCache 类cache 属性,就是有一个常量的map集合。其余的就是结果集的处理比较复杂一点,这里还是得自己跟一下代码。就说这么多,看过文章有不会的,或者说我讲解的比较模糊,可以留言给我,我看到了会详细解答,有大佬看见我说的不对,也给我跟我说,我会及时改正。下篇我们再说代理模式下的调用。