源码分析Mybatis MappedStatement的创建流程

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 源码分析Mybatis MappedStatement的创建流程

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


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

image.png

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


  1. SqlSessionFactory#mapperLocations,指定xml文件的配置路径。
  2. SqlSessionFactory#configLocation,指定mybaits的配置文件,该配置文件也可以配置mapper.xml的配置路径信息。
  3. MapperScannerConfigurer,扫描Mapper的java类(DAO)。


我们已经详细介绍了Mybatis Mapper对象的扫描与构建,那接下来我们将重点介绍MaperProxy与mapper.xml文件是如何建立关联关系的。


根据上面的罗列以及上文的讲述xml映射文件与Mapper建立联系的入口有三:


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


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


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


image.png


buildSqlSessionFactory


1if (xmlConfigBuilder != null) {  // XMLConfigBuilder   // @1
 2      try {
 3        xmlConfigBuilder.parse();
 4
 5        if (logger.isDebugEnabled()) {
 6          logger.debug("Parsed configuration file: '" + this.configLocation + "'");
 7        }
 8      } catch (Exception ex) {
 9        throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
10      } finally {
11        ErrorContext.instance().reset();
12      }
13    }
14
15if (!isEmpty(this.mapperLocations)) {   // @2
16      for (Resource mapperLocation : this.mapperLocations) {
17        if (mapperLocation == null) {
18          continue;
19        }
20
21        try {
22          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
23              configuration, mapperLocation.toString(), configuration.getSqlFragments());
24          xmlMapperBuilder.parse();
25        } catch (Exception e) {
26          throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
27        } finally {
28          ErrorContext.instance().reset();
29        }
30
31        if (logger.isDebugEnabled()) {
32          logger.debug("Parsed mapper file: '" + mapperLocation + "'");
33        }
34      }
35    } else {
36      if (logger.isDebugEnabled()) {
37        logger.debug("Property 'mapperLocations' was not specified or no matching resources found");
38      }
39    }

上文有两个入口:


代码@1:处理configLocation属性。


代码@2:处理mapperLocations属性。


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


XMLConfigBuilder


我们直接查看其parseConfiguration方法。


1private void parseConfiguration(XNode root) {
 2    try {
 3      propertiesElement(root.evalNode("properties")); //issue #117 read properties first
 4      typeAliasesElement(root.evalNode("typeAliases"));
 5      pluginElement(root.evalNode("plugins"));
 6      objectFactoryElement(root.evalNode("objectFactory"));
 7      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
 8      settingsElement(root.evalNode("settings"));
 9      environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
10      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
11      typeHandlerElement(root.evalNode("typeHandlers"));
12      mapperElement(root.evalNode("mappers"));   // @1
13    } catch (Exception e) {
14      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
15    }
16  }

重点关注mapperElement,从名称与参数即可以看出,该方法主要是处理中mappers的定义,即mapper sql语句的解析与处理。如果使用过Mapper的人应该不难知道,我们使用mapper节点,通过resource标签定义具体xml文件的位置。


XMLConfigBuilder#mapperElement


1private void mapperElement(XNode parent) throws Exception {
 2    if (parent != null) {
 3      for (XNode child : parent.getChildren()) {
 4        if ("package".equals(child.getName())) {
 5          String mapperPackage = child.getStringAttribute("name");
 6          configuration.addMappers(mapperPackage);
 7        } else {
 8          String resource = child.getStringAttribute("resource");
 9          String url = child.getStringAttribute("url");
10          String mapperClass = child.getStringAttribute("class");
11          if (resource != null && url == null && mapperClass == null) {
12            ErrorContext.instance().resource(resource);
13            InputStream inputStream = Resources.getResourceAsStream(resource);
14            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());    // @1
15            mapperParser.parse();
16          } else if (resource == null && url != null && mapperClass == null) {
17            ErrorContext.instance().resource(url);
18            InputStream inputStream = Resources.getUrlAsStream(url);
19            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
20            mapperParser.parse();
21          } else if (resource == null && url == null && mapperClass != null) {
22            Class<?> mapperInterface = Resources.classForName(mapperClass);
23            configuration.addMapper(mapperInterface);
24          } else {
25            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
26          }
27        }
28      }
29    }
30  }


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


XMLMapperBuilder


1XMLMapperBuilder#parse
 2public void parse() {
 3    if (!configuration.isResourceLoaded(resource)) {     // @1
 4      configurationElement(parser.evalNode("/mapper"));
 5      configuration.addLoadedResource(resource);
 6      bindMapperForNamespace();
 7    }
 8
 9    parsePendingResultMaps();                                    // @2
10    parsePendingChacheRefs();                                   // @3
11    parsePendingStatements();                                     // @4
12  }

代码@1:如果该映射文件(*.Mapper.xml)文件未加载,则首先先加载,完成xml文件的解析,提取xml中与mybatis相关的数据,例如sql、resultMap等等。


代码@2:处理mybatis xml中ResultMap。


代码@3:处理mybatis缓存相关的配置。


代码@4:处理mybatis statment相关配置,这里就是本篇关注的,Sql语句如何与Mapper进行关联的核心实现。


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


XMLMapperBuilder


1private void parsePendingStatements() {
 2      Collection<XMLStatementBuilder> incompleteStatements = configuration.getIncompleteStatements();
 3      synchronized (incompleteStatements) {
 4          Iterator<XMLStatementBuilder> iter = incompleteStatements.iterator();    // @1
 5          while (iter.hasNext()) {
 6              try {
 7                  iter.next().parseStatementNode();   // @2
 8                  iter.remove();
 9              } catch (IncompleteElementException e) {
10                  // Statement is still missing a resource...
11              }
12          }
13      }
14  }

代码@1:遍历解析出来的所有SQL语句,用的是XMLStatementBuilder对象封装的,故接下来重点看一下代码@2,如果解析statmentNode。


XMLStatementBuilder


1public void parseStatementNode() {
 2    String id = context.getStringAttribute("id");                                                                  // @1 start
 3    String databaseId = context.getStringAttribute("databaseId");
 4
 5    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) return;
 6
 7    Integer fetchSize = context.getIntAttribute("fetchSize");
 8    Integer timeout = context.getIntAttribute("timeout");
 9    String parameterMap = context.getStringAttribute("parameterMap");
10    String parameterType = context.getStringAttribute("parameterType");
11    Class<?> parameterTypeClass = resolveClass(parameterType);
12    String resultMap = context.getStringAttribute("resultMap");
13    String resultType = context.getStringAttribute("resultType");
14    String lang = context.getStringAttribute("lang");
15    LanguageDriver langDriver = getLanguageDriver(lang);
16
17    Class<?> resultTypeClass = resolveClass(resultType);
18    String resultSetType = context.getStringAttribute("resultSetType");
19    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
20    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
21
22    String nodeName = context.getNode().getNodeName();
23    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
24    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
25    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
26    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
27    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
28
29    // Include Fragments before parsing
30    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
31    includeParser.applyIncludes(context.getNode());
32
33    // Parse selectKey after includes and remove them.
34    processSelectKeyNodes(id, parameterTypeClass, langDriver);             // @1 end
35
36    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
37    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);                // @2
38    String resultSets = context.getStringAttribute("resultSets");
39    String keyProperty = context.getStringAttribute("keyProperty");
40    String keyColumn = context.getStringAttribute("keyColumn");
41    KeyGenerator keyGenerator;
42    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
43    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
44    if (configuration.hasKeyGenerator(keyStatementId)) {
45      keyGenerator = configuration.getKeyGenerator(keyStatementId);
46    } else {
47      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
48          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
49          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
50    }
51
52    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,                             // @3
53        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
54        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
55        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
56  }

这个方法有点长,其关注点主要有3个:


代码@1:构建基本属性,其实就是构建MappedStatement的属性,因为MappedStatement对象就是用来描述Mapper-SQL映射的对象。


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

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


Configuration#addMappedStatement


1public void addMappedStatement(MappedStatement ms) {
2   mappedStatements.put(ms.getId(), ms);
3}

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对象,这样就建立起联系了。


其对应的代码:

1// MapperMethod 构造器
 2public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
 3    this.command = new SqlCommand(config, mapperInterface, method);
 4    this.method = new MethodSignature(config, method);
 5}
 6
 7// SqlCommand 构造器
 8public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) throws BindingException {
 9      String statementName = mapperInterface.getName() + "." + method.getName();
10      MappedStatement ms = null;
11      if (configuration.hasStatement(statementName)) {
12        ms = configuration.getMappedStatement(statementName);
13      } else if (!mapperInterface.equals(method.getDeclaringClass().getName())) { // issue #35
14        String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
15        if (configuration.hasStatement(parentStatementName)) {
16          ms = configuration.getMappedStatement(parentStatementName);
17        }
18      }
19      if (ms == null) {
20        throw new BindingException("Invalid bound statement (not found): " + statementName);
21      }
22      name = ms.getId();
23      type = ms.getSqlCommandType();
24      if (type == SqlCommandType.UNKNOWN) {
25        throw new BindingException("Unknown execution method for: " + name);
26      }
27    }

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

e5c0891220349cea50cc1e80506f0417.jpg

本文的讲解就到此结束了,下文将介绍Mybaits与Sharding-Jdbc整合时,SQL语句的执行过程。

相关文章
|
5月前
|
SQL Java 数据库连接
Mybatis源码分析系列之第三篇:Mybatis的操作类型对象
Mybatis源码分析系列之第三篇:Mybatis的操作类型对象
|
5月前
|
SQL XML Java
MyBatis初探:揭示初始化阶段的核心流程与内部机制
MyBatis初探:揭示初始化阶段的核心流程与内部机制
54 2
MyBatis初探:揭示初始化阶段的核心流程与内部机制
|
5月前
|
Java 关系型数据库 数据库连接
MyBatis 执行流程分析
MyBatis 执行流程分析
47 2
|
5月前
|
SQL Java 数据库连接
MyBatis源码篇:mybatis拦截器源码分析
MyBatis源码篇:mybatis拦截器源码分析
|
5月前
|
缓存 Java 数据库连接
|
5月前
|
SQL Java 数据库连接
|
5月前
|
设计模式 SQL Java
Mybatis源码分析系列之第四篇:Mybatis中代理设计模型源码详解
Mybatis源码分析系列之第四篇:Mybatis中代理设计模型源码详解
|
5月前
|
存储 SQL Java
Mybatis源码分析系列之第二篇:Mybatis的数据存储对象
Mybatis源码分析系列之第二篇:Mybatis的数据存储对象
|
5月前
|
Java 关系型数据库 数据库连接
Mybatis源码分析系列之第一篇:回顾一下MyBatis的使用
Mybatis源码分析系列之第一篇:回顾一下MyBatis的使用
|
21天前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
下一篇
无影云桌面