MyBatis配置文件解析(一)——mappers

438 阅读7分钟

官方文档——XML映射文件:mybatis.org/mybatis-3/z…

前言

映射文件

映射文件(Mapper)用于配置SQL语句,字段映射关系等。映射文件中包含 <cache><cache-ref><resultMap><sql><select|insert|update|delete> 等二级节点。

mapper标签

该标签的作用是加载映射文件的,加载方式有:

  • 使用相对于类路径的资源引用
<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
</mappers>
  • 使用完全限定资源定位符(URL)
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
</mappers>
  • 使用映射器接口实现类的完全限定类名
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
</mappers>
  • 将包内的映射器接口实现全部注册为映射器
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

解析类型

映射文件的解析过程是配置文件解析过程的一部分。解析逻辑封装在 mapperElement() 方法中。此方法的主要逻辑是遍历 <mappers> 的子节点,并根据节点属性值判断通过何种方式加载映射文件。

//mapperElement(root.evalNode("mappers"));
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();
        } 
        
        // 使用完全限定资源定位符(URL)
        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.");
        }
      }
    }
  }
}

解析XML映射文件

XMLMapperBuilder 是用于解析XML映射文件,其中 parser() 方法为解析入口。 从 <mapper> 节点开始解析

public void parse() {
   //判断该映射文件是否已经被解析过
  if (!configuration.isResourceLoaded(resource)) {
    // 解析mapper节点
    configurationElement(parser.evalNode("/mapper"));
    // 添加映射文件路径到“已解析资源集合”中
    configuration.addLoadedResource(resource);
    // 通过命名空间绑定 Mapper 接口
    bindMapperForNamespace();
  }

  // 处理未完成解析的节点
  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

解析 <mapper> 下的节点

映射文件包含多种二级节点,比如 <cache><resultMap><sql>以及<select|insert|update|delete> 等。除此之外,还包含了一些三级节点,比如 <include><if><where>

<mapper namespace="com.xxx.dao.UserMapper">
   
    <cache/>

    <resultMap id="BaseUserResult" type="User">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <!-- ... -->
    </resultMap>

    <sql id="table">
         user
    </sql>
    
    <select id="findOne" resultMap="BaseUserResult">
         SELECT id, name, age FROM
            <include refid="table"/>
         WHERE id = #{id}
    </select>
    
    <!-- <insert|update|delete/> -->
</mapper>

解析映射文件节点的逻辑封装在相应的方法中,由 XMLMapperBuilder# configurationElement 方法提供解析入口。

private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.isEmpty()) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/sql"));
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
  }

解析<select|insert|update|delete>

XMLMapperBuilder#buildStatementFromContext 方法完成解析操作。该方法是为每个 XNode对象创建一个 XMLStatementBuilder对象,具体解析操作委托给 XMLStatementBuilder#parseStatementNode方法完成。

private void buildStatementFromContext(List<XNode> list) {
  if (configuration.getDatabaseId() != null) {
    // 调用重载方法构建 Statement
    buildStatementFromContext(list, configuration.getDatabaseId());
  }
  // 调用重载方法构建 Statement,requireDatabaseId 参数为空
  buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    // 创建Statment Builder类
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      // 解析 Statment 节点,并将解析结果存入到 Configuration的 mappedStatements 集合中
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
     // 解析失败,将解析器放入Configuration的incompleteStatements集合中
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

解析 <select|insert|update|delete> 节点的各个属性并创建 MappedStatement 对象。

// XMLStatementBuilder
public void parseStatementNode() {
  // 获取id 和 databaseId
  String id = context.getStringAttribute("id");
  String databaseId = context.getStringAttribute("databaseId");

  // 根据当前节点的databaseId和目标databaseId进行检测,决定是否保存或者忽略当前节点的SQL语句
  // databaseId MyBatis支持多厂商的功能
  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    return;
  }

  // 获取节点名称,比如 <select> 的节点名称为 select
  String nodeName = context.getNode().getNodeName();
  // 根据节点名称解析成 SqlCommandType
  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> 节点
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());

  String parameterType = context.getStringAttribute("parameterType");
  Class<?> parameterTypeClass = resolveClass(parameterType);

  String lang = context.getStringAttribute("lang");
  LanguageDriver langDriver = getLanguageDriver(lang);

  // 解析 <selectKey> 节点
  processSelectKeyNodes(id, parameterTypeClass, langDriver);

  // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
  KeyGenerator keyGenerator;
  String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
  keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
  if (configuration.hasKeyGenerator(keyStatementId)) {
    // 获取 KeyGenerator 实例
    keyGenerator = configuration.getKeyGenerator(keyStatementId);
  } else {
    // 创建 KeyGenerator 实例
    keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
        configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
        ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  }

  // 解析 SQL 语句
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  
  // 解析 Statement 类型,默认为 PREPARED
  StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  Integer fetchSize = context.getIntAttribute("fetchSize");
  Integer timeout = context.getIntAttribute("timeout");
  String parameterMap = context.getStringAttribute("parameterMap");
  
  //通过别名解析 resultType 对应的类型
  String resultType = context.getStringAttribute("resultType");
  Class<?> resultTypeClass = resolveClass(resultType);
  
  String resultMap = context.getStringAttribute("resultMap");
  String resultSetType = context.getStringAttribute("resultSetType");
  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
  if (resultSetTypeEnum == null) {
    resultSetTypeEnum = configuration.getDefaultResultSetType();
  }
  String keyProperty = context.getStringAttribute("keyProperty");
  String keyColumn = context.getStringAttribute("keyColumn");
  String resultSets = context.getStringAttribute("resultSets");

  // 构建 MappedStatement 对象,并将该对象存储到 Configuration 的 mappedStatements集合中
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered,
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

创建 SqlSource

SqlSource记录了 SQL 语句的信息。创建 SqlSource 是由 XMLLanguageDriver#createSqlSource 方法完成。方法内部将创建功能委托给 XMLScriptBuilder 类。

public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  return builder.parseScriptNode();
}

SQL 语句的解析逻辑封装在 XMLScriptBuilder#parseScriptNode 方法中。该方法首先会调用 parseDynamicTags() 解析 SQL 语句的节点,在解析过程中,会判断节点是是否包含一些动态标记,比如 ${} 占位符以及动态 SQL 节点等。若包含动态标记,则会将 isDynamic 设为 true。后续可根据 isDynamic 创建不同的 SqlSource

// XMLScriptBuilder
public SqlSource parseScriptNode() {
  // 解析 SQL 语句节点
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource;
  // 根据 isDynamic 变量 创建不同的 SqlSource
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}

parseDynamicTags() 主要是用来判断 SQL 语句是否包含一些动态标记,比如 ${} 占位符以及 动态SQL节点 等。这里,不管是 动态SQL节点 还是 静态SQL节点,MyBatis 都把它们看成是 SqlNode一条 SQL 语句由多个 SqlNode 组成。在解析过程中,这些 SqlNode 被存储在 contents 集合中。

最后,该集合会被传给 MixedSqlNode 构造方法,用于创建 MixedSqlNode 实例。从 MixedSqlNode 类名上可知,它会存储多种类型的 SqlNode

protected MixedSqlNode parseDynamicTags(XNode node) {
  List<SqlNode> contents = new ArrayList<>();
  NodeList children = node.getNode().getChildNodes();
  // 遍历子节点
  for (int i = 0; i < children.getLength(); i++) {
    XNode child = node.newXNode(children.item(i));
    if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
      // 获取文本内容
      String data = child.getStringBody("");
      TextSqlNode textSqlNode = new TextSqlNode(data);
      
      // 判断文本内容是否包含 动态节点标签 或 ${} 占位符
      if (textSqlNode.isDynamic()) {
        contents.add(textSqlNode);
        isDynamic = true;
      } else {
        // 创建 StaticTextSqlNode
        contents.add(new StaticTextSqlNode(data));
      }
     
    // child 节点是 ELEMENT_NODE 类型,比如 <if>、<where> 等
    } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { 
      // 获取节点名称 如 if、where
      String nodeName = child.getNode().getNodeName();
      // 根据节点名称获取 对应的 NodeHandler
      NodeHandler handler = nodeHandlerMap.get(nodeName);
      if (handler == null) {
        // 如果handler为空,表明当前节点不是MyBatis官方支持的节点,无法解析
        throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
      }
      // 处理 当前节点,生成相应的 SqlNode
      handler.handleNode(child, contents);
      isDynamic = true;
    }
  }
  return new MixedSqlNode(contents);
}
NodeHandler

SqlNode 是通过 NodeHandler 类创建的。NodeHandler 是个接口,处理不同的SQL动态节点由不同的子类去实现。

// XMLScriptBuilder
private void initNodeHandlerMap() {
  nodeHandlerMap.put("trim", new TrimHandler());
  nodeHandlerMap.put("where", new WhereHandler());
  nodeHandlerMap.put("set", new SetHandler());
  nodeHandlerMap.put("foreach", new ForEachHandler());
  nodeHandlerMap.put("if", new IfHandler());
  nodeHandlerMap.put("choose", new ChooseHandler());
  nodeHandlerMap.put("when", new IfHandler());
  nodeHandlerMap.put("otherwise", new OtherwiseHandler());
  nodeHandlerMap.put("bind", new BindHandler());
}

创建 MappedStatement

SQL 语句节点可以定义很多属性,这些属性和属性值以及 SqlSource 最终存储在 MappedStatement 中。

// MapperBuilderAssistant
public MappedStatement addMappedStatement(
    String id,
    SqlSource sqlSource,
    StatementType statementType,
    SqlCommandType sqlCommandType,
    Integer fetchSize,
    Integer timeout,
    String parameterMap,
    Class<?> parameterType,
    String resultMap,
    Class<?> resultType,
    ResultSetType resultSetType,
    boolean flushCache,
    boolean useCache,
    boolean resultOrdered,
    KeyGenerator keyGenerator,
    String keyProperty,
    String keyColumn,
    String databaseId,
    LanguageDriver lang,
    String resultSets) {

  if (unresolvedCacheRef) {
    throw new IncompleteElementException("Cache-ref not yet resolved");
  }

  id = applyCurrentNamespace(id, false);
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

  // 创建 Builder,设置各种属性
  MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
      .resource(resource)
      .fetchSize(fetchSize)
      .timeout(timeout)
      .statementType(statementType)
      .keyGenerator(keyGenerator)
      .keyProperty(keyProperty)
      .keyColumn(keyColumn)
      .databaseId(databaseId)
      .lang(lang)
      .resultOrdered(resultOrdered)
      .resultSets(resultSets)
      .resultMaps(getStatementResultMaps(resultMap, resultType, id))
      .resultSetType(resultSetType)
      .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
      .useCache(valueOrDefault(useCache, isSelect))
      .cache(currentCache);

  // 获取或者创建 ParameterMap,已经被官方废弃
  ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
  if (statementParameterMap != null) {
    statementBuilder.parameterMap(statementParameterMap);
  }

  // 创建MappedStatement,并添加到 Configuration 中的 mappedStatements 集合中
  MappedStatement statement = statementBuilder.build();
  configuration.addMappedStatement(statement);
  return statement;
}

绑定mapper接口

映射文件解析完成后,还需要通过命名空间绑定mapper接口,这样才能将映射文件中的sql语句和mapper接口中的方法绑定在一起。后续就可以直接通过调用mapper接口方法执行与之对应的sql语句。

private void bindMapperForNamespace() {
  // 映射文件的命名空间
  String namespace = builderAssistant.getCurrentNamespace();
  if (namespace != null) {
    Class<?> boundType = null;
    try {
      // 根据命名空间生成 mapper接口类
      boundType = Resources.classForName(namespace);
    } catch (ClassNotFoundException e) {
      // ignore, bound type is not required
    }
    // 检测当前 mapper接口类 是否被绑定过
    if (boundType != null && !configuration.hasMapper(boundType)) {
      configuration.addLoadedResource("namespace:" + namespace);
      //绑定mapper类
      configuration.addMapper(boundType);
    }
  }
}

最终调用 MapperRegistry#addMapper() 绑定mapper类

public <T> void addMapper(Class<T> type) {
  if (type.isInterface()) {
    if (hasMapper(type)) {
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }
    boolean loadCompleted = false;
    try {
      // 将 type 和 MapperProxyFactory 进行绑定
      // MapperProxyFactory 可为 mapper 接口生成代理类
      knownMappers.put(type, new MapperProxyFactory<>(type));
     
      // 创建注解解析器。在MyBatis中,有XML和注解2种配置方式可选
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      // 解析注解中的信息
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

Mapper接口绑定过程:

  1. 获取命名空间,并根据命名空间生成mapper接口类。
  2. typeMapperProxyFactory实例存入knownMappers中。
  3. 解析注解中的信息。

处理未完成解析的节点

在解析某些节点的过程中,如果这些节点引用了其他一些未被解析的配置,会导致当前 节点解析工作无法进行下去。对于这种情况,MyBatis 的做法是抛出 IncompleteElementException 。外部逻辑会捕捉这个异常,并将节点对应的解析器放入 Configuration中incomplet* 集合中。

解析未完成的 <cache-ref> 节点

假设MyBatis先解析映射文件1,再解析映射文件2。按照这样的解析顺序,映射文件1中的<cache-ref>节点就无法完成解析,因为它所引用的缓存还未被解析。

<!-- 映射文件1 -->
<mapper namespace="com.xxx.dao.UserMapper1">
    <cache-ref namespace="com.xxx.dao.UserMapper2"/>
</mapper>

<!-- 映射文件2 -->
<mapper namespace="com.xxx.dao.UserMapper2">
    <cache/>
</mapper>

当映射文件2解析完成后,MyBatis 会调用 parsePendingCacheRefs() 方法处理在此之前未完成解析的节点。

private void parsePendingCacheRefs() {
  // 获取 CacheRefResolver 列表
  Collection<CacheRefResolver> incompleteCacheRefs = configuration.getIncompleteCacheRefs();
  synchronized (incompleteCacheRefs) {
    Iterator<CacheRefResolver> iter = incompleteCacheRefs.iterator();
    // 通过迭代器遍历列表
    while (iter.hasNext()) {
      try {
        // 尝试解析 <cache-ref> 节点,若解析失败,则抛处 IncompleteElementException,此时下面的remove操作不会执行。
        iter.next().resolveCacheRef();
        
        // 移除 CacheRefResolver 对象。代表已经成功解析 <cache-ref> 节点
        iter.remove();
      } catch (IncompleteElementException e) {
        // 如果再次捕获 IncompleteElementException 异常,表示当前映射文件中并没有<cache-ref>所引用的缓存。有可能所引用的缓存在后面的映射文件中。所以需要保留 CacheRefResolver。
      }
    }
  }
}

解析步骤:

  1. 获取获取 CacheRefResolver 列表,并进行遍历
  2. 尝试解析节点,若解析失败再次抛出异常
  3. 若解析成功则列表中移除相关节点