上文源码分析Mybatis MapperProxy建立流程重点阐述MapperProxy的建立流程,但并无介绍*.Mapper.java(UserMapper.java)是如何与*Mapper.xml文件中的SQL语句是如何创建关联的。本文将重点接开这个谜团。java
接下来重点从源码的角度分析Mybatis MappedStatement的建立流程。node
咱们注意到这里有两三个与Mapper相关的配置:sql
咱们已经详细介绍了Mybatis Mapper对象的扫描与构建,那接下来咱们将重点介绍MaperProxy与mapper.xml文件是如何创建关联关系的。缓存
根据上面的罗列以及上文的讲述,Mapper.xml与Mapper创建联系主要的入口有三: 1)MapperScannerConfigurer扫描Bean流程中,在调用MapperReigistry#addMapper时若是Mapper对应的映射文件(Mapper.xml)未加载到内存,会触发加载。 2)实例化SqlSessionFactory时,若是配置了mapperLocations。 3)示例化SqlSessionFactory时,若是配置了configLocation。mybatis
本节的行文思路:从SqlSessionFacotry的初始化开始讲起,由于mapperLocations、configLocation都是是SqlSessionFactory的属性。并发
舒适提示:下面开始从源码的角度对其进行介绍,你们能够先跳到文末看看其调用序列图。app
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属性。ide
咱们先从XMLConfigBuilder#parse开始进行追踪。该方法主要是解析configLocation指定的配置路径,对其进行解析,具体调用parseConfiguration方法。源码分析
咱们直接查看其parseConfiguration方法。post
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文件的位置。
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。
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语句)。
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。
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中。
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语句是怎样创建的关系了吗?为了让你们更清晰的了解上述过程,现给出其调用时序图:
本文就介绍到这里了,下一篇将详细介绍:Mybatis执行SQL的4大基础组件详解。
做者介绍:《RocketMQ技术内幕》做者,维护公众号:中间件兴趣圈,目前主要发表了源码阅读java集合、JUC(java并发包)、Netty、ElasticJob、Mycat、Dubbo、RocketMQ、mybaits等系列源码。