源码分析MybatisMappedStatement的创建流程

编程

上文源码分析Mybatis MapperProxy创建流程重点阐述 MapperProxy 的创建流程,但并没有介绍 *.Mapper.java(UserMapper.java) 是如何与 *Mapper.xml 文件中的 SQL 语句是如何建立关联的。本文将重点接开这个谜团。

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

1、上节回顾

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

  1. SqlSessionFactory#mapperLocations,指定xml文件的配置路径。
  2. SqlSessionFactory#configLocation,指定mybaits的配置文件,该配置文件也可以配置mapper.xml的配置路径信息。
  3. 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](https://my.oschina.net/u/1198)

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](https://my.oschina.net/u/1198)

} 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() &amp;&amp; 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语句是怎样建立的关系了吗?为了让大家更清晰的了解上述过程,现给出其调用时序图:


>作者介绍:丁威,《RocketMQ技术内幕》作者,RocketMQ 社区布道师,公众号:中间件兴趣圈 维护者,目前已陆续发表源码分析Java集合、Java 并发包(JUC)、Netty、Mycat、Dubbo、RocketMQ、Mybatis等源码专栏。

以上是 源码分析MybatisMappedStatement的创建流程 的全部内容, 来源链接: utcz.com/z/510778.html

回到顶部