上文源码分析Mybatis MapperProxy创建流程重点阐述MapperProxy的创建流程,但并没有介绍*.Mapper.java(UserMapper.java)是如何与*Mapper.xml文件中的SQL语句是如何建立关联的。本文将重点接开这个谜团。
接下来重点从源码的角度分析Mybatis MappedStatement的创建流程。
源码分析Mybatis MappedStatement的创建流程
1、上节回顾2、SqlSessionFacotry2.1 XMLConfigBuilder2.1.1XMLConfigBuilder#mapperElement
2.2 XMLMapperBuilder2.2.1 XMLMapperBuilder#parsePendingStatements2.2.2 XMLStatementBuilder#parseStatementNode2.2.3 Configuration#addMappedStatement
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
) {
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
)) {
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"));
typeAliasesElement(root
.evalNode("typeAliases"));
pluginElement(root
.evalNode("plugins"));
objectFactoryElement(root
.evalNode("objectFactory"));
objectWrapperFactoryElement(root
.evalNode("objectWrapperFactory"));
settingsElement(root
.evalNode("settings"));
environmentsElement(root
.evalNode("environments"));
databaseIdProviderElement(root
.evalNode("databaseIdProvider"));
typeHandlerElement(root
.evalNode("typeHandlers"));
mapperElement(root
.evalNode("mappers"));
} 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());
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
)) {
configurationElement(parser
.evalNode("/mapper"));
configuration
.addLoadedResource(resource
);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
代码@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();
while (iter
.hasNext()) {
try {
iter
.next().parseStatementNode();
iter
.remove();
} catch (IncompleteElementException e
) {
}
}
}
}
代码@1:遍历解析出来的所有SQL语句,用的是XMLStatementBuilder对象封装的,故接下来重点看一下代码@2,如果解析statmentNode。
2.2.2 XMLStatementBuilder#parseStatementNode
public void parseStatementNode() {
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();
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);
XMLIncludeTransformer includeParser
= new XMLIncludeTransformer(configuration
, builderAssistant
);
includeParser
.applyIncludes(context
.getNode());
processSelectKeyNodes(id
, parameterTypeClass
, langDriver
);
SqlSource sqlSource
= langDriver
.createSqlSource(configuration
, context
, parameterTypeClass
);
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
,
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对象,这样就建立起联系了。
其对应的代码:
public MapperMethod(Class
<?> mapperInterface
, Method method
, Configuration config
) {
this.command
= new SqlCommand(config
, mapperInterface
, method
);
this.method
= new MethodSignature(config
, method
);
}
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())) {
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语句是怎样建立的关系了吗?为了让大家更清晰的了解上述过程,现给出其调用时序图:
源码分析Mybatis系列大纲: 1、源码分析Mybatis MapperProxy创建流程 【已发布】 2、 源码分析Mybatis MappedStatement的创建流程 【已发布】 3、源码解析MyBatis Sharding-Jdbc SQL语句执行流程详解 4、源码分析Mybatis插件(Plugin)机制 5、源码分析Mybatis一二级缓存
欢迎加笔者微信号(dingwpmz),加群探讨,笔者优质专栏目录: 1、源码分析RocketMQ专栏(40篇+) 2、源码分析Sentinel专栏(12篇+) 3、源码分析Dubbo专栏(28篇+) 4、源码分析Mybatis专栏 5、源码分析Netty专栏(18篇+) 6、源码分析JUC专栏 7、源码分析Elasticjob专栏 8、Elasticsearch专栏(20篇+) 9、源码分析MyCat专栏
中间件兴趣圈
认证博客专家
RocketMQ
资深架构师
中间件兴爱好者
丁威,《RocketMQ技术内幕》作者、博客专家,原创公众号『中间件兴趣圈』维护者。目前就职于中通快递研发中心担任资深架构师,负责消息中间件与全链路压测的实施与落地。欢迎大家加我个人微信:dingwpmz,拉您入技术交流群,共同发展,抱团取暖。擅长JAVA编程,对主流中间件RocketMQ、Dubbo、ElasticJob、Netty、Sentienl、Mybatis、Mycat等中间件有深入研究。