Mybatis源码解析-6.Mapper映射文件配置解析

6.XML文件格式的mapper标签解析

上一节已经知道,对于XML文件中mapper标签的解析都是通过XMLMapperBuilder进行处理的。

接下来让我们首先对XMLMapperBuilder进行分析,然后再详细考察mapper标签的解析逻辑。

XMLMapperBuilder

XMLMapperBuilder,顾名思义,该工具类是用于解析mapper标签的,这里我们主要分析XMLMapperBuilder的四个属性,便于下面分析具体的业务逻辑。XMLMapperBuilder拥有如下四个属性:

  1. XPathParser parser:XML文件解析器,该对象是XML文件解析的工具类。
  2. MapperBuilderAssistant builderAssistant:Mapper构建协助器,由于Mybatis中的很多对象都比较大,属性比较多,而解析结果本身就是一个对象,因此Mybatis将解析结果的构建代码进行封装,封装到这个构建协助器里,让代码更加简洁。除此之外,这里还存储了当前的命名空间,方便获取。
  3. Map<String, XNode> sqlFragments :Sql段,保存<mapper>标签的<sql>子标签定义的sql语句,这里我们先不考虑该对象含义。
  4. String resource:要解析的资源的名称

开始解析mapper标签

了解了XMLMapperBuilder的各个属性的含义,就可以开始解析mapper标签了,否则我们可能在解析到一半就不知道哪个东西是做什么的了。

根据XMLConfigBuildermapperElement(XNode)方法中如下代码:

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();

我们知道,mapper标签的解析是由XMLMapperBuilderparse()方法完成的。让我们考察该方法代码:

public void parse() {
    // 如果资源没有被解析过,则需要进行解析
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }
    // 解析未完成解析的ResultMap、CacheRef和Statement
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

对于该方法的前半部分肯定大家都没有意见,如果资源没有被解析,就开始解析,这是一个正常的需求,而后半部分,则就无法理解了,为什么这里无论如何都要执行呢?事实上,并不是解析了一个mapper配置文件就可以获取到所有的ResultMapCacheRefStatements的,所以再每次解析完一个mapper配置文件都会进行解析一遍上面三个资源,而且,对于具有Java配置的Mybatis,可能还需要解析完Java配置后,再去解析上面的三个资源。所以才会出现后半部分。

让我们先讨论前半部分的解析mapper标签的部分。其中:

  1. configurationElement(parser.evalNode("/mapper")) 这行代码负责主要解析mapper配置,并创建一个MappedStatement放入到Configuration类型的对象中。
  2. configuration.addLoadedResource(resource); 这行代码负责记录,该配置文件已经被解析完毕
  3. bindMapperForNamespace(); 最后的一行代码负责将Java的Mapper对象与命名空间(即XML配置文件)联系起来。

解析mapper标签

下面让我们考察一下configurationElement(XNode)方法。该方法是解析XML映射文件的主要逻辑。代码如下:

private void configurationElement(XNode context) {
    try {
      // 获取命名空间名称
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      // 存储当前命名空间
      builderAssistant.setCurrentNamespace(namespace);
      // 解析另一命名空间的缓存配置
      cacheRefElement(context.evalNode("cache-ref"));
      // 解析当前命名空间是否开启缓存
      cacheElement(context.evalNode("cache"));
      // 解析parameterMap标签
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // 解析resultMap标签
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 解析sql标签
      sqlElement(context.evalNodes("/mapper/sql"));
      // 解析select|insert|update|delete标签
      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);
    }
}
解析cache-ref标签

首先处理<cache-ref>标签,该标签是引用其他缓存的标签,Mybatis对该标签描述如下:

对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新。 但你可能会想要在多个命名空间中共享相同的缓存配置和实例。要实现这种需求,你可以使用 cache-ref 元素来引用另一个缓存。

该属性的解析由cacheRefElement(XNode)方法处理,其实代码很简单:

private void cacheRefElement(XNode context) {
    if (context != null) {
      // 在Configuration的缓存引用注册表中添加一条
      // 当前命名空间:要引用的命名空间
      // 用于标识命名空间缓存之间的引用关系
      configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
      // 将builderAssistant和要引用的命名空间缓存封装入一个CacheRefResolver去解析缓存
      // 解析成功后放到builderAssistant备用
      CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
      try {
        // 尝试解析缓存
        // 解析失败则放入到Configuration对象的incompleteCacheRefs注册表中
        cacheRefResolver.resolveCacheRef();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteCacheRef(cacheRefResolver);
      }
    }
}

事实证明,在Mybatis中,储存命名空间使用缓存关系的是一张名为cacheRefMap格式为<String,String>的注册表,其中key和value都是命名空间的名称。因此此处,是将<当前命名空间,引用的缓存的命名空间>放入到注册表中。然后再根据缓存命名空间解析使用的缓存。

当然对于解析失败的缓存,Mybatis将解析失败,即当前未生成的缓存,就将其放入到incompleteCacheRefs注册表中,等待进一步解析,这也就是mapper标签解析中后三个方法的意义。

解析cache标签

<cache>标签用于定义当前命名空间使用的缓存,该标签的解析是由cacheElement(XNode)方法完成的。解析方法相对简单:

private void cacheElement(XNode context) {
    if (context != null) {
      // 获取缓存类型
      // 默认情况下是永久缓存,没有过期时间
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      // 解析过期策略,默认是LRU,最近最少使用
      String eviction = context.getStringAttribute("eviction", "LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      // 设置刷新间隔
      Long flushInterval = context.getLongAttribute("flushInterval");
      // 设置缓存大小
      Integer size = context.getIntAttribute("size");
      // 设置缓存是否只读
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      boolean blocking = context.getBooleanAttribute("blocking", false);
      // 将所有属性按照Properties读取
      Properties props = context.getChildrenAsProperties();
      // 将读取到的属性应用到缓存中,构建缓存
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

之前说过,MapperBuilderAssistant的一大功能就是创建复杂对象并注册到Configuration注册表中,这里就是通过读取到的属性建立一个缓存,然后注册到Configuration对象中。查看MapperBuilderAssistant.useNewCache(...)方法:

public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    // 根据配置构建Cache
    // 默认状态是永久缓存,使用LRU算法
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    // 将缓存注册到Configuration中
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
}

那么在Mybatis中,缓存是如何存储的呢?考察如下代码:

configuration.addCache(cache);

public void addCache(Cache cache) {
    caches.put(cache.getId(), cache);
}

可以看到,缓存在Configuration对象中也是存放在一个注册表中,该注册表叫做caches,格式是<String, Cache>,其中key是命名空间,value是缓存实例。

解析parameterMap标签

<parameterMap>标签用于声明方法参数类型,解析该标签使用的是parameterMapElement(List<XNode>)方法,考察该方法:

private void parameterMapElement(List<XNode> list) {
    // 遍历所有parameterMap标签
    for (XNode parameterMapNode : list) {
      // 获取parameterMap的id
      String id = parameterMapNode.getStringAttribute("id");
      // 获取parameterMap代表的Class类型
      String type = parameterMapNode.getStringAttribute("type");
      // 将type解析为Class对象
      Class<?> parameterClass = resolveClass(type);
      // 获取所有parameter子标签
      List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter");
      // 创建列表存储子标签内容
      List<ParameterMapping> parameterMappings = new ArrayList<>();
      // 遍历所有子标签
      for (XNode parameterNode : parameterNodes) {
        // 获取属性名
        String property = parameterNode.getStringAttribute("property");
        // 获取属性类型
        String javaType = parameterNode.getStringAttribute("javaType");
        // 获取属性代表的JDBC类型
        String jdbcType = parameterNode.getStringAttribute("jdbcType");
        // 获取resultMap属性,具体使用到的情况请看Mybatis文档
        String resultMap = parameterNode.getStringAttribute("resultMap");
        // 获取mode属性
        String mode = parameterNode.getStringAttribute("mode");
        // 获取typeHandler属性
        String typeHandler = parameterNode.getStringAttribute("typeHandler");
        // 获取numericScale属性,该属性表示小数保留的位数
        Integer numericScale = parameterNode.getIntAttribute("numericScale");
        ParameterMode modeEnum = resolveParameterMode(mode);
        Class<?> javaTypeClass = resolveClass(javaType);
        JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
        Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
        // 将子标签构造成一个ParameterMapping
        ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);
        // 并放入一个列表中
        parameterMappings.add(parameterMapping);
      }
      // 最后将结果列表注册到`Configuration`对象的`parameterMaps`属性中
      builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
    }
}

仔细考察builderAssistant.addParameterMap(id, parameterClass, parameterMappings);可以发现,Configuration对象使用parameterMaps注册表存储各个ParameterMap,格式为<String, ParameterMap>。其中key是id,parameterMap的id是命名空间 + id字段,value就是parameterMap对象。

解析resultMap标签

<resultMap>标签用于表示Mybatis数据库操作的结果集,是Mybatis引以为豪的一部分。该标签功能相当强大,在一般的结果映射上增添了许多功能。ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。

解析<resultMap>标签的方法是resultMapElements(List<XNode>),该方法只是遍历所有的<resultMap>标签对其进行解析罢了:

private void resultMapElements(List<XNode> list) throws Exception {
    for (XNode resultMapNode : list) {
      try {
        resultMapElement(resultMapNode);
      } catch (IncompleteElementException e) {
        // ignore, it will be retried
      }
    }
  }

真正的解析方法其实在resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType)中,因为resultMapElement(resultMapNode);就是调用的该方法,我们直接考察该方法:

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    // 获取resultMap的id
    String id = resultMapNode.getStringAttribute("id",
        resultMapNode.getValueBasedIdentifier());
    // 获取resultMap的java类型
    // 获取顺序是type->ofType->resultType->javaType
    String type = resultMapNode.getStringAttribute("type",
        resultMapNode.getStringAttribute("ofType",
            resultMapNode.getStringAttribute("resultType",
                resultMapNode.getStringAttribute("javaType"))));
    // 获取extends属性,
    String extend = resultMapNode.getStringAttribute("extends");
    // 获取autoMapping属性
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    // 通过获取到的Java类型获取对应的Class对象
    Class<?> typeClass = resolveClass(type);
    Discriminator discriminator = null;
    List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
    resultMappings.addAll(additionalResultMappings);
    // 遍历resultMap标签的所有子标签
    // resultMap的子标签共有4种:
    // 1. constructor:用于声明创建结果集的构造器
    // 2. discriminator:配置鉴别器
    // 3. id:用于标记字段是id
    // 4. result:用于标记属性与数据库字段的对应关系
    List<XNode> resultChildren = resultMapNode.getChildren();
    for (XNode resultChild : resultChildren) {
      if ("constructor".equals(resultChild.getName())) {
        // connstuctor标签的解析
        processConstructorElement(resultChild, typeClass, resultMappings);
      } else if ("discriminator".equals(resultChild.getName())) {
        // discriminator 标签的解析
        discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
      } else {
        // id 标签的解析
        List<ResultFlag> flags = new ArrayList<ResultFlag>();
        if ("id".equals(resultChild.getName())) {
          flags.add(ResultFlag.ID);
        }
        // result标签的解析
        resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
      }
    }
    // 通过ResultMapResolver将解析结果添加到Configuration的resultMaps注册表中
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
      return resultMapResolver.resolve();
    } catch (IncompleteElementException  e) {
      configuration.addIncompleteResultMap(resultMapResolver);
      throw e;
    }
}

事实上,Mybatis中的<resultMap>标签最后会变成一个ResultMap对象。我们之前一共谈到了如下几个属性,他们与ResultMap对象的属性对应关系如下:

  1. id:ResultMap.id
  2. type(Java类型):ResultMap.type
  3. result子标签集合:ResultMap.resultMappings
  4. id子标签:ResultMap.idResultMappings
  5. 构造器子标签:ResultMap.constructorResultMappings
  6. 鉴别器子标签:ResultMap.discriminator
  7. autoMapping属性:ResultMap.autoMapping

需要注意,各个XXXResultMappings属性都是ResultMapping对象列表ResultMapping主要用来存储java对象属性与数据库字段之间的对应关系,例如:

 <result property="doorCount" column="door_count" />

上面的xml配置就会生成一个ResultMapping对象。我们根据这一段xml文件配置查看一下ResultMapping到底如何进行存储的?ResultMapping对象具有如下几个属性,分别代表的意义列在注释上:

  // 保存该ResultMap的Configuration对象
  private Configuration configuration;
  // 当前标签所代表的属性,上面例子中则是doorCount
  private String property;
  // 当前标签所配置的数据库列名,上面例子中则是door_count
  private String column;
  // 当前字段在Java中的类型
  private Class<?> javaType;
  // 当前字段在数据库中的Jdbc类型
  private JdbcType jdbcType;
  // 处理该类型字段的类型处理器
  private TypeHandler<?> typeHandler;
  // 对于Mybatis来说经常会有如下的情况
  // <collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
  // 如果是这种情况,就会将resultMap的id填在此处
  private String nestedResultMapId;
  // 对于Mybatis的resultMap标签会有如下情况
  // <collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
  // 如果是这种情况,就会把select中指定的查询语句的id填在此处
  private String nestedQueryId;
  private Set<String> notNullColumns;
  // 列前缀
  private String columnPrefix;
  // 当前字段的标识,例如该字段是ID
  private List<ResultFlag> flags;
  // 对于Mybatis的resultMap标签会有如下情况:
  // <collection property="posts" ofType="domain.blog.Post">
  //   <id property="id" column="post_id"/>
  //   <result property="subject" column="post_subject"/>
  //   <result property="body" column="post_body"/>
  // </collection>
  // 这样就会出现ResultMapping的嵌套关系,而这里就用来存储这一嵌套关系
  private List<ResultMapping> composites;
  private String resultSet;
  // 外键
  private String foreignColumn;
  // 该字段是否需要懒加载
  private boolean lazy;

通过上面的解析,我们已经了解了<resultMap>标签在Mybatis配置对象中保存的结构,其实每个<resultMap>标签就是一个ResultMap对象,这个对象中存储了对象属性与数据库字段的关系,并且存储了获取这些对象使用的查询sqlId,就好像一个命令模式,提供了输出格式和操作过程,由执行器执行具体的逻辑(SQL查询语句),然后拼接返回结果。

解析sql标签

mapper映射文件的配置中,我们经常用到如下配置:

<sql id="someinclude">
  from
    <include refid="${include_target}"/>
</sql>

这种引用某个SQL片段的情况。<sql>标签就是为了防止重复书写相同配置而存在的。该标签由XMLMapperBuilder.sqlElement(List<XNode>)方法进行解析。下面我们考察该方法:

private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
    for (XNode context : list) {
      // 获取databaseId属性
      String databaseId = context.getStringAttribute("databaseId");
      // 获取id属性
      String id = context.getStringAttribute("id");
      // 将id拼接上命名空间,构建成完整的id
      id = builderAssistant.applyCurrentNamespace(id, false);
      // 如果当前正在使用的数据库ID与获取到的SQL上的ID相同的话
      // 那么引用这个sql标签
      // 否则不使用它
      if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
        sqlFragments.put(id, context);
      }
  }
}

注意,此处的应用是将XNode节点放入sqlFragments属性中,就是我们之前卖关子的属性。真正解析该标签的过程是在真正解析操作数据库的sql语句的时候。话不多说,让我们考察那些SQL语句吧。

解析语句标签

真正操作数据库,执行数据库操作的SQL是由<insert>selectupdatedelete标签声明的,这四个标签分别声明了插入、查找、更新、删除操作。事实上,对于mybatis来说,数据库操作只有两种:

  1. 查找:select
  2. 更新:包括insert、update、delete

处理上述四个标签解析逻辑的是XMLMapperBuilder.buildStatementFromContext(List<XNode>)方法。该方法传入所有的数据库操作SQL标签的XML节点,源码如下:

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    // 遍历所有的insert、update、delete、select标签
    for (XNode context : list) {
      // 为每个标签创建XMLStatementBuilder
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        // 通过XMLStatementBuilder进行解析
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
}

之前已经说过,在Mybatis的BaseBuilder类有多个子类,其中除了XMLConfigBuilder类以外,其余的类都是用于解析<mapper>标签的,这里的XMLStatementBuilder就是用来解析<mapper>标签中声明的持久化操作的。具体的解析逻辑在XMLStatementBuilder.parseStatementNode()方法中。注意在创建XMLStatementBuilder传入了如下参数:

  1. configuration:Mybatis配置存储的配置对象
  2. builderAssistant:配置解析的协助器,用于简化各种复杂的构建操作,并将构建的结果对象添加到configuration对象中
  3. context:要解析的XML节点
  4. requiredDatabaseId:mybatis配置中指定的databaseId,由于之前已经提到,<sql>标签只有在自己声明的databaseId与Mybatis配置的databaseId相同的情况下才会使用。而sql标签内容的解析在各个使用了include标签的地方进行解析。

明确了上面的内容之后,我们就可以开始分析解析操作数据的sql的真正逻辑了,即XMLStatementBuilder.parseStatementNode()方法,源码如下:

public void parseStatementNode() {
    // 获取SQL语句ID
    String id = context.getStringAttribute("id");
    // 获取SQL语句适应的数据库
    String databaseId = context.getStringAttribute("databaseId");
    // 如果当前sql指定的数据库与Mybatis中配置的不同,则不进行解析
    // 需要注意,这里还做了一个判重操作,如果有相同id的语句已经解析过,并且其databaseId不为null,那么就跳过本次解析
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    // 获取fetchSize属性
    Integer fetchSize = context.getIntAttribute("fetchSize");
    // 获取timeout属性
    Integer timeout = context.getIntAttribute("timeout");
    // 获取parameterMap属性
    String parameterMap = context.getStringAttribute("parameterMap");
    // 获取parameterType属性
    String parameterType = context.getStringAttribute("parameterType");
    // 将parameterType解析成Java的Class对象
    Class<?> parameterTypeClass = resolveClass(parameterType);
    // 获取resultMap属性
    String resultMap = context.getStringAttribute("resultMap");
    // 获取resultType属性
    String resultType = context.getStringAttribute("resultType");
    // 获取语言属性
    String lang = context.getStringAttribute("lang");
    // 查找对应的语言驱动
    LanguageDriver langDriver = getLanguageDriver(lang);
    // 将resultType属性解析为对应的Java的Class对象
    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));
    // 判断SQL是否是查询语句
    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());

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // 解析SQL与动态SQL标签
    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;
    // 处理自动生成id
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }
    // 将解析结果构建成一个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);
}

其实上述解析操作大致可以分为5步:

  1. 判断databaseId与当前配置是否匹配,并判断该id的语句是否被解析过,这就是问什么要优先获取id的原因
  2. 获取标签基本信息
  3. 处理include标签
  4. 处理自动生成key的配置
  5. 解析SQL
  6. 构建MappedStatement放入到configuration对象中

其中第一步和第二步都比较简单,这里我们主要介绍后面三个步骤。首先是处理include标签。

include标签的处理

<include>标签就是用来引用之前在sql标签中声明的sql的。上面的代码中处理include标签的是如下几行:

XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());

可以看到对于<include>标签的解析交给了XMLIncludeTransformer这一解析器,该解析器与别的解析器有一个很大的不同,就是别的解析器都是解析标签,然后将标签的内容封装为一个Java对象,放到Configuration对象中,而这个解析器则是,修改XML解析对象,然后再次交给XMLStatementsBuilder进行解析。了解了这一点之后,我们可以考察一下XMLIncludeTransformer.applyIncludes(Node source, final Properties variablesContext, boolean included)方法的源码:

private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
    // 情况1
    // 如果传入的节点是<include>节点
    // 那么获取refid属性,以及<include>中声明的参数
    // 根据这些去查找对应的sql段
    // 如果sql段中有<include>标签,则递归处理
    // 否则将include标签替换成sql标签
    // 然后再将sql标签中所有的内容替换到sql标签所在的位置
    if (source.getNodeName().equals("include")) {
      Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
      Properties toIncludeContext = getVariablesContext(source, variablesContext);
      applyIncludes(toInclude, toIncludeContext, true);
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      source.getParentNode().replaceChild(toInclude, source);
      while (toInclude.hasChildNodes()) {
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      toInclude.getParentNode().removeChild(toInclude);
      // 情况2
      // 如果传入的标签是一般的非<include>标签
      // 则应用属性表中的属性到标签中,然后递归初期传入标签的子标签
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
      if (included && !variablesContext.isEmpty()) {
        // replace variables in attribute values
        NamedNodeMap attributes = source.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
          Node attr = attributes.item(i);
          attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
        }
      }
      NodeList children = source.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        applyIncludes(children.item(i), variablesContext, included);
      }
      // 情况3
      // 如果传入的标签是文本标签
      // 那么应用属性表中的属性
    } else if (included && source.getNodeType() == Node.TEXT_NODE
        && !variablesContext.isEmpty()) {
      // replace variables in text node
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
}

处理<include>标签使用了递归的方式进行处理。这里我们举例说明会比较形象,考虑下面的例子:

<sql id="sometable">
  ${prefix}Table
</sql>

<sql id="someinclude">
  from
    <include refid="${include_target}"/>
</sql>

<select id="select" resultType="map">
  select
    field1, field2, field3
  <include refid="someinclude">
    <property name="prefix" value="Some"/>
    <property name="include_target" value="sometable"/>
  </include>
</select>

一般情况下,我们解析id为select<select>标签,这里包含了一个<include>标签,实际上会将该<select>标签传入到applyIncludes(Node source, final Properties variablesContext, boolean included),其中source就是该<select>标签内容,variablesContext<include>标签中的属性表,最后的included表示是不是include标签内的内容。

这里首先会调用情况2,解析<select>标签,获取所有该标签的子标签进行遍历处理<include>标签,事实上,该标签只有两个标签,第一个是select field1, field2, field3,第二个是include标签,这里第一个标签交给情况3处理,<include>标签交给情况1处理。

需要注意的是,可以发现,Mybatis并没有进行<sql>标签自身的循环依赖问题处理。实际上如果出现了此问题,Mybatis只能是在解析时无限递归导致stackOverFlow,并不会给出任何提示,不像是Spring还会给出出现了循环依赖问题。

例如,你写的sql标签是这样的:

<sql id="id">
  c.id
  <include refid="id"></include>
</sql>

这种情况就会导致<include>标签的解析出现无限的递归,因此,出现stackOverFlow的情况。

selectKey标签的处理

Mybatis的<selectKey>标签用于处理自动生成主键的问题。有些数据库或者JDBC驱动不支持自动生成主键,因此只能自己创建自定义主键。但是频繁调用setter方法设置主键又不好,因此就有了selectKey这一标签。Mybatis中给出的<selectKey>标签的使用方法如下:

<insert id="insertAuthor">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
  </selectKey>
  insert into Author
    (id, username, password, email,bio, favourite_section)
  values
    (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>

简单介绍一下<selectKey>子标签的属性:

  1. keyProperty:selectKey 语句结果应该被设置到的目标属性。如果生成列不止一个,可以用逗号分隔多个属性名称。
  2. keyColumn:返回结果集中生成列属性的列名。如果生成列不止一个,可以用逗号分隔多个属性名称。
  3. resultType:结果的类型。通常 MyBatis 可以推断出来,但是为了更加准确,写上也不会有什么问题。MyBatis 允许将任何简单类型用作主键的类型,包括字符串。如果生成列不止一个,则可以使用包含期望属性的 Object 或 Map。
  4. order:可以设置为 BEFORE 或 AFTER。如果设置为 BEFORE,那么它首先会生成主键,设置 keyProperty 再执行插入语句。如果设置为 AFTER,那么先执行插入语句,然后是 selectKey 中的语句 - 这和 Oracle 数据库的行为相似,在插入语句内部可能有嵌入索引调用。
  5. statementType:和前面一样,MyBatis 支持 STATEMENT,PREPARED 和 CALLABLE 类型的映射语句,分别代表 Statement, PreparedStatement 和 CallableStatement 类型。

处理<selectKey>标签的是XMLStatementBuilder.processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver)方法。考察该方法源码如下:

private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
    List<XNode> selectKeyNodes = context.evalNodes("selectKey");
    // 处理<selectKey>标签
    if (configuration.getDatabaseId() != null) {
      parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
    }
    parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
    // 移除<selectKey>标签
    removeSelectKeyNodes(selectKeyNodes);
}

通过刚才给出的例子我们可以看到,<selectKey>标签中的内容仅用于查询一个值,该值用于作为<insert>操作的主键,因此真正执行插入操作时是不需要这条sql的,因此需要在解析之后把它清除掉。

parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);

这两行代码负责解析<selectKey>标签,而后面的removeSelectKeyNodes(selectKeyNodes)负责将<selectKey>标签移除。

下面我们考察解析操作中占主要地位的XMLStatementBuilder.parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId)方法。该方法其实仅仅是读取所有的<selectKey>标签然后进行解析,代码如下:

private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
    // 遍历所有的`<selectKey>`标签,对每个标签进行解析
    for (XNode nodeToHandle : list) {
      String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
      String databaseId = nodeToHandle.getStringAttribute("databaseId");
      if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
        parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
      }
    }
}

可以看到,对于每个<selectKey>标签来说,他的默认id是其父id+!selectKey。例如上面的例子中,<selectKey>的id就是insertAuthor!selectKey。不过实际上<selectKey>就是一个查询语句,也就相当于<select>标签,Mybatis确实也是这样处理的。我们都知道对于<select>标签来说,最后都会变成一个MappedStatement。对于<selectKey>标签也是,该MappedStatement的id就是我们上面生成的那个id。真正构建MappedStatement的方法就是parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);这行代码。也就是XMLStatementBuilder.parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId)方法。源码如下 :

private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
    // 获取返回类型
    String resultType = nodeToHandle.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    // 获取statementType
    StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    // 获取Java属性名称
    String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
    //  获取数据库属性名称
    String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
    // 判断是在插入操作之前调用还是之后调用
    boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

    //defaults
    boolean useCache = false;
    boolean resultOrdered = false;
    KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
    Integer fetchSize = null;
    Integer timeout = null;
    boolean flushCache = false;
    String parameterMap = null;
    String resultMap = null;
    ResultSetType resultSetTypeEnum = null;

    SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
    SqlCommandType sqlCommandType = SqlCommandType.SELECT;
    // 创建一个MappedStatement保存该查询操作
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);

    id = builderAssistant.applyCurrentNamespace(id, false);

    MappedStatement keyStatement = configuration.getMappedStatement(id, false);
    // 使用该查询Sql创建一个KeyGenerator
    // 并将该查询sql与keyGenerator简历联系
    configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}

事实证明,通常情况下,我们很少使用<selectKey>标签显式指定id,更习惯的是使用数据库的自增id,这种时候,我们只需要在<insert>useGeneratedKeys属性置为true。这时候使用的就是默认的Jdbc3KeyGenerator

至于各种KeyGenerator的具体执行流程,我们在后面讲解Mybatis执行流程时将会讨论该问题。

解析SQL

到这里其实我们已经快把Mybatis的配置解析讨论完了,但是我们并没有细节的讨论Mybatis的SQL到底是怎么解析的到底放在哪儿,我们只知道,保存SQL的就是一个叫SqlSource的对象,这个对象到底是怎样生成的呢?事实上我们还有一个BaseBuilder没有讨论,就是XMLScriptBuilder,其实该BaseBuilder是用来做动态SQL的解析的,但是事实上也用来解析一般SQL。解析入口就是上面的如下方法:

public void parseStatementNode() {
  ...
  LanguageDriver langDriver = getLanguageDriver(lang);
  ...
  // 解析SQL与动态SQL标签
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  ...
}

可以看到解析SQL和动态SQL标签的是一个LanguageDriver对象,该对象通过XMLStatementBuilder.getLanguageDriver(String lang)方法处理,查看该方法:

private LanguageDriver getLanguageDriver(String lang) {
    Class<? extends LanguageDriver> langClass = null;
    if (lang != null) {
      langClass = resolveClass(lang);
    }
    return configuration.getLanguageDriver(langClass);
}

事实上,很少有人去设置SQL语言,所以一般都用的默认配置,即XMLLanguageDriver,一般我们获取到的就是这个,如果想要设置可以通过<settings>标签的defaultScriptingLanguage属性设置。让我们考察这个XMLLanguageDrivercreateSqlSource(Configuration configuration, XNode script, Class<?> parameterType)方法,该方法负责创建SqlSource。源码如下:

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

可以看到,创建SqlSource的工作其实就是我们没有讨论的XMLScriptBuilder做的。查看该类的parseScriptNode()方法:

public SqlSource parseScriptNode() {
    // 解析动态SQL标签
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    // 如果含有动态SQL标签则返回结果为DynamicSqlSource
    // 否则返回RawSqlSource
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}

可以看到,如果传入的SQL有动态SQL标签,则返回的是DynamicSqlSource,否则返回的是RawSqlSource。事实上RawSqlSource就是<select>这类标签中写入的SQL,而DynamicSqlSource则是一个SQLNode列表,每一部分动态SQL就是一个SQLNode,这部分相对较为复杂,等到之后讲解真正解析SQL时会详细描述。

MappedStatement的结构

执行了上面的所有操作,我们就已经做好了充足的准备构建一个新的MappedStatement用来表示一个操作的SQL语句。那么MappedStatement到底是怎么和我们刚才获取到的数据对应上的呢?下面让我们考虑MappedStatement的属性以及其业务含义。

  // 该MappedStatement所对应的mapper.xml
  private String resource;
  // 存储该MappedStatement的Configuration对象
  private Configuration configuration;
  // 该MappedStatement的id
  private String id;
  // 该MappedStatement获取结果的默认长度
  private Integer fetchSize;
  // 该MappedStatement代表的SQL执行的超时时间
  private Integer timeout;
  // 该MappedStatement的Statement类型
  private StatementType statementType;
  // 该MappedStatement代表的SQL的返回集合类型
  private ResultSetType resultSetType;
  // 该MappedStatement的Sql元数据
  private SqlSource sqlSource;
  // 该MappedStatement所使用的缓存
  private Cache cache;
  // 该MappedStatement所使用的参数列表
  private ParameterMap parameterMap;
  // 该MappedStatement所使用的结果集列表
  private List<ResultMap> resultMaps;
  // 执行该MappedStatement所代表的操作时是否要刷新缓存
  private boolean flushCacheRequired;
  // 执行该MappedStatement所代表的操作时是否要使用缓存
  private boolean useCache;
  private boolean resultOrdered;
  // 该MappedStatement所代表SQL的类型(UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;)
  private SqlCommandType sqlCommandType;
  // 该MappedStatement的对应key生成器
  private KeyGenerator keyGenerator;
  // 该MappedStatement对应的key生成器使用的属性
  private String[] keyProperties;
  // 该MappedStatement对应的key生成器对应的数据库列
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  // 数据库类型
  private String databaseId;
  // statementLog
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;

可以看到MappedStatement包含了一条SQL执行的所有格式上的数据,只缺少查询参数这类内容数据,这就像一个命令模式,只需要传入命令格式和对应的内容就可以执行。

这里除了对MappedStatement结构进行讲解以外,还要特别讲解一下我们的如下方法:

builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);

在我们分析了那么多的Mybatis配置解析源码都没有仔细提这个builderAssistant,即我们的构建协助器,但是现在我们不得不说一下该类的addMappedStatement(XXXX)方法,因为这涉及到我们的Mapper到底使用哪个缓存,下面我们考察这个方法的部分代码:

public MappedStatement addMappedStatement(...) {
    ...

    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 statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
  }

可以看到,最后Mapper真正使用的缓存就是这个currentCache。更改这个属性的代码就在解析<cache><cache-ref>标签中。

了解了最主要的MappedStatement,我们其实就可以开始分析Mybatis的执行流程了。但是,MappedStatement到底是怎么和对应的Class对象联系起来的呢 ?

标识资源已经解析

让我们现在再回归XMLMapperBuilderparse()方法上,再次给出该方法源码:

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

我们之前讨论的那么多都是configurationElement(parser.evalNode("/mapper"));这一行代码。接下来我们会继续讨论接下来的代码。这里我们讨论的是configuration.addLoadedResource(resource);

这行代码负责将已解析的资源添加到一个注册表中(Configuration的loadedResources属性中),避免重复解析。这一点功能很明确。但是,除此之外还有另一个功能,实际上Mybatis并不是所有的配置都是XML文件书写的,有一部分配置是在Java类上的。因此这部分配置也要进行解析。但是解析之后可能有XML配置与Java配置两者互补的地方。因此就需要避免重复解析,直接调用后面的三个parseXXX方法就好了。而刚才说到的解析不全的地方,相比通过阅读上面一节已经很清楚了,就被保存在Configuration对象的incompleteXXX对象中。

将XML文件绑定到对应的Java Class对象上

最后让我们讲解一下XML文件是如何和Java对象结合的,使用过Mybatis的读者都知道,mapper.xmlnamespace属性的值就是Java类的权限定类名,那这一步对应是怎么做的呢?

我们仍然查看XMLMapperBuilderparse()方法上,方法源码如下:

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

请注意bindMapperForNamespace();这行代码,方法名已经很明确了,这就是将Mapper Class对象和命名空间整合的地方,让我们考察该方法的源码:

private void bindMapperForNamespace() {
    // 获取当前的命名空间
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        // 通过命名空间获取对应的Class对象
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound type is not required
      }
      // 如果获取到的Class对象不为空
      // 查找有没有为其分配MappedStatement
      // 如果没有则添加解析记录
      // 并且将Class对象与XML文件联系起来(即将Class对象添加到Configuration的knownMappers注册表中)
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          // Spring may not know the real resource name so we set a flag
          // to prevent loading again this resource from the mapper interface
          // look at MapperAnnotationBuilder#loadXmlResource
          configuration.addLoadedResource("namespace:" + namespace);
          configuration.addMapper(boundType);
        }
      }
    }
  }

通过上面的代码可以看到,将MappedStatement联系到Class对象,仅仅是确保Class对象已经解析过,然后将Class对象放入到Configuration的knownMappers注册表中。如果没解析过得话,那就开始解析Java的Class对象,可以考察configuration.addMapper(boundType);方法:

public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
}

可以看到,这就是接下来要讲解的基于Java注解配置的Mapper对象的解析方法。所以在解析XML文件配置后紧接着就会解析Java的Class对象。除此之外,上述代码还有如下部分需要注意:

// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);

说实话,个人认为,这是一个极其智障的注释,但是是一个良好的设计,我们知道Mybatis的每一个Mapper对应一个命名空间,由于Mybatis防止资源重复解析是通过Configuration的loadedResources注册表完成的,该注册表是一个String的Set,如果该注册表存入的是资源名称,那么就会出现很大的问题。命名空间就将具体的文件与配置进行了解耦。

既然已经提到了基于注解的Mapper标签解,那么让我们进入下一节:《7.Java注解方式配置的mapper标签的解析》。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值