上篇文章描述研究源码所用的demo,本篇文章开始正式追踪源码,在开看前,再次确认下我们要研究的问题:
1、mybatis如何找到存储sql的xml文件
2、mybatis如何解析xml中sql相关标签并拼装成完整的sql语句
3、mybatis如何将xml中sql与接口中方法绑定
4、mybatis如何将sql发送出去并获得结果
另外,mybatis一般有两种xml配置,一种是mybatis-config.xml这种配置管理mybatis全局属性的文件,一种是person.xml配置接口类方法对应sql的文件,为了容易区分,再后续的文章中,前者有时也称为配置文件,后者有时则称为mapper文件或mapper.xml文件或包含sql的文件。
1)首先我们指定mybatis的配置文件路径,根据配置文件创建SqlSessionFactory。
build方法中有两个比较重要,一个是根据指定的mybatis-config.xml文件流生成xml解析器,另一个则是调用解析器的parse方法解析xml文件并生成配置对象。生成xml解析器的过程不属于我们关注的重点,所以这里不深入阅读,这里我们着重看下parse方法。
parse方法中,首先会去解析configuration标签中的内容,生成XNode对象(存储xml文件的数据结构),然后在parseConfiguration进行处理,我们先断点看下XNode对象中存储的信息。
可以看到mybatis-config.xml的文件信息全存储到了XNode中,接下来我们再来看看parseConfiguration方法中的处理流程。
在 parseConfiguration中会依次处理properties、settings、typeAlias .... mappers等xml标签,而我们编辑sql的xml文件便是在mapppers标签中指定的,如下:
root.evalNode("mappers")首先会获取mappers标签的内容,随后通过mapperElement方法进行处理,所以接下来深入mapperElement方法来看看mappers标签的处理过程(暂时打断下,我们的第一个问题“mybatis如何找到存储sql的xml文件”至此已经解决)。
这个方法中需要讲解的内容比较多,为了更好的讲解源码,这里不再截图,直接在源码中进行注释说明:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//如果不写mapper.xml文件(含有sql的文件),可以直接在dao方法上添加@Select,@Insert等注解。此时只需要在配置文件的mappers标签中增加package属性进行。此时mybatis会扫描指定的包路径下的所有接口类,并查找其中的注解,解析后放入类型为Map<Class<?>, MapperProxyFactory<?>> 属性名为knownMappers的集合中。
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
//分别读取mappers标签中resource、url、class的属性值
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
//resouce不为空,url和class为空的场景
if (resource != null && url == null && mapperClass == null) {
//初始化创建一个错误信息存储对象,该对象通过threadlocal存储,可以做到线程独立性
ErrorContext.instance().resource(resource);
//获取resource指定路径文件的流
InputStream inputStream = Resources.getResourceAsStream(resource);
//创建mapper解析器(与前面mybatis-config.xml配置文件解析器XMLConfigBuilder类似,要区分开)
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
//解析包含sql的xml文件
mapperParser.parse();
//url不为空,resouce和class为空的场景
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
//class不为空,url和resouce为空的场景
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
通过上面的源码可以知道,mybatis不仅可以通过xml文件配置sql语句,直接通过注解也可以设置sql语句(注解中其实还会再扫描确认一次是否有含sql的xml文件)。因为我们的demo是通过xml编辑的sql,且mappers标签中只设置了resource属性,所以这里着重看下resouce不为空,url和class为空的代码处理逻辑:
public void parse() {
//如果resource指定的xml文件未被加载过则进行处理
if (!configuration.isResourceLoaded(resource)) {
//获取mapper标签内容并通过configurationElement方法处理
configurationElement(parser.evalNode("/mapper"));
//将该resource放入set集合,该集合统计已经加载过的xml文件,避免重复加载
configuration.addLoadedResource(resource);
//将resource指定的xml文件和命名空间绑定,表明该文件处理过。并通过命名空间将加载接口类,将接口信息存入到Configuration中的mapperRegistry属性中。
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
比较重要的主要有configurationElement、bindMapperForNamespace等方法,接下来我们再进入看一下:
private void configurationElement(XNode context) {
try {
//获取标签中namespace属性值,namespace为空间命名,非接口编程时可以随便写。在通过接口编程时,该namespace则为对应接口的包路径。
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//暂存namespace空间命名信息,再后面会根据命名空间加载接口类,并存储命名空间防止包含sql的映射文件被多次加载(前面已经暂存过resource指定的路径防止多次加载,这里是另一重保险,具体就不细讲了)
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标签
sqlElement(context.evalNodes("/mapper/sql"));
//处理select|insert|update|delete标签
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);
}
}
configurationElement方法中会对不同的标签进行不同的操作处理,因为demo中只有一个select标签,所以我们直接深入buildStatementFromContext方法查看进一步的处理:
// select|insert|update|delete 标签可能有多个,所以使用List参数
private void buildStatementFromContext(List<XNode> list) {
//根据是否配置了额外的数据库,传入不同的参数
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
//遍历 select|insert|update|delete 所有的标签内容
for (XNode context : list) {
//创建sql语句解析器
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
//解析sql
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
和前面解析配置文件和mapper文件类似,这里也是通过解析器解析select|insert|update|delete标签中的sql内容,这里我们再接着看下parseStatementNode方法:
public void parseStatementNode() {
// 获取sql语句对应标签中的各种属性 (这些是xml解析的重点,但不是回答我们问题的重点,所以下面关于一些结果类型的解析并未深入查看)
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
// 获取结果类型
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// 加载结果类
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
// 获取当前标签节点的名称
String nodeName = context.getNode().getNodeName();
//根据标签节点的名称判断当前sql的类型
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
//如果当前节点描述的是select查询语句,则置位isSelect属性
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//根据是否为select查询语句,获取flushCache和useCache的标识位
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//select查询默认会开启缓存,当然这里仅仅是开启mybatis的自带缓存(session级别或namesapce级别,且缓存有限),如果想做到分布式缓存,最好是外接使用第三方缓存中间件
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// 创建include标签解析器
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
// 初始化解析标签中的include子标签,解析完成后移除include标签
includeParser.applyIncludes(context.getNode());
// 解析标签中的selectKey子标签,解析完成后移除selectKey标签
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 创建一个包含sql语句的源对象,源对象包含提取的sql信息以及参数解析器等
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
//分别获取标签中resultSets keyProperty keyColumn三个属性
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
//主键生成器,一般insert语句用的到
KeyGenerator keyGenerator;
// 解析xml namespace属性,namespace加id构成了sql的唯一标识信息(也是我们解答第三题的关键)
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
//将这条sql标签对应的所有信息存入到MappedStatement对象中(建造者模式),并放入到一个map集合中,该map的key即为“包名.类型.标签中id属性名”
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
这个方法结束后,sql被提取了出来,并且该条sql对应的所有信息存储成一个以包名.类型.标签中id属性名为key的键值对,至此sql如何被解析出来我们基本已经清楚,主要是在生成SqlSource的过程中提取出来的,后面我们会再接着看。再接着看之前我想再提醒大家注意一个和第三问相关的点,目前xml中提取的sql存储到一个map集合中,这个map的key则是namespace.标签中id属性名,请注意此时这个key实际也是包名.类名.接口名,因为id属性名一般都是定义为接口中的方法名,所以这个key可以直接定位到方法,即达到sql和方法绑定的目的(我们解答第三题的关键点之一)。demo中的源码情况如下所示:
具体sql和方法的绑定调用我们放在下一篇文章中讲,这里先提醒大家注意下。本文后续还是接着讲如何提取sql,下面我们深入到SqlSource的创建源码中去看。
这里可以看到仍然是老套路创建“XML脚本解析器”,然后通过解析器解析节点,我们着重看下解析方法。
终于看到我们朝思暮想的sql提取方法了,这里会创建一个DynamicContext对象,通过该对象提取XML中的sql语句,这里因为源码不太好深入进去看,而且优点偏向另一个领域的知识,所以sql的获取就追踪到这。有兴趣的可以再深入看看。