MyBatis系列2_启动分析

1,871 阅读16分钟

MyBatis启动

org.apache.ibatis.session.Configuration

这个类算是MyBatis运行过程中最核心的类了,没有之一。它类似于MyBatis的上下文概念,贯彻了MyBatis的整个生命周期,它存放了MyBatis的基础配置信息,以及在运行时执行sql的MapperStatement。

MyBatis在正式执行前就是初始化该类,并往里面设置数据。

2023-04-17-21-13-31-image.png

可以看到无论是何种方式使用MyBatis框架,第一件事就是创建生成Configuration。

Configuration的重要属性

Configuration有很多属性,可以看到很多mybatis-config.xml的身影,如的cacheEnabled、useGeneratedKeys、logImpl、defaultExecutorType...具体可以参考MyBatis官方文档对配置信息的说明:MyBatis配置。获取创建好的Configuration可以通过sqlSessionFactory.getConfiguration();来进行获取。

在配置文件中有的,我们就不做多分析了,官方文档有具体的说明,我们分析一下常用并且在配置文件中没有的属性。

mappedStatements

protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection") .conflictMessageProducer((savedValue, targetValue) -> ". please check " + savedValue.getResource() + " and " + targetValue.getResource());

在使用MyBatis的时候你有好奇过它是啥时候解析你的Mapper.xml吗?

你有好奇过它是执行一次sql是否就要读取一次Mapper.xml吗?

你有好奇过它参数组装?传参返回结果?include是怎么解析的吗?

通过分析这个这个属性的创建就能得到所有答案。

实际MyBatis在启动过程中会将每个要执行sql的方法都生成一个对应的MappedStatement对象,并将它添加到Configuration的mappedStatements中,它是通过解析注解或者xml进行生成的。

如mapper.xml中:就会生成一个MappedStatemnt,mapper.xml的生成逻辑可以查看XMLMapperBuilder#parse解析生成的。

<select id="selectConfigById" parameterType="Long" resultMap="SysConfigResult">
        <include refid="selectConfigVo"/>
        where config_id = #{configId}
</select>

在注解中是通过MapperAnnotationBuilder#parse解析生成的。

     @Delete("delete from user where id=#{id}")  //删除
    public void delete(int id);

无论是XMLMapperBuilder还是MapperAnnotationBuilder它们都依赖了一个MapperBuilderAssistant实例,这个实例用于在提供MappedStatement的一些帮助,如创建MappedStatement,并添加到Configuration。

在创建完所有要配置的MappedStatement对象后,具体后面执行都是获取对应的MappedStatement对象,然后通过它可以获取相关解析好的SQL语句,对应的API为MappedStatement#getSqlSource#getBoundSql。

sqlFragments

protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");

这也是一个比较有意思属性,我们通常用的标签,里面的sql片段就是存储在这个属性的。

<sql id="selectConfigVo">select config_id, config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark FROM ry.sys_config</sql>
<select id="selectConfigById" parameterType="Long" resultMap="SysConfigResult">
        <include refid="selectConfigVo"/>
        where config_id = #{configId}
</select>

它是通过XMLMapperBuilder#configurationElement里面的sqlElement(context.evalNodes("/mapper/sql"));解析的,解析的结果就是存储到Configuration的sqlFragments中的,这也就是为什么mapper之间可以通过互相引用它们的片段。

另外提一嘴,<include>标签实际底层原理是复制<sql>标签xml节点,然后插入替换掉当前的<include>标签来实现的,具体源码可以参见XMLIncludeTransformer#applyIncludes(org.w3c.dom.Node)

其它

Configuration还有非常多的属性,如:

  • languageRegistry,用于注册一个脚本语言解析驱动,默认的有XMLLanguageDriver,它决定在解析sql片段的时候如何解析,如何创建SqlSource,比如我们要实现一个非XML结构的mybatis的mapper解析器,就可以自定义设置这个属性。
  • resultMaps,全局的ResultMap,也可以通过namespace引用,和sqlFragements类似。
  • ....还有很多,我这只列举说明了在经常使用的一些属性,通过这些属性就可以分析出整个MyBatis的启动流程。

并且在Configuration类中,可以看到非常多MyBatis框架的相关功能具体的实现类,从而方便查阅源码:

2023-04-17-22-28-25-image.png 可以这样说Configuration描述了MyBatis框架的所有功能,以及具体实现入口,在看MyBatis源码的时候,只要找到关心的属性进行set方法或者调用的地方debug就可以了。

解析Mapper.xml

上面我们分析了MyBatis的启动到使用过程,大体可以总结为:

    1、读取配置信息(从mybatis-config.xml或者Spring通过MyBatisProperties属性注入,在MyBatisAutoConfiguration进行设置)

    2、根据配置信息生成Configuration实例

    3、将Configuration实例传给SqlSessionFactory,进行初始化数据库会话连接工厂

    4、通过SqlSessionFactory的实例获取到session就可以进行操作

这里每一步,都有SpringBoot或者MyBatis帮我们实现了,日常开发中更多的工作还是定义Mapper接口,以及在对应的mapper.xml中编写sql,mapper.xml中对sql编写非常灵活,也支持一些非常强大的功能,那么还是非常有必要了解一下,MyBatis是如何解析这么复杂的xml,并且它们之间是如何交互的。

解析Mapper.xml流程

在前面我们知道,解析mapper.xml只是真个MyBatis流程中的一部分,但是也是非常重要的一部分,它发生在创建Configuration阶段,会根据mapper.xml的sql片段生成MappedStatement,这个MappedStatement在前面有具体介绍。

解析mapper.xml的入口

2023-04-23-20-49-24-image.png

  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);
    }
  }

接下来我们针对常用的sql片段来进行分析MyBatis是如何实现复杂的mapper.xml解析的。在分析mapper.xml解析细节的的前提,一定要先知道一个类MapperBuilderAssistant这个类是解析mapper.xml的一个辅助类,很多对mapper.xml标签的解析,都是通过这个类来实现的。

cache-ref和cache

cache二级缓存这一块的解析没有上下文关联性,它的逻辑很简单,就是解析cache里面属性然后通过CacheBuilder创建一个Cache对象,并将其放入一个全局配置对象Configuration中,就完事。

cache-ref从Configuration中获取到Cache对象,维护一个引用对象,表示共享其它namespace的缓存对象。

// org.apache.ibatis.builder.MapperBuilderAssistant#useCacheRef
  public Cache useCacheRef(String namespace) {
    if (namespace == null) {
      throw new BuilderException("cache-ref element requires a namespace attribute.");
    }
    try {
      unresolvedCacheRef = true;  
      // 搭配下面的cache标签看
      Cache cache = configuration.getCache(namespace);
      if (cache == null) {
        throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
      }
      currentCache = cache;
      unresolvedCacheRef = false;
      return cache;
    } catch (IllegalArgumentException e) {
      throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
    }
  }

cache标签,主要是为当前mapper.xml生成一个Cache对象实例并放入Configuration中

核心源码为org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache

public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {  
    // 通过解析到在cache标签的属性,生成一个Cache对象
    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;
  }

这一步解析mapper.xml中的cache只是生成对应的Cache对象和维护好cache-ref,等到真正使用的时候才会起作用。

parameterMap

<parameterMap id="" type=""></parameterMap>

parameterMap是和resultMap有着相似功能的标签,都是描述java实体和数据列之间的映射关系,不过一个是输入(parameterMap),一个是输出(resultMap)。

实际开发中还是很少使用parameterMap的,一般都是用parameterType,除非没有具体对数据库表映射实体(一般不推荐这么做),或者两边完全没有规律可言,如以下场景可以使用该标签:

User.java

public class User {
    private String uName;
    private String uSix;
}

而数据库中字段为name、six,此时实体和数据库列名称对不上,就需要手动映射了。

言归正传,我们来看解析parameterMap标签MyBatis做了什么:简单来说,创建了一个ParameterMap对象,存到了Configuration对象中,完事儿。

org.apache.ibatis.builder.xml.XMLMapperBuilder#parameterMapElement

// parameterMapElement(context.evalNodes("/mapper/parameterMap"));

private void parameterMapElement(List<XNode> list) {  
    // 解析mapper中的所有parameterMap,并生成ParameterMapping
    for (XNode parameterMapNode : list) {
      String id = parameterMapNode.getStringAttribute("id");
      String type = parameterMapNode.getStringAttribute("type");
      Class<?> parameterClass = resolveClass(type);
      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");
        String jdbcType = parameterNode.getStringAttribute("jdbcType");
        String resultMap = parameterNode.getStringAttribute("resultMap");
        String mode = parameterNode.getStringAttribute("mode");
        String typeHandler = parameterNode.getStringAttribute("typeHandler");
        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 = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);
        parameterMappings.add(parameterMapping);
      }

      // 将ParamterMapping转换成ParameterMap添加到到Configuration中
      builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
    }
  }
// org.apache.ibatis.builder.MapperBuilderAssistant#addParameterMap
  public ParameterMap addParameterMap(String id, Class<?> parameterClass, List<ParameterMapping> parameterMappings) {
    id = applyCurrentNamespace(id, false);
    ParameterMap parameterMap = new ParameterMap.Builder(configuration, id, parameterClass, parameterMappings).build();
    configuration.addParameterMap(parameterMap);
    return parameterMap;
  }

当后面解析到DDM(select | insert | update | delete)语句的时候,解析到parameterMap属性的时候,就从Configuration中获取到对应的ParameterMap对象,根据映射关系设置值。

resultMap

resultMap常用,它跟parameterMap一样用来描述查询结果的返回列与java对象实体映射关系,它的解析流程跟paramterMap也差不多。

但是呢,由于parameterMap是根据已有实例进行映射转换,resultMap需要将查询结果转换成对象实例所以需要创建对象,因此resultMap的解析要稍微麻烦一点,涉及到constructor的解析,对象继承体系等的解析,但是最后都是一样的原理,就是生成了一个ResultMap对象,并添加到Configuration中

//  resultMapElements(context.evalNodes("/mapper/resultMap"));
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    String type = resultMapNode.getStringAttribute("type",      
        resultMapNode.getStringAttribute("ofType",
            resultMapNode.getStringAttribute("resultType",
                resultMapNode.getStringAttribute("javaType"))));
    Class<?> typeClass = resolveClass(type);
    if (typeClass == null) {
      typeClass = inheritEnclosingType(resultMapNode, enclosingType);
    }
    Discriminator discriminator = null;
    List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
    List<XNode> resultChildren = resultMapNode.getChildren();
    for (XNode resultChild : resultChildren) {
      if ("constructor".equals(resultChild.getName())) {
        processConstructorElement(resultChild, typeClass, resultMappings);
      } else if ("discriminator".equals(resultChild.getName())) {
        discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
      } else {
        List<ResultFlag> flags = new ArrayList<>();
        if ("id".equals(resultChild.getName())) {
          flags.add(ResultFlag.ID);
        }
        resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
      }
    }
    String id = resultMapNode.getStringAttribute("id",
            resultMapNode.getValueBasedIdentifier());
    String extend = resultMapNode.getStringAttribute("extends");
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
      return resultMapResolver.resolve();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteResultMap(resultMapResolver);
      throw e;
    }
  }

org.apache.ibatis.builder.MapperBuilderAssistant#addResultMap   
public ResultMap addResultMap(
      String id,
      Class<?> type,
      String extend,
      Discriminator discriminator,
      List<ResultMapping> resultMappings,
      Boolean autoMapping) {
   ....
    // 创建ResultMap,添加到Configuration
    ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
        .discriminator(discriminator)
        .build();
    configuration.addResultMap(resultMap);
    return resultMap;
  }

使用场景跟parameterMap也是一样的在解析DDM语句的时候,会通过id从Configuration实例中获取到对应ResultMap,并生成对象。

sql片段解析

标签也是属于声明式的标签,它的主要作用是,在其它地方引用,因此根据以上的经验,基本上可以确定对该片段的解析也就是,将片段的信息,存放到Configuration实例中,在需要引用的时候再获取使用。

// 将<sql>片段,存储sqlFragments中
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      String databaseId = context.getStringAttribute("databaseId");
      String id = context.getStringAttribute("id");
      id = builderAssistant.applyCurrentNamespace(id, false);
      if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
        sqlFragments.put(id, context);
      }
    }
  }

// 获取<sql>片段
public XNode getSqlFragment(String refid) {
   return sqlFragments.get(refid);
}

我们可以看到,在sql片段解析结果是存放到sqlFragments这个属性中的,它的声明如下:

 private final Map<String, XNode> sqlFragments;

刚刚有说到不是将sql片段存放到Configuration实例对象中嘛,怎么放到sqlFragments属性中了呢?那么这个sqlFragments属性是从何而来的呢?

这个就要看XMLMapperBuilder的构造器

  private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    super(configuration);
    this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
    this.parser = parser;
    this.sqlFragments = sqlFragments;
    this.resource = resource;
  }

可以看到这个sqlFragments属性是通过构造器传入的,然后回到XMLConfigBuilder#mapperElement方法,可以看到创建XMLMapperBuilder的代码:

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

可以看到sqlFragments是通过configuration.getSqlFragments() 传入的,实际中也是在Configuration实例对象中,那么为什么要这样使用呢,而不是像parameterMap和resultMap直接通过configuration引用添加呢?这是因为的refid可以通过namespace引入其它的mapper文件的sql片段,不限于当前mapper.xml,这样可以进行全局添加。

DDM语句解析

前面分析了解析Mapper.xml的几个全局标签,接下来我们分析分析开发过程中最常用的标签,即DDM语句的标签<select>、<insert>、<update>、<delete> 。这些标签对应了mapper接口方法,它们的解析在全局标签之后,因为它们会引入或者引用全局标签。

它们入口同样位于:XMLMapperBuilder#configurationElement

 // XMLMapperBuilder#configurationElement
 buildStatementFromContext(context.evalNodes("select|insert|update|delete"));


  private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }

  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

可以看到源码中,针对DDM语句,专门有一个类来进行解析,就是XMLStatementBuilder,调用它的statementParser.parseStatementNode(); 方法进行解析。

statementParser.parseStatementNode(); 这个方法中我们会看到非常多熟悉的属性:

  public void parseStatementNode() {
    // 解析id和databaseId
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    // 如果不匹配当前数据库方言,则不做解析sql片段
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    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标签,用sql标签替换include处的标签
    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

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

    // 解析语言驱动,默认就是XMLLanguageDriver,用于决定解析动态sql节点,ddm语句下的所有子标签节点都是由它来确定解析的
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // 解析selectKey标签,常用于生成id
    // Parse selectKey after includes and remove them.
    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 = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    // 通过上面的LanguageDriver去解析标签里面的原生,生成SqlSource。这个SqlSource包含了预编译sql,也就是说是这里解析标签并生成sql语句
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

    // 下面就是对一些属性进行取值
    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");
    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");

    // 通过MapperBuilderAssistant往Configuration里面添加一个MappedStatement对象
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

可以看到上面解析ddm语句的核心逻辑除了对一些属性取值外,最核心的就在于对标签解析和解析标签生成sql脚本:

    // 解析include标签,用sql标签替换include处的标签
    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // 通过上面的LanguageDriver去解析标签里面的原生,生成SqlSource。这个SqlSource包含了预编译sql,也就是说是这里解析标签并生成sql语句
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

注意:这里的context对象就是对应<select|update|insert|delete>的xml节点对象

include标签解析

标签搭配着标签 一起使用,从而达到对sql语句的复用目的。

    <sql id="selectDeptVo">
        select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time 
        from sys_dept d
    </sql>   
    <select id="selectDeptList" parameterType="SysDept" resultMap="SysDeptResult">
        <include refid="selectDeptVo"/>
        where d.del_flag = '0'
    </select>    

日常中我们使用的也挺多,接下来我们分析分析MyBatis是如何解析include的,在分析include之前要先了解前面标签的解析,回顾一下,前面说道对标签解析,会将读取到xml中的节点放入Configuration实例中。 在include会根据refid从Configuration实例中获取到节点,并将其复制一份里面内容将其替换掉原来的节点,从而完成标签的解析处理。

接下来看源码解析:

    // org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
    // 在实际解析xml节点之前,先解析<include>标签
    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

// 替换include    
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
    if ("include".equals(source.getNodeName())) {
      // 根据include的refid从Configuration中获取<sql>节点 
      Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
      Properties toIncludeContext = getVariablesContext(source, variablesContext);
      // 处理<include>节点里面的<include>标签
      applyIncludes(toInclude, toIncludeContext, true);
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      //  执行替换,用sql节点替换掉include节点
      source.getParentNode().replaceChild(toInclude, source);
      while (toInclude.hasChildNodes()) {
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      // 最后移除<sql>节点
      toInclude.getParentNode().removeChild(toInclude);
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
      // 进入该分支,已经不是include第一层了
      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();
      // 可能包含多个include子节点,继续解析
      for (int i = 0; i < children.getLength(); i++) {
        applyIncludes(children.item(i), variablesContext, included);
      }
    } else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
        && !variablesContext.isEmpty()) {
      // replace variables in text node
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
  }

总的来说,对include标签的解析是一种替换操作,但是替换过程包含了属性解析和递归替换,看起来稍微复杂点,但是debug跟一下代码就可以了。

SQL语句解析

前面的所有节点解析,都是为了最后一步,因为搞那么多功能,最终也是为了生成一个完整的sql语句,实际从XML中分析生成一个预编译SQL语句的地方就是这一步完成的,也就是MyBatis中创建SqlSource

具体生成SQL语句需要经历,标签解析->转换为SqlNode对象->根据参数调用SqlNode动态生成拼接SQL语句。

这个SqlSource是一个接口,会返回一个BoundSql对象,而BoundSql描述了一个sql以及参数信息。最后老规矩将SqlSource的实例包装到MappedStatement中,放入到Configuration实例中。

public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

} 

public class BoundSql {

  private final String sql;
  private final List<ParameterMapping> parameterMappings;
  private final Object parameterObject;
  private final Map<String, Object> additionalParameters;
  private final MetaObject metaParameters;

}

SqlSource有如下的实现类:

2023-04-25-21-42-11-image.png

分别是动态、注解、原生、静态的SqlSource。

对外暴露的一般就是DynamicSqlSource、ProviderSqlSource、RawSqlSource。StaticSqlSource其实和RawSqlSource是搭配使用的,这两个都是描述该sql是一个静态sql,不存在动态标签。

动态标签指的是if、where、foreach....这种标签

解析源码分析:

    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang); 
    // 创建SqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

由语言驱动进行创建,默认的语言驱动MyBatis默认是使用XMLLanguageDriver来解析的。

2023-04-25-21-28-28-image.png

在XMLLanguageDriver中,用了XMLScriptBuilder来解析具体的sql语句:

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

在XMLScriptBuilder中的parseScriptNode()方法:

org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
  public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }
标签解析 parseDynamicTags

可以看到最后返回了一个动态sqlSource和原生的sqlSource,具体返回哪一个是根据parseDynamicTags(context) 解析结果判断的。而这个方法就是解析动态节点的入口,也是核心,如if、where、set等这些标签都算是动态节点。

具体处理动态节点的处理类:

  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());
  }

从以上源码可以看到,每一个动态标签都对应了一个处理类,这些处理类都是NodeHandler接口的实现类

  private interface NodeHandler {
    /**
          nodeToHandle 当前动态节点,如<if>....</if>
          targetContents  将xml中node节点也就是参数nodeToHandle进行处理转换成SqlNode加入到这个集合中
                          这个集合描述了整个整个sql片段中的所有node节点
                          如<select> 
                                  select * from tab_name where 
                                  <where>
                                       <if test="configId !=null">and config_id = #{configId}</if>  
                                  </where>
                             </select>
                          这一段select将全部被转为SqlNode的实例,放入到targetContents集合中
    */
    void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
  }

调用NodeHandler将xml的节点转为SqlNode对象:

  protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    NodeList children = node.getNode().getChildNodes();
    // 遍历子节点,如果是动态节点,则通过NodeHandler递归调用解析
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      // 文本节点,如 select * from tab_name where ,在w3c-dom中也会被解析为一个节点
      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 {
          // 纯静态文本节点,就是一个字符串,可以直接拼接的那种
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        // 非文本节点,也就是<where><if>这种节点则需要通过获取到对应的Handler来处理,这个Handler是由initNodeHandlerMap维护的
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        // 使用具体的handler来解析动态节点,在handler里面也是会调用这个方法的,因此才说使用了递归调用
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }

parseDynamicTags方法的作用就是解析一个sql节点,将里面的所有内容都转换为SqlNode的实现类。这个转换操作是由具体的Handler来操作的,也就是前面的initNodeHandlerMap初始化映射实例化的Handler。

SqlNode实现类如下:

2023-04-25-22-05-29-image.png

我们随便挑一个简单的SqlNode实现类来分析,如IfSqlNode,他负责解析<if test="orgId!=null">.....</if>这样的语句:

private class IfHandler implements NodeHandler {
    public IfHandler() {
      // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {  
      // 递归调用parseDynamicTags方法解析<if>下面的子节点
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      // 取出当前if的条件表达式,创建IfSqlNode实例,并将其添加到targetContents中
      String test = nodeToHandle.getStringAttribute("test");
      IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
      targetContents.add(ifSqlNode);
    }
  }

我们可能会有疑问,因为在通常xml节点定义的时候子节点是由先后和嵌套关系的,这种一股脑丢给targetContens最后解析的时候不是变得无序了嘛?这时候就不得不提一个辅助SqlNode就是MixedSqlNode在分析SqlNode实现类的时候,每个实现类都能找到标签,但是这个找不到,是因为这个MixedSqlNode是不对外使用的它就是为了维护SqlNode直接的嵌套层次顺序关系的。

简单来说这个MixedSqlNode就是一个复合型SqlNode,没有具体含义,单纯维护标签转换成SqlNode的层次关系,该节点在parseDynamicTags方法返回的时候创建:

public class MixedSqlNode implements SqlNode {
  private final List<SqlNode> contents;

  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }

  @Override
  public boolean apply(DynamicContext context) {
    contents.forEach(node -> node.apply(context));
    return true;
  }
}

例如我们这样一段xml:

    <select id="selectRoleDeptTree" parameterType="Long" resultType="String">
        select concat(d.dept_id, d.dept_name) as dept_name
        from sys_dept d
            left join sys_role_dept rd on d.dept_id = rd.dept_id
        <where>
             d.del_flag = '0' 
             <if test="roleId!=null">
                 AND rd.role_id = #{roleId}
             </if>
        </where>

        order by d.parent_id, d.order_num
    </select>

最后生成的SqlNode结构为:

MixedSqlNode

    [0]StaticTextSqlNode select concat(d.dept_id, d.dept_name) as dept_name from sys_dept d ....

    [1]WhereSqlNode

        StaticTextSqlNode d.del_flag = '0'

        IfSqlNode 条件:roleId!=null

            TextSqlNode AND rd.role_id = #{roleId}

    [2]StaticTextSqlNode order by d.parent_id, d.order_num

以上描述了调用parseDynamicTags方法后,返回的MixedSqlNode的一个层次关系。

最后将解析的结果生成对应SqlSource,用于创建MappedStatement放入Configuration中。

注意:此时还没有生成sql语句,只是将xml中的sql标签转为了一个个SqlNode对象放入了Configuration对象中,等到真正执行的时候回调用这些SqlNode的实例进行sql语句拼接