上文源码分析Mybatis MapperProxy创建流程
<https://yq.aliyun.com/articles/718482?spm=a2c4e.11155435.0.0.323233123lqEeh>
重点阐述MapperProxy的创建流程,但并没有介绍.Mapper.java(UserMapper.java)是如何与
Mapper.xml文件中的SQL语句是如何建立关联的。本文将重点接开这个谜团。

接下来重点从源码的角度分析Mybatis MappedStatement的创建流程。

@TOC <https://yq.aliyun.com/go/articleRenderRedirect?url=%23>

1、上节回顾

我们注意到这里有两三个与Mapper相关的配置:

* SqlSessionFactory#mapperLocations,指定xml文件的配置路径。
* SqlSessionFactory#configLocation,指定mybaits的配置文件,该配置文件也可以配置mapper.xml的配置路径信息。
* MapperScannerConfigurer,扫描Mapper的java类(DAO)。
我们已经详细介绍了Mybatis Mapper对象的扫描与构建,那接下来我们将重点介绍MaperProxy与mapper.xml文件是如何建立关联关系的。

根据上面的罗列以及上文的讲述,Mapper.xml与Mapper建立联系主要的入口有三:

1)MapperScannerConfigurer扫描Bean流程中,在调用MapperReigistry#addMapper时如果Mapper对应的映射文件(Mapper.xml)未加载到内存,会触发加载。
2)实例化SqlSessionFactory时,如果配置了mapperLocations。
3)示例化SqlSessionFactory时,如果配置了configLocation。


本节的行文思路:从SqlSessionFacotry的初始化开始讲起,因为mapperLocations、configLocation都是是SqlSessionFactory的属性。

温馨提示:下面开始从源码的角度对其进行介绍,大家可以先跳到文末看看其调用序列图。

2、SqlSessionFacotry
if (xmlConfigBuilder != null) { // XMLConfigBuilder // @1 try {
xmlConfigBuilder.parse(); if (logger.isDebugEnabled()) { logger.debug("Parsed
configuration file: '" + this.configLocation + "'"); } } catch (Exception ex) {
throw new NestedIOException("Failed to parse config resource: " +
this.configLocation, ex); } finally { ErrorContext.instance().reset(); } } if
(!isEmpty(this.mapperLocations)) { // @2 for (Resource mapperLocation :
this.mapperLocations) { if (mapperLocation == null) { continue; } try {
XMLMapperBuilder xmlMapperBuilder = new
XMLMapperBuilder(mapperLocation.getInputStream(), configuration,
mapperLocation.toString(), configuration.getSqlFragments());
xmlMapperBuilder.parse(); } catch (Exception e) { throw new
NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'",
e); } finally { ErrorContext.instance().reset(); } if (logger.isDebugEnabled())
{ logger.debug("Parsed mapper file: '" + mapperLocation + "'"); } } } else { if
(logger.isDebugEnabled()) { logger.debug("Property 'mapperLocations' was not
specified or no matching resources found"); } }
上文有两个入口:
代码@1:处理configLocation属性。
代码@2:处理mapperLocations属性。


我们先从XMLConfigBuilder#parse开始进行追踪。该方法主要是解析configLocation指定的配置路径,对其进行解析,具体调用parseConfiguration方法。

2.1 XMLConfigBuilder

我们直接查看其parseConfiguration方法。
private void parseConfiguration(XNode root) { try {
propertiesElement(root.evalNode("properties")); //issue #117 read properties
first typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
settingsElement(root.evalNode("settings"));
environmentsElement(root.evalNode("environments")); // read it after
objectFactory and objectWrapperFactory issue #631
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers")); // @1 } catch (Exception e) { throw
new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
} }
重点关注mapperElement,从名称与参数即可以看出,该方法主要是处理中mappers的定义,即mapper
sql语句的解析与处理。如果使用过Mapper的人应该不难知道,我们使用mapper节点,通过resource标签定义具体xml文件的位置。

2.1.1XMLConfigBuilder#mapperElement
private void mapperElement(XNode parent) throws Exception { if (parent !=
null) { for (XNode child : parent.getChildren()) { if
("package".equals(child.getName())) { String mapperPackage =
child.getStringAttribute("name"); configuration.addMappers(mapperPackage); }
else { String resource = child.getStringAttribute("resource"); String url =
child.getStringAttribute("url"); String mapperClass =
child.getStringAttribute("class"); if (resource != null && url == null &&
mapperClass == null) { ErrorContext.instance().resource(resource); InputStream
inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder
mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
configuration.getSqlFragments()); // @1 mapperParser.parse(); } 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(); } 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."); } } } } }

上面的代码比较简单,不难看出,解析出Mapper标签,解析出resource标签的属性,创建对应的文件流,通过构建XMLMapperBuilder来解析对应的mapper.xml文件。此时大家会惊讶的发现,在SqlSessionFacotry的初始化代码中,处理mapperLocations时就是通过构建XMLMapperBuilder来解析mapper文件,其实也不难理解,因为这是mybatis支持的两个地方可以使用mapper标签来定义mapper映射文件,具体解析代码当然是一样的逻辑。那我们解析来重点把目光投向XMLMapperBuilder。

2.2 XMLMapperBuilder
XMLMapperBuilder#parse public void parse() { if
(!configuration.isResourceLoaded(resource)) { // @1
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource); bindMapperForNamespace(); }
parsePendingResultMaps(); // @2 parsePendingChacheRefs(); // @3
parsePendingStatements(); // @4 }

代码@1:如果该映射文件(*.Mapper.xml)文件未加载,则首先先加载,完成xml文件的解析,提取xml中与mybatis相关的数据,例如sql、resultMap等等。
代码@2:处理mybatis xml中ResultMap。
代码@3:处理mybatis缓存相关的配置。
代码@4:处理mybatis statment相关配置,这里就是本篇关注的,Sql语句如何与Mapper进行关联的核心实现。

接下来我们重点探讨parsePendingStatements()方法,解析statement(对应SQL语句)。

2.2.1 XMLMapperBuilder#parsePendingStatements
private void parsePendingStatements() { Collection<XMLStatementBuilder>
incompleteStatements = configuration.getIncompleteStatements(); synchronized
(incompleteStatements) { Iterator<XMLStatementBuilder> iter =
incompleteStatements.iterator(); // @1 while (iter.hasNext()) { try {
iter.next().parseStatementNode(); // @2 iter.remove(); } catch
(IncompleteElementException e) { // Statement is still missing a resource... }
} } }
代码@1:遍历解析出来的所有SQL语句,用的是XMLStatementBuilder对象封装的,故接下来重点看一下代码@2,如果解析statmentNode。

2.2.2 XMLStatementBuilder#parseStatementNode
public void parseStatementNode() { String id =
context.getStringAttribute("id"); // @1 start 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(); SqlCommandType sqlCommandType =
SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); boolean isSelect
= sqlCommandType == SqlCommandType.SELECT; boolean flushCache =
context.getBooleanAttribute("flushCache", !isSelect); boolean useCache =
context.getBooleanAttribute("useCache", isSelect); boolean resultOrdered =
context.getBooleanAttribute("resultOrdered", false); // Include Fragments
before parsing XMLIncludeTransformer includeParser = new
XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode()); // Parse selectKey after
includes and remove them. processSelectKeyNodes(id, parameterTypeClass,
langDriver); // @1 end // Parse the SQL (pre: <selectKey> and <include> were
parsed and removed) SqlSource sqlSource =
langDriver.createSqlSource(configuration, context, parameterTypeClass); // @2
String resultSets = context.getStringAttribute("resultSets"); String
keyProperty = context.getStringAttribute("keyProperty"); String keyColumn =
context.getStringAttribute("keyColumn"); KeyGenerator keyGenerator; 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)) ? new Jdbc3KeyGenerator() : new
NoKeyGenerator(); } builderAssistant.addMappedStatement(id, sqlSource,
statementType, sqlCommandType, // @3 fetchSize, timeout, parameterMap,
parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache,
useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId,
langDriver, resultSets); }
这个方法有点长,其关注点主要有3个:
代码@1:构建基本属性,其实就是构建MappedStatement的属性,因为MappedStatement对象就是用来描述Mapper-SQL映射的对象。

代码@2:根据xml配置的内容,解析出实际的SQL语句,使用SqlSource对象来表示。


代码@3:使用MapperBuilderAssistant对象,根据准备好的属性,构建MappedStatement对象,最终将其存储在Configuration中。

2.2.3 Configuration#addMappedStatement
public void addMappedStatement(MappedStatement ms) {
mappedStatements.put(ms.getId(), ms); }
MappedStatement的id为:mapperInterface +
methodName,例如com.demo.dao.UserMapper.findUser。


即上述流程完成了xml的解析与初始化,对终极目标是创建MappedStatement对象,上一篇文章介绍了mapperInterface的初始化,最终会初始化为MapperProxy对象,那这两个对象如何关联起来呢?

从下文可知,MapperProxy与MappedStatement是在调用具Mapper方法时,可以根据mapperInterface.getName +
methodName构建出MappedStatement的id,然后就可以从Configuration的mappedStatements容器中根据id获取到对应的MappedStatement对象,这样就建立起联系了。

其对应的代码:
// MapperMethod 构造器 public MapperMethod(Class<?> mapperInterface, Method
method, Configuration config) { this.command = new SqlCommand(config,
mapperInterface, method); this.method = new MethodSignature(config, method); }
// SqlCommand 构造器 public SqlCommand(Configuration configuration, Class<?>
mapperInterface, Method method) throws BindingException { String statementName
= mapperInterface.getName() + "." + method.getName(); MappedStatement ms =
null; if (configuration.hasStatement(statementName)) { ms =
configuration.getMappedStatement(statementName); } else if
(!mapperInterface.equals(method.getDeclaringClass().getName())) { // issue #35
String parentStatementName = method.getDeclaringClass().getName() + "." +
method.getName(); if (configuration.hasStatement(parentStatementName)) { ms =
configuration.getMappedStatement(parentStatementName); } } if (ms == null) {
throw new BindingException("Invalid bound statement (not found): " +
statementName); } name = ms.getId(); type = ms.getSqlCommandType(); if (type ==
SqlCommandType.UNKNOWN) { throw new BindingException("Unknown execution method
for: " + name); } }

怎么样,从上面的源码分析中,大家是否已经了解MapperProxy与Xml中的SQL语句是怎样建立的关系了吗?为了让大家更清晰的了解上述过程,现给出其调用时序图:


原文发布时间为:2019-05-23
本文作者:丁威,《RocketMQ技术内幕》作者。
本文来自中间件兴趣圈
<https://yq.aliyun.com/go/articleRenderRedirect?url=https%3A%2F%2Fmp.weixin.qq.com%2Fs%2FNMjjH5wf6FxExp6iMXbV_g>
,了解相关信息可以关注中间件兴趣圈
<https://yq.aliyun.com/go/articleRenderRedirect?url=https%3A%2F%2Fmp.weixin.qq.com%2Fs%2FNMjjH5wf6FxExp6iMXbV_g>