MyBatis中SQL解析原理

前面在MyBatis中动态SQL执行过程一文中介绍了MyBatis在运行时大体的运行过程。其中有介绍到通过MappedStatement来获取SQL语句,不过没有展开分析。本文就接着来剖析一下,一段带有动态XML标签的文本是如何被MyBatis解析成SQL字符串的。


MappedStatement

在服务(与Spring Boot整合的情况,本文也只会分析这种情况)启动时,会构建SqlSessionFactory,而在这个构建过程中,会解析主配置configLocation指向路径下的mapper文件并为其中的四种SQL语句创建MappedStatement对象。所以本文先来介绍一下SqlSessionFactory的构建。

构建SqlSessionFactory

v3.x
MybatisAutoConfiguration
SqlSessionFactoryBean
<
>
java
mybatis-spring-boot-2.3.2/mybatis-spring-boot-autoconfigure/src/main/java/org/mybatis/spring/boot/autoconfigure/MybatisAutoConfiguration.java
/*
 * 创建sqlSessionFactory
 */
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
  // 创建FactoryBean
  SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
  // 设置数据源
  factory.setDataSource(dataSource);
  // 设置MyBatis使用的虚拟文件系统类型
  factory.setVfs(SpringBootVFS.class);
  // 如果配置了主配置文件的位置
  if (StringUtils.hasText(this.properties.getConfigLocation())) {
    // 设置主配置文件的位置
    factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
  }
  // 将Configuration对象设置到factory的属性中
  applyConfiguration(factory);
  if (this.properties.getConfigurationProperties() != null) {
    factory.setConfigurationProperties(this.properties.getConfigurationProperties());
  }
  // 将拦截器设置到工厂bean中,最后会设置到Configuration对象中,如果设置了的话
  if (!ObjectUtils.isEmpty(this.interceptors)) {
    factory.setPlugins(this.interceptors);
  }
  if (this.databaseIdProvider != null) {
    factory.setDatabaseIdProvider(this.databaseIdProvider);
  }
  if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
    factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
  }
  if (this.properties.getTypeAliasesSuperType() != null) {
    factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
  }
  // 设置类型处理器的包名,如果设置了的话
  if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
    factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
  }
  // 设置类型处理器,如果设置了的话
  if (!ObjectUtils.isEmpty(this.typeHandlers)) {
    factory.setTypeHandlers(this.typeHandlers);
  }
  // 解析mapper文件的位置
  Resource[] mapperLocations = this.properties.resolveMapperLocations();
  if (!ObjectUtils.isEmpty(mapperLocations)) {
    // 设置mapper文件的位置
    factory.setMapperLocations(mapperLocations);
  }
  // 获取SqlSessionFactoryBean类中的字段名称
  Set<String> factoryPropertyNames = Stream
      .of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName)
      .collect(Collectors.toSet());
  // 获取默认的脚本语言驱动,默认是null
  Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
  // 默认情况下this.languageDrivers是null,所以不满足这里的if条件
  if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
    // Need to mybatis-spring 2.0.2+
    factory.setScriptingLanguageDrivers(this.languageDrivers);
    if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
      defaultLanguageDriver = this.languageDrivers[0].getClass();
    }
  }
  if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
    // Need to mybatis-spring 2.0.2+
    factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
  }
  // 默认没有操作
  applySqlSessionFactoryBeanCustomizers(factory);
  // 创建SqlSessionFactory
  return factory.getObject();
}
java
mybatis-spring/src/main/java/org/mybatis/spring/SqlSessionFactoryBean.java
@Override
public SqlSessionFactory getObject() throws Exception {
  // 如果还没创建对象,则创建
  if (this.sqlSessionFactory == null) {
    afterPropertiesSet();
  }

  return this.sqlSessionFactory;
}
@Override
public void afterPropertiesSet() throws Exception {
  notNull(dataSource, "Property 'dataSource' is required");
  notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
  // configuration和configLocation不能同时为空,也不能同时指定
  state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
      "Property 'configuration' and 'configLocation' can not specified with together");

  // 构建SqlSessionFactory
  this.sqlSessionFactory = buildSqlSessionFactory();
}
/*
 * 这个方法绝大部分都在构建和设置Configuration对象,这是整个MyBatis的顶层核心类,贯穿启动时和运行时。
 */
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

  // 记录MyBatis的配置
  final Configuration targetConfiguration;

  // 用于解析xml配置文件
  XMLConfigBuilder xmlConfigBuilder = null;
  if (this.configuration != null) { // 如果指定了configuration对象,则直接使用;对应了代码配置;
    targetConfiguration = this.configuration;
    if (targetConfiguration.getVariables() == null) {
      targetConfiguration.setVariables(this.configurationProperties);
    } else if (this.configurationProperties != null) {
      targetConfiguration.getVariables().putAll(this.configurationProperties);
    }
  } else if (this.configLocation != null) { // 如果指定了配置文件的路径,则解析配置文件;对应了XML文件配置;
    xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
    /*
     * 注意,这里获取到的是一个新创建的配置对象,还没有执行配置文件的解析,下面才会调用builder对象的parse方法来解析
     * 但实际上,从这里到下面调用parse方法之间没有对xmlConfigBuilder的额外操作,那为什么不在这里就解析?
     * 应该是因为避免下面以及在parse方法之前的一些操作覆盖了配置文件中的内容。
     */
    targetConfiguration = xmlConfigBuilder.getConfiguration();
  } else { // 如果两种配置方式都没使用,则创建空的配置对象
    LOGGER.debug(
        () -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
    targetConfiguration = new Configuration();
    Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
  }

  Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);
  Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory);
  Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl);

  // 扫描类型别名
  if (hasLength(this.typeAliasesPackage)) {
    scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream()
        .filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface())
        .filter(clazz -> !clazz.isMemberClass()).forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);
  }

  if (!isEmpty(this.typeAliases)) {
    Stream.of(this.typeAliases).forEach(typeAlias -> {
      targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
      LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");
    });
  }

  // 设置插件
  if (!isEmpty(this.plugins)) {
    Stream.of(this.plugins).forEach(plugin -> {
      // 添加插件到Configuration对象中
      targetConfiguration.addInterceptor(plugin);
      LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
    });
  }

  // 设置类型处理器
  if (hasLength(this.typeHandlersPackage)) {
    scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass())
        .filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
        .forEach(targetConfiguration.getTypeHandlerRegistry()::register);
  }

  if (!isEmpty(this.typeHandlers)) {
    Stream.of(this.typeHandlers).forEach(typeHandler -> {
      targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
      LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");
    });
  }

  // 设置默认的枚举类型处理器
  targetConfiguration.setDefaultEnumTypeHandler(defaultEnumTypeHandler);

  // 设置脚本语言驱动
  if (!isEmpty(this.scriptingLanguageDrivers)) {
    Stream.of(this.scriptingLanguageDrivers).forEach(languageDriver -> {
      targetConfiguration.getLanguageRegistry().register(languageDriver);
      LOGGER.debug(() -> "Registered scripting language driver: '" + languageDriver + "'");
    });
  }
  /*
   * 这里一般不会设置,而是在XMLConfigBuilder中调用的Configuration的setDefaultScriptingLanguage
   * 如果这里有设置,会覆盖掉上面设置的
   */
  Optional.ofNullable(this.defaultScriptingLanguageDriver)
      .ifPresent(targetConfiguration::setDefaultScriptingLanguage);

  // 设置数据库ID(有何作用?)
  if (this.databaseIdProvider != null) {// fix #64 set databaseId before parse mapper xmls
    try {
      targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
    } catch (SQLException e) {
      throw new IOException("Failed getting a databaseId", e);
    }
  }

  Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache);

  if (xmlConfigBuilder != null) {
    try {
      // 解析MyBatis的主配置(xml)文件
      xmlConfigBuilder.parse();
      LOGGER.debug(() -> "Parsed configuration file: '" + this.configLocation + "'");
    } catch (Exception ex) {
      throw new IOException("Failed to parse config resource: " + this.configLocation, ex);
    } finally {
      ErrorContext.instance().reset();
    }
  }

  // 创建并设置环境对象,注意不是Spring中的Environment,而是MyBatis自己定义的类
  targetConfiguration.setEnvironment(new Environment(this.environment,
      // 创建并设置为Spring的事务工厂类
      this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
      this.dataSource));

  /*
   * 处理mapper的xml文件,注意别和configLocation搞混了。
   */
  if (this.mapperLocations != null) {
    if (this.mapperLocations.length == 0) { // 找不到指定的mapper配置
      LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
    } else {
      // 遍历mapper文件
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }
        try {
          // 创建解析器(每个mapper文件对应一个解析器),注意不是上面解析主配置文件的XMLConfigBuilder,别搞混了。
          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
          // 解析mapper文件
          xmlMapperBuilder.parse();
        } catch (Exception e) {
          throw new IOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
        } finally {
          ErrorContext.instance().reset();
        }
        LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
      }
    }
  } else {
    LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
  }

  // 构建SqlSessionFactory,configuration对象会被设置到factory对象中
  return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}

MyBatis使用了经典的FactoryBean来构建SqlSessionFactory,在其afterPropertiesSet阶段,执行了真正的构建过程。在该过程中涉及了两种文件的解析:

  • 通过XMLConfigBuilder来解析主配置文件;
  • 通过XMLMapperBuilder来解析mapper文件; 与本文主题相关的是后者。

解析四种SQL语句

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/builder/xml/XMLMapperBuilder.java
public void parse() {
  // 如果没有解析过才解析
  if (!configuration.isResourceLoaded(resource)) {
    /*
     * 核心步骤:
     *  解析mapper节点及其子节点,
     *  主要的解析操作都在该方法中
     */
    configurationElement(parser.evalNode("/mapper"));
    // 添加被解析的mapper文件
    configuration.addLoadedResource(resource);
    // 解析命名空间并和当前mapper绑定,在运行时会通过这种映射关系找到mapper实现。
    bindMapperForNamespace();
  }

  /*
   * 解析一些pending状态(未完成解析)的标签,
   *  至于为什么会产生这种状态的配置,就要参考上面的配置解析过程了。(感觉大概是有因为这几种配置中可以存在跨文件引用)
   */

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}
/*
 * 解析mapper文件内容
 */
private void configurationElement(XNode context) {
  try {
    // 获取namespace属性,没有校验配置有效性(比如配置的mapper接口是否真的存在?),而是仅判断有无
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.isEmpty()) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    // 设置命名空间到帮助对象中
    builderAssistant.setCurrentNamespace(namespace);
    // 解析cache-ref节点
    cacheRefElement(context.evalNode("cache-ref"));
    // 解析cache节点
    cacheElement(context.evalNode("cache"));
    // 已废弃节点,可忽略
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    // 解析resultMap节点
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    // 解析sql节点
    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);
  }
}
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) {
    // 创建builder对象
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      // 解析语句节点
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

整个解析过程涉及非常多的内容,而我们现在只关心四种SQL语句的解析。在XMLMapperBuilder中,这部分操作是委托给XmlStatementBuilder来执行的。

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/builder/xml/XMLStatementBuilder.java
public void parseStatementNode() {
  // 获取两个属性
  String id = context.getStringAttribute("id");
  String databaseId = context.getStringAttribute("databaseId");

  // 判断databaseId是否匹配,不匹配则跳过不会解析
  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    return;
  }

  // 获取节点名称,如select、insert等
  String nodeName = context.getNode().getNodeName();
  // SQL语句类型,即标签语句名称的大写形式
  SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

  // 下面三个属性只有对于select语句才有效
  boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); // 默认值为false:表示在执行语句前清空一级和二级缓存
  boolean useCache = context.getBooleanAttribute("useCache", isSelect); // 默认值为true:表示使用二级缓存;但要主配置(Configuration)中开启了才有效。
  boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

  // Include Fragments before parsing
  // 解析include节点
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());

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

  /*
   * 获取语言驱动,大部分情况下都是默认情况,但支持自定义语言驱动来支持特定的SQL生成需求。
   */
  String lang = context.getStringAttribute("lang");
  LanguageDriver langDriver = getLanguageDriver(lang);

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

  /*
   * 创建sqlSource对象,用来处理SQL,在解析阶段会被封装在MappedStatement对象中(参考下面),
   *  后续运行时会被Executor用来创建BoundSQL对象,而该对象又会被用来获取实际执行的SQL。
   *  这里传入context(XML上下文)后会解析,然后判断创建哪种(动态还是静态)SqlSource,并把解析到的各种SqlNode保存到SqlSource对象中。
   */
  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");
  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对象,并将其存储到配置对象中
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered,
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

在上面的方法中,现在重点关注两个地方:

  • 构建SqlSourceSqlSource与SQL解析关系非常密切,是一个非常重要的接口,MappedStatement实际上也是将SQL解析委托给它来处理的。
  • 构建MappedStatement

构建SqlSource

MyBatis作为一个框架,提供了不错的扩展性。比如,除了使用MyBatis提供的默认动态SQL节点外,还可以自定义其他使用方式,只需实现对应的LanguageDriver即可。不过,在实际工作中,很少场景需要自定义的,大部分还是使用的默认提供的能力。

获取LanguageDriver

SqlSource是被LanguageDriver来构建的。

v3.x
XMLStatementBuilder
Configuration
LanguageDriverRegistry
<
>
java
mybatis-3/src/main/java/org/apache/ibatis/builder/xml/XMLStatementBuilder.java
private LanguageDriver getLanguageDriver(String lang) {
  Class<? extends LanguageDriver> langClass = null;
  // mapper文件指定了languageDriver
  if (lang != null) {
    langClass = resolveClass(lang);
  }
  // 默认情况
  return configuration.getLanguageDriver(langClass);
}
java
mybatis-3/src/main/java/org/apache/ibatis/session/Configuration.java
public void setDefaultScriptingLanguage(Class<? extends LanguageDriver> driver) {
  if (driver == null) {
    // 设置默认的driver类型
    driver = XMLLanguageDriver.class;
  }
  getLanguageRegistry().setDefaultDriverClass(driver);
}
public LanguageDriver getLanguageDriver(Class<? extends LanguageDriver> langClass) {
  if (langClass == null) {
    // 返回默认的语言驱动
    return languageRegistry.getDefaultDriver();
  }
  languageRegistry.register(langClass);
  return languageRegistry.getDriver(langClass);
}
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/LanguageDriverRegistry.java
public class LanguageDriverRegistry {

  private final Map<Class<? extends LanguageDriver>, LanguageDriver> LANGUAGE_DRIVER_MAP = new HashMap<>();

  private Class<? extends LanguageDriver> defaultDriverClass;

  public void register(Class<? extends LanguageDriver> cls) {
    if (cls == null) {
      throw new IllegalArgumentException("null is not a valid Language Driver");
    }
    MapUtil.computeIfAbsent(LANGUAGE_DRIVER_MAP, cls, k -> {
      try {
        return k.getDeclaredConstructor().newInstance();
      } catch (Exception ex) {
        throw new ScriptingException("Failed to load language driver for " + cls.getName(), ex);
      }
    });
  }

  public void register(LanguageDriver instance) {
    if (instance == null) {
      throw new IllegalArgumentException("null is not a valid Language Driver");
    }
    Class<? extends LanguageDriver> cls = instance.getClass();
    if (!LANGUAGE_DRIVER_MAP.containsKey(cls)) {
      LANGUAGE_DRIVER_MAP.put(cls, instance);
    }
  }

  public LanguageDriver getDriver(Class<? extends LanguageDriver> cls) {
    return LANGUAGE_DRIVER_MAP.get(cls);
  }

  public LanguageDriver getDefaultDriver() {
    return getDriver(getDefaultDriverClass());
  }

  public Class<? extends LanguageDriver> getDefaultDriverClass() {
    return defaultDriverClass;
  }

  public void setDefaultDriverClass(Class<? extends LanguageDriver> defaultDriverClass) {
    register(defaultDriverClass);
    this.defaultDriverClass = defaultDriverClass;
  }

}

一般情况下,项目中不会特地设置langDriver,而默认的设置会由XMLConfigBuilder在解析主配置过程中来触发,即调用Configuration中的setDefaultScriptingLanguage方法,而入参又是null,所以实际注册XMLLanguageDriver类型的driver,而该类型则是主角。

XMLScriptBuilder执行构建SqlSource

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/xmltags/XMLLanguageDriver.java
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  /*
   * 创建builder对象,一条语句对应一个builder
   * 实际的创建工作是委托给XMLScriptBuilder来执行的。
   */
  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  // 执行解析
  return builder.parseScriptNode();
}

没想到,实际的主角应该是XMLScriptBuilder(上面标题已埋下伏笔)。

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/xmltags/XMLScriptBuilder.java
public SqlSource parseScriptNode() {
  // 解析动态SQL标签,获取sqlNode根节点
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource;
  /*
   * 根据是否是动态SQL,创建不同的对象
   * 注意,如果语句内容中只有文本,即不包含其他动态SQL的XML标签,而且文本中不包含“${}”,则会认为是静态的,即使包含“#{}”,也不是动态的。
   */
  if (isDynamic) { // 动态SQL
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else { // 静态SQL
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}
protected MixedSqlNode parseDynamicTags(XNode node) {
  // 会把解析到的多个SqlNode封装为MixedSqlNode返回
  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));
    /*
     * 如果节点类型是TEXT_NODE类型
     * CDATA_SECTION_NODE是指XML中的一些特殊符号的转义表示,比如'>'被转义为'&gt;',就会被解析为这种类型的节点。
     */
    if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
      // 获取文本内容
      String data = child.getStringBody("");
      TextSqlNode textSqlNode = new TextSqlNode(data);
      /*
       * 若文本中包含${}占位符,则认为是动态节点
       * 注意,如果只有#{},则会创建StaticTextSqlNode,而不是TextSqlNode。
       */
      if (textSqlNode.isDynamic()) {
        // 可见如果是动态SQL,则添加的是TextSqlNode
        contents.add(textSqlNode);
        // 标记是动态语句
        isDynamic = true;
      } else {
        // 如果是非动态SQL,则添加的是StaticTextSqlNode
        contents.add(new StaticTextSqlNode(data));
      }
    }
    // 如果是节点类型,如if、when等,共8种语句中的
    else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
      // 获取节点名称
      String nodeName = child.getNode().getNodeName();
      // 获取节点对应的处理器,并判断是不是所支持的节点类型
      NodeHandler handler = nodeHandlerMap.get(nodeName);
      if (handler == null) {
        // 不是支持的节点,则抛出异常
        throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
      }
      // 处理子节点
      handler.handleNode(child, contents);
      // 只要是动态SQL节点,则直接标记为是动态SQL
      isDynamic = true;
    }
  }
  return new MixedSqlNode(contents);
}

这里根据SQL是否是动态的创建了不同的SqlSource实现类类型的对象。那这里的动态是否是指SQL中使用了变量?让我们来一探究竟! 答案在parseDynamicTags方法中,如果包含动态SQL的XML节点(如<if>),那么确实算作是动态SQL;但没有这些节点的话,则要由TextSqlNode来判断。

v3.x
TextSqlNode
GenericTokenParser
<
>
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/xmltags/TextSqlNode.java
public boolean isDynamic() {
  DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
  /*
   * 创建占位符为“${}”的解析器
   * 只要文本中存在占位符就能说明是动态SQL,但是这里只考虑了”${}“,而没有考虑”#{}“,为什么会这样?
   * 这就是MyBatis的专门设计吧,对#{}的处理应该在其他地方。
   */
  GenericTokenParser parser = createParser(checker);
  parser.parse(text);
  return checker.isDynamic();
}
private GenericTokenParser createParser(TokenHandler handler) {
  return new GenericTokenParser("${", "}", handler);
}
private static class DynamicCheckerTokenParser implements TokenHandler {

  private boolean isDynamic;

  public DynamicCheckerTokenParser() {
    // Prevent Synthetic Access
  }

  public boolean isDynamic() {
    return isDynamic;
  }

  @Override
  public String handleToken(String content) {
    // 只要存在token,就说明是动态SQL。
    this.isDynamic = true;
    /* 这里怎么返回null?岂不是会导致SQL字符串中被拼接“null”?
     * 该TokenHandler只有在TextSqlNode的isDynamic方法中会被使用,而且仅用来判断是否是动态SQL,不涉及SQL字符串的拼接。
     *  在拼接SQL的时候调的是BindingTokenParser这个处理器,而不是当前处理器。
     */
    return null;
  }
}
java
mybatis-3/src/main/java/org/apache/ibatis/parsing/GenericTokenParser.java
public class GenericTokenParser {

  private final String openToken;
  private final String closeToken;
  // 处理openToken和closeToken中间内容的处理器
  private final TokenHandler handler;

  public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
    this.openToken = openToken;
    this.closeToken = closeToken;
    this.handler = handler;
  }

  public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    // 找到第一个占位符
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    do {
      if (start > 0 && src[start - 1] == '\\') { // 转义字符的情况
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        // 跳过占位符
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          // 重置expression,便于处理下一个占位符
          expression.setLength(0);
        }
        // 将上次处理的末尾到当前这个开始占位符中间的内容拷贝
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        // 找到结束占位符
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\\') { // 转义符的情况
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            // 跳过该占位符
            offset = end + closeToken.length();
            // 寻找下一个结束占位符
            end = text.indexOf(closeToken, offset);
          } else {
            // 取得占位符中的参数名,并保存到expression中
            expression.append(src, offset, end - offset);
            break;
          }
        }
        /*
         * 如果没有找到结束占位符,则将剩下的内容直接全部拷贝,并退出循环,结束寻找
         * 所以说,如果只定义了开始占位符,没有定义结束占位符,并不会在MyBatis层面报错,而是让SQL本身就变成有错误的SQL。
         */
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          /*
           * 取得参数对应的值,并拼接在sql中,占位符中可能并不是简单的参数名(比如还会设置jdbcType,typeHandler等),所以需要handler来处理
           */
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      // 从offset处开始往后找下一个开始占位符
      start = text.indexOf(openToken, offset);
    } while (start > -1);
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
}

可以发现,如果文本中包含${},则会被认为是动态的。特别要注意的是,如果只包含#{},则不会被认为是动态的,而会被创建RawSqlSource,而不是DynamicSqlSource,关于两者有什么区别,在后续运行时阶段会介绍到。

XMLScriptBuilderparseDynamicTags方法中,动态SQL的各个部分会被解析为各种SqlNode,然后被封装在MixedSqlNode中,最后设置到SqlSource中。下面会单独介绍SqlNode

构建MappedStatement

铺垫了这么久,终于到了开头提到的MappedStatement

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java
// 该方法其实就是在创建一个MappedStatement对象,然后把各种属性设置在其中
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;

  // 创建用于构建statement的builder对象,其实这里往builder上设置的属性都会设置到MappedStatement对象中。
  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对象
  MappedStatement statement = statementBuilder.build();
  /*
   * 添加statement
   * 其实在启动时的解析阶段,就是为每个语句创建MappedStatement对象;
   * 在运行时,Executor从Configuration中获取MappedStatement对象,然后通过其内部的SqlSource对象来创建BoundSql对象。
   */
  configuration.addMappedStatement(statement);
  return statement;
}

这里其实没有做什么复杂的操作,就是创建MappedStatement实例,然后设置一些属性(尤其是SqlSource),最后注册到Configuration中就完了。

认识SqlNode

在介绍SQL构建之前,需要认识一下重要的接口SqlNode及其实现类。 在上面提到,一条(动态)SQL中的各个部分会被解析为各种SqlNode

v3.x
SqlNode
DynamicContext
<
>
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/xmltags/SqlNode.java
/*
 * 该接口表示了MyBatis中动态SQL中的XML标签节点,比如where,set等;
 */
public interface SqlNode {
  boolean apply(DynamicContext context);
}
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java
public class DynamicContext {

  public static final String PARAMETER_OBJECT_KEY = "_parameter";
  public static final String DATABASE_ID_KEY = "_databaseId";

  static {
    OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
  }

  // 动态SQL的参数映射
  private final ContextMap bindings;
  private final StringJoiner sqlBuilder = new StringJoiner(" ");
  private int uniqueNumber = 0;

  public DynamicContext(Configuration configuration, Object parameterObject) {
    if (parameterObject != null && !(parameterObject instanceof Map)) {
      MetaObject metaObject = configuration.newMetaObject(parameterObject);
      boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
      bindings = new ContextMap(metaObject, existsTypeHandler);
    } else {
      bindings = new ContextMap(null, false);
    }
    bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
  }

  public Map<String, Object> getBindings() {
    return bindings;
  }

  public void bind(String name, Object value) {
    bindings.put(name, value);
  }

  public void appendSql(String sql) {
    sqlBuilder.add(sql);
  }

  public String getSql() {
    return sqlBuilder.toString().trim();
  }

  public int getUniqueNumber() {
    return uniqueNumber++;
  }

  static class ContextMap extends HashMap<String, Object> {
    private static final long serialVersionUID = 2977601501966151582L;
    private final MetaObject parameterMetaObject;
    private final boolean fallbackParameterObject;

    public ContextMap(MetaObject parameterMetaObject, boolean fallbackParameterObject) {
      this.parameterMetaObject = parameterMetaObject;
      this.fallbackParameterObject = fallbackParameterObject;
    }

    @Override
    public Object get(Object key) {
      String strKey = (String) key;
      if (super.containsKey(strKey)) {
        return super.get(strKey);
      }

      if (parameterMetaObject == null) {
        return null;
      }

      if (fallbackParameterObject && !parameterMetaObject.hasGetter(strKey)) {
        return parameterMetaObject.getOriginalObject();
      } else {
        // issue #61 do not modify the context when reading
        return parameterMetaObject.getValue(strKey);
      }
    }
  }

  static class ContextAccessor implements PropertyAccessor {

    @Override
    public Object getProperty(Map context, Object target, Object name) {
      Map map = (Map) target;

      Object result = map.get(name);
      if (map.containsKey(name) || result != null) {
        return result;
      }

      Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
      if (parameterObject instanceof Map) {
        return ((Map)parameterObject).get(name);
      }

      return null;
    }

    @Override
    public void setProperty(Map context, Object target, Object name, Object value) {
      Map<Object, Object> map = (Map<Object, Object>) target;
      map.put(name, value);
    }

    @Override
    public String getSourceAccessor(OgnlContext arg0, Object arg1, Object arg2) {
      return null;
    }

    @Override
    public String getSourceSetter(OgnlContext arg0, Object arg1, Object arg2) {
      return null;
    }
  }
}

该接口只定义了一个方法,该方法在构建SQL时会被调用,用来提供各个部分对应的实际SQL的内容片段。整个解析过程中的中间数据会被保存在DynamicContext中,包括SQL字符串的拼接过程。

  • TextNode:表示包含的${}纯文本SQL片段;
  • StaticTextSqlNode:表示不包含的${}的纯文本SQL片段;
  • IfSqlNode:用于处理<if>标签;
  • ForEachSqlNode:用于处理<foreach>标签;
  • TrimSqlNode:用于处理<trim>标签;
  • SetSqlNode:用于处理<set>标签;
  • WhereSqlNode:用于处理<where>标签;
  • ChooseSqlNode:用于处理<choose>标签;
  • VarDeclSqlNode:用于处理<bind/>标签,该标签可以用来定义一些变量,在需要多处引用复杂的表达式时很有用;
  • MixedSqlNode:用于封装多个或多种SqlNode对象;

下面分别是各种SqlNode实现类的源码,逻辑比较简单,不再介绍。

v3.x
TextSqlNode
StaticTextSqlNode
IfSqlNode
ForEachSqlNode
TrimSqlNode
SetSqlNode
WhereSqlNode
ChooseSqlNode
VarDeclSqlNode
MixedSqlNode
<
>
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/xmltags/TextSqlNode.java
/*
 * TextSqlNode的设计目标是为了处理占位符${},这也是和StaticTextSqlNode不同的地方。
 * 只要包含${},就会被认为是动态SQL,而只包含#{}(且没有其他动态SQL XML节点)则不会。
 */
public class TextSqlNode implements SqlNode {
  private final String text;
  private final Pattern injectionFilter;

  public TextSqlNode(String text) {
    this(text, null);
  }

  public TextSqlNode(String text, Pattern injectionFilter) {
    this.text = text;
    this.injectionFilter = injectionFilter;
  }



  public boolean isDynamic() {
    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
    /*
     * 创建占位符为“${}”的解析器
     * 只要文本中存在占位符就能说明是动态SQL,但是这里只考虑了”${}“,而没有考虑”#{}“,为什么会这样?
     * 这就是MyBatis的专门设计吧,对#{}的处理应该在其他地方。
     */
    GenericTokenParser parser = createParser(checker);
    parser.parse(text);
    return checker.isDynamic();
  }



  @Override
  public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    context.appendSql(parser.parse(text));
    return true;
  }



  private GenericTokenParser createParser(TokenHandler handler) {
    return new GenericTokenParser("${", "}", handler);
  }



  private static class BindingTokenParser implements TokenHandler {

    private DynamicContext context;
    private Pattern injectionFilter;

    public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
      this.context = context;
      this.injectionFilter = injectionFilter;
    }

    @Override
    public String handleToken(String content) {
      // 获取待绑定的参数
      Object parameter = context.getBindings().get("_parameter");
      if (parameter == null) {
        context.getBindings().put("value", null);
      } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) { // 如果参数是简单类型
        context.getBindings().put("value", parameter);
      }
      // 执行参数绑定
      Object value = OgnlCache.getValue(content, context.getBindings());
      String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
      checkInjection(srtValue);
      return srtValue;
    }

    private void checkInjection(String value) {
      if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
        throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
      }
    }
  }



  private static class DynamicCheckerTokenParser implements TokenHandler {

    private boolean isDynamic;

    public DynamicCheckerTokenParser() {
      // Prevent Synthetic Access
    }

    public boolean isDynamic() {
      return isDynamic;
    }

    @Override
    public String handleToken(String content) {
      // 只要存在token,就说明是动态SQL。
      this.isDynamic = true;
      /* 这里怎么返回null?岂不是会导致SQL字符串中被拼接“null”?
       * 该TokenHandler只有在TextSqlNode的isDynamic方法中会被使用,而且仅用来判断是否是动态SQL,不涉及SQL字符串的拼接。
       *  在拼接SQL的时候调的是BindingTokenParser这个处理器,而不是当前处理器。
       */
      return null;
    }
  }



}
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/xmltags/StaticTextSqlNode.java
public class StaticTextSqlNode implements SqlNode {
  private final String text;

  public StaticTextSqlNode(String text) {
    this.text = text;
  }

  @Override
  public boolean apply(DynamicContext context) {
    context.appendSql(text);
    return true;
  }

}
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/xmltags/IfSqlNode.java
public class IfSqlNode implements SqlNode {
  private final ExpressionEvaluator evaluator;
  private final String test;
  private final SqlNode contents;

  public IfSqlNode(SqlNode contents, String test) {
    this.test = test;
    this.contents = contents;
    this.evaluator = new ExpressionEvaluator();
  }

  @Override
  public boolean apply(DynamicContext context) {
    // 表达式为true,才应用子SqlNode
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }

}
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java
public class ForEachSqlNode implements SqlNode {
  public static final String ITEM_PREFIX = "__frch_";

  private final ExpressionEvaluator evaluator;
  private final String collectionExpression;
  private final Boolean nullable;
  private final SqlNode contents;
  private final String open;
  private final String close;
  private final String separator;
  private final String item;
  private final String index;
  private final Configuration configuration;

  /**
   * @deprecated Since 3.5.9, use the {@link #ForEachSqlNode(Configuration, SqlNode, String, Boolean, String, String, String, String, String)}.
   */
  @Deprecated
  public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
    this(configuration, contents, collectionExpression, null, index, item, open, close, separator);
  }

  /**
   * @since 3.5.9
   */
  public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, Boolean nullable, String index, String item, String open, String close, String separator) {
    this.evaluator = new ExpressionEvaluator();
    this.collectionExpression = collectionExpression;
    this.nullable = nullable;
    this.contents = contents;
    this.open = open;
    this.close = close;
    this.separator = separator;
    this.index = index;
    this.item = item;
    this.configuration = configuration;
  }

  @Override
  public boolean apply(DynamicContext context) {
    Map<String, Object> bindings = context.getBindings();
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings,
      Optional.ofNullable(nullable).orElseGet(configuration::isNullableOnForEach));
    if (iterable == null || !iterable.iterator().hasNext()) {
      return true;
    }
    boolean first = true;
    applyOpen(context);
    int i = 0;
    for (Object o : iterable) {
      DynamicContext oldContext = context;
      if (first || separator == null) {
        context = new PrefixedContext(context, "");
      } else {
        context = new PrefixedContext(context, separator);
      }
      int uniqueNumber = context.getUniqueNumber();
      // Issue #709
      if (o instanceof Map.Entry) {
        @SuppressWarnings("unchecked")
        Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
        applyIndex(context, mapEntry.getKey(), uniqueNumber);
        applyItem(context, mapEntry.getValue(), uniqueNumber);
      } else {
        applyIndex(context, i, uniqueNumber);
        applyItem(context, o, uniqueNumber);
      }
      contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
      if (first) {
        first = !((PrefixedContext) context).isPrefixApplied();
      }
      context = oldContext;
      i++;
    }
    applyClose(context);
    context.getBindings().remove(item);
    context.getBindings().remove(index);
    return true;
  }

  private void applyIndex(DynamicContext context, Object o, int i) {
    if (index != null) {
      context.bind(index, o);
      context.bind(itemizeItem(index, i), o);
    }
  }

  private void applyItem(DynamicContext context, Object o, int i) {
    if (item != null) {
      context.bind(item, o);
      context.bind(itemizeItem(item, i), o);
    }
  }

  private void applyOpen(DynamicContext context) {
    if (open != null) {
      context.appendSql(open);
    }
  }

  private void applyClose(DynamicContext context) {
    if (close != null) {
      context.appendSql(close);
    }
  }

  private static String itemizeItem(String item, int i) {
    return ITEM_PREFIX + item + "_" + i;
  }

  private static class FilteredDynamicContext extends DynamicContext {
    private final DynamicContext delegate;
    private final int index;
    private final String itemIndex;
    private final String item;

    public FilteredDynamicContext(Configuration configuration,DynamicContext delegate, String itemIndex, String item, int i) {
      super(configuration, null);
      this.delegate = delegate;
      this.index = i;
      this.itemIndex = itemIndex;
      this.item = item;
    }

    @Override
    public Map<String, Object> getBindings() {
      return delegate.getBindings();
    }

    @Override
    public void bind(String name, Object value) {
      delegate.bind(name, value);
    }

    @Override
    public String getSql() {
      return delegate.getSql();
    }

    @Override
    public void appendSql(String sql) {
      GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
        String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
        if (itemIndex != null && newContent.equals(content)) {
          newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
        }
        return "#{" + newContent + "}";
      });

      delegate.appendSql(parser.parse(sql));
    }

    @Override
    public int getUniqueNumber() {
      return delegate.getUniqueNumber();
    }

  }


  private class PrefixedContext extends DynamicContext {
    private final DynamicContext delegate;
    private final String prefix;
    private boolean prefixApplied;

    public PrefixedContext(DynamicContext delegate, String prefix) {
      super(configuration, null);
      this.delegate = delegate;
      this.prefix = prefix;
      this.prefixApplied = false;
    }

    public boolean isPrefixApplied() {
      return prefixApplied;
    }

    @Override
    public Map<String, Object> getBindings() {
      return delegate.getBindings();
    }

    @Override
    public void bind(String name, Object value) {
      delegate.bind(name, value);
    }

    @Override
    public void appendSql(String sql) {
      if (!prefixApplied && sql != null && sql.trim().length() > 0) {
        delegate.appendSql(prefix);
        prefixApplied = true;
      }
      delegate.appendSql(sql);
    }

    @Override
    public String getSql() {
      return delegate.getSql();
    }

    @Override
    public int getUniqueNumber() {
      return delegate.getUniqueNumber();
    }
  }

}
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java
public class TrimSqlNode implements SqlNode {

  private final SqlNode contents;
  private final String prefix;
  private final String suffix;
  private final List<String> prefixesToOverride;
  private final List<String> suffixesToOverride;
  private final Configuration configuration;

  public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
    this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
  }

  protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) {
    this.contents = contents;
    this.prefix = prefix;
    this.prefixesToOverride = prefixesToOverride;
    this.suffix = suffix;
    this.suffixesToOverride = suffixesToOverride;
    this.configuration = configuration;
  }

  @Override
  public boolean apply(DynamicContext context) {
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    boolean result = contents.apply(filteredDynamicContext);
    filteredDynamicContext.applyAll();
    return result;
  }

  private static List<String> parseOverrides(String overrides) {
    if (overrides != null) {
      // 多个override用竖线分隔
      final StringTokenizer parser = new StringTokenizer(overrides, "|", false);
      final List<String> list = new ArrayList<>(parser.countTokens());
      while (parser.hasMoreTokens()) {
        list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
      }
      return list;
    }
    return Collections.emptyList();
  }

  private class FilteredDynamicContext extends DynamicContext {
    // 委托目标
    private DynamicContext delegate;
    private boolean prefixApplied;
    private boolean suffixApplied;
    private StringBuilder sqlBuffer;

    public FilteredDynamicContext(DynamicContext delegate) {
      super(configuration, null);
      this.delegate = delegate;
      this.prefixApplied = false;
      this.suffixApplied = false;
      this.sqlBuffer = new StringBuilder();
    }

    public void applyAll() {
      sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
      String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
      // 存在内容才处理
      if (trimmedUppercaseSql.length() > 0) {
        // 处理前缀
        applyPrefix(sqlBuffer, trimmedUppercaseSql);
        // 处理后缀
        applySuffix(sqlBuffer, trimmedUppercaseSql);
      }
      // 处理trim标签的内部内容
      delegate.appendSql(sqlBuffer.toString());
    }

    @Override
    public Map<String, Object> getBindings() {
      return delegate.getBindings();
    }

    @Override
    public void bind(String name, Object value) {
      delegate.bind(name, value);
    }

    @Override
    public int getUniqueNumber() {
      return delegate.getUniqueNumber();
    }

    @Override
    public void appendSql(String sql) {
      sqlBuffer.append(sql);
    }

    @Override
    public String getSql() {
      return delegate.getSql();
    }

    private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
      if (!prefixApplied) {
        prefixApplied = true;
        if (prefixesToOverride != null) {
          // 依次匹配定义的各个前缀并去除
          for (String toRemove : prefixesToOverride) {
            if (trimmedUppercaseSql.startsWith(toRemove)) {
              // 移除前缀
              sql.delete(0, toRemove.trim().length());
              break;
            }
          }
        }
        if (prefix != null) {
          sql.insert(0, " ");
          sql.insert(0, prefix);
        }
      }
    }

    private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
      if (!suffixApplied) {
        suffixApplied = true;
        if (suffixesToOverride != null) {
          for (String toRemove : suffixesToOverride) {
            if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
              int start = sql.length() - toRemove.trim().length();
              int end = sql.length();
              sql.delete(start, end);
              break;
            }
          }
        }
        if (suffix != null) {
          sql.append(" ");
          sql.append(suffix);
        }
      }
    }

  }

}
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/xmltags/SetSqlNode.java
public class SetSqlNode extends TrimSqlNode {

  private static final List<String> COMMA = Collections.singletonList(",");

  public SetSqlNode(Configuration configuration,SqlNode contents) {
    // 注意只会覆盖掉前缀,后缀是不会覆盖的,毕竟suffixesToOverride传的是null
    super(configuration, contents, "SET", COMMA, null, COMMA);
  }

}
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/xmltags/WhereSqlNode.java
public class WhereSqlNode extends TrimSqlNode {

  // 默认会去掉这些前缀
  private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");

  public WhereSqlNode(Configuration configuration, SqlNode contents) {
    // 注意只会覆盖掉前缀,后缀是不会覆盖的,毕竟suffixesToOverride传的是null
    super(configuration, contents, "WHERE", prefixList, null, null);
  }

}
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/xmltags/ChooseSqlNode.java
public class ChooseSqlNode implements SqlNode {
  private final SqlNode defaultSqlNode;
  private final List<SqlNode> ifSqlNodes;

  public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
    this.ifSqlNodes = ifSqlNodes;
    this.defaultSqlNode = defaultSqlNode;
  }

  @Override
  public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : ifSqlNodes) {
      if (sqlNode.apply(context)) {
        return true;
      }
    }
    if (defaultSqlNode != null) {
      defaultSqlNode.apply(context);
      return true;
    }
    return false;
  }
}
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/xmltags/VarDeclSqlNode.java
public class VarDeclSqlNode implements SqlNode {

  private final String name;
  private final String expression;

  public VarDeclSqlNode(String name, String exp) {
    this.name = name;
    this.expression = exp;
  }

  @Override
  public boolean apply(DynamicContext context) {
    // 执行表达式的值
    final Object value = OgnlCache.getValue(expression, context.getBindings());
    // 将值绑定到上下文的参数映射中
    context.bind(name, value);
    return true;
  }

}
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/xmltags/MixedSqlNode.java
/*
 * 这是一种抽象的特殊的SqlNode,用来封装其他多种具有实际意义的SqlNode。
 */
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;
  }
}

其中,SetSqlNodeWhereSqlNode是基于TrimSqlNode实现的,毕竟其逻辑就是在子节点开头设置和覆盖掉一些内容。

构建SQL

上面一开始就说到,SQL是通过MappedStatement来构建的。

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/mapping/MappedStatement.java
public BoundSql getBoundSql(Object parameterObject) {
  // 通过SqlSource来创建BoundSql对象,传入实际参数,处理动态SQL
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  // 获取SQL中的#{}占位符对应的参数信息,被封装为ParameterMapping对象
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  /*
   * 什么情况下为空? (如果XML内容中没有#{}占位符,就为empty,倒不会为null,因为有初始化)
   * 这里parameterMap返回的ParameterMapping是空列表,具体参考上面Builder内部类的初始化操作。
   *
   * MyBatis已经不再推荐在sql标签上设置parameterMap属性,实际工作中也基本不会使用该属性
   * 所以这里基本上可以理解为无效操作,可以忽略掉!
   */
  if (parameterMappings == null || parameterMappings.isEmpty()) {
    boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
  }

  // check for nested result maps in parameter mappings (issue #30)
  /*
   * 获取设置resultMap
   * 为什么这里resultMap信息封装在ParameterMapping中的?
   * 参考SqlSourceBuilder中的ParameterMappingTokenHandler内部类中的buildParameterMapping方法
   *
   * 另外可以忽略这里,暂时不知道什么场景会在#{}中指定resultMap。
   */
  for (ParameterMapping pm : boundSql.getParameterMappings()) {
    String rmId = pm.getResultMapId();
    if (rmId != null) {
      ResultMap rm = configuration.getResultMap(rmId);
      if (rm != null) {
        hasNestedResultMaps |= rm.hasNestedResultMaps();
      }
    }
  }

  return boundSql;
}

这里第一行代码印证了前言所述,真正的SQL解析是由SqlSource来处理的。

v3.x
DynamicSqlSource
RawSqlSource
<
>
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java
/*
 * 用于描述XML中的动态SQL,比如在SQL中用了变量
 */
public class DynamicSqlSource implements SqlSource {

  private final Configuration configuration;
  private final SqlNode rootSqlNode;

  /*
   * 在解析mapper时,在XMLScriptBuilder中的parseScriptNode方法中会调用该构造器。
   */
  public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
    this.configuration = configuration;
    this.rootSqlNode = rootSqlNode;
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    // 创建一个上下文对象,用来解析SQL
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 调用各SqlNode来处理动态SQL的各个部分
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    /*
     * 判断参数对象的类型,在上层MapperMethod中利用了参数解析器来将mapper方法的实参进行处理,
     * 因为参数有各种情况,所以是以Object一路传下来的。
     *
     * 这里从实参来获取参数类型,而不是配置中的parameterType属性,是和RawSqlSource不同的地方。
     * 原因是DynamicSqlSource每次运行时解析,而RawSqlSource是解析时运行一次解析后不再重复解析。
     */
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    /*
     * 创建SqlSource对象,用于创建BoundSql对象
     * 当前类就是一个SqlSource,为什么还要创建SqlSource?
     * 别忘了,所有的SqlNode中都是没有处理#{}的,所以这里创建的SqlSource是在处理占位符#{}。
     * 这里返回的SqlSource是StaticSqlSource类型的!
     */
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    // 正式创建BoundSql对象,再次强调,这里的sqlSource是StaticSqlSource
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    // 将参数映射也设置到boundSql对象中
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

}
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java
/*
 * 表示静态SQL,但并不是指不包含变量信息,可以包含#{},但不包含${}及其他动态SQL节点
 */
public class RawSqlSource implements SqlSource {

  private final SqlSource sqlSource;

  public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
  }

  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    // 如果没有指定parameterType,那么各参数的javaType会被设置为Object.class
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    /*
     * 返回的是StaticSqlSource
     * 这里是在构造方法中解析,整个运行过程中只需解析一次,而不像DynamicSqlSource一样每次运行时解析,这是两者的最大区别。
     */
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
  }

  private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    // 创建一个上下文用于各SqlNode执行
    DynamicContext context = new DynamicContext(configuration, null);
    rootSqlNode.apply(context);
    return context.getSql();
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    return sqlSource.getBoundSql(parameterObject);
  }

}

两种SqlSource都是基于SqlSourceBuilder来进行解析的。但两者有本质区别,即RawSqlSource是在构造时进行解析,整个运行过程中只会执行一次解析;而DynamicSqlSource是每次运行时解析。因为这种运行机制,所以RawSqlSource会使用XML节点中配置的parameterType属性,而DynamicSqlSource是根据实参来获取类型。毕竟RawSqlSource被创建时还没实参,也不能根据参数来获取类型。

构建动态SQL

不管是哪种SqlSource,首先都会调用各种SqlNodeapply方法来将XML片段的SQL描述,转为纯文本的SQL。 上面已经给出了各种SqlNode和解析时用到的上下文DynamicContext的源码,所以这里不再赘述。 经过此阶段的处理后,DynamicContextgetSql方法返回的字符串中只可能包含#{}占位符,接下来会进行动态变量的处理。

这里将#{}引入的变量称为动态变量,而${}引入的变量称为静态变量。两者是有本质区别的,后者是在TextSqlNode中被处理的,是进行字符串的替换操作;而前者是转为PrepareStatement的参数占位符并进行参数设置。

处理SQL中的动态变量

v3.x
SqlSourceBuilder
GenericTokenParser
<
>
java
mybatis-3/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java
  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {

    // 创建参数映射处理器
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);

    /*
     * 这里只有处理#{},那又在哪里处理${}呢? 答案就是在TextSqlNode中。
     * 而且是这里之前就处理过了,originalSql就是各SqlNode处理之后的结果。
     */
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);

    /*
     * 重点:
     *  解析SQL语句,替换动态SQL中的占位符“#{}”,得到SQL的字符串形式;
     *  既然这里都已经得到了SQL的字符串形式,为什么下面还需要封装为SqlSource,以及后续为什么还要转为BoundSql对象呢?
     */
    String sql;
    if (configuration.isShrinkWhitespacesInSql()) { // 是否移除SQL中的空格
      sql = parser.parse(removeExtraWhitespaces(originalSql));
    } else {
      sql = parser.parse(originalSql);
    }
    // 封装解析结果
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }



  public static String removeExtraWhitespaces(String original) {
    // 将SQL字符串根据空格拆分成多个有效内容部分
    StringTokenizer tokenizer = new StringTokenizer(original);
    StringBuilder builder = new StringBuilder();
    boolean hasMoreTokens = tokenizer.hasMoreTokens();
    while (hasMoreTokens) {
      builder.append(tokenizer.nextToken());
      hasMoreTokens = tokenizer.hasMoreTokens();
      if (hasMoreTokens) {
        // 每部分用单个空格拼接
        builder.append(' ');
      }
    }
    return builder.toString();
  }

  private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {

    private final List<ParameterMapping> parameterMappings = new ArrayList<>();
    private final Class<?> parameterType;
    private final MetaObject metaParameters;

    public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) {
      super(configuration);
      this.parameterType = parameterType;
      this.metaParameters = configuration.newMetaObject(additionalParameters);
    }

    public List<ParameterMapping> getParameterMappings() {
      return parameterMappings;
    }

    @Override
    public String handleToken(String content) {
      // 解析占位符内容
      parameterMappings.add(buildParameterMapping(content));
      // jdbc的参数占位符
      return "?";
    }

    private ParameterMapping buildParameterMapping(String content) {
      Map<String, String> propertiesMap = parseParameterMapping(content);
      // 获取参数名称
      String property = propertiesMap.get("property");
      /*
       * 判断参数的类型
       * 这里会根据实际参数类型来判断javaType,所以编码时没有指定javaType,MyBatis也能正确判断参数类型。
       */
      Class<?> propertyType;
      // 引用<bind/>标签定义的参数,参考DynamicSqlSource中调用SqlSourceBuilder的parse方法的第三个参数
      if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
        propertyType = metaParameters.getGetterType(property);
      } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) { // 如果全局注册的类型处理器支持当前参数类型,最常见的情况
        propertyType = parameterType;
      } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
        propertyType = java.sql.ResultSet.class;
      } else if (property == null || Map.class.isAssignableFrom(parameterType)) { // 如果参数是null或者参数类型被封装为Map(具体要参考上层MapperMethod参数处理器中的逻辑)
        propertyType = Object.class;
      } else {
        MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
        // 引用对象中的字段,如常使用的透传dto存controller层到dao层
        if (metaClass.hasGetter(property)) { // 如果有getter方法,则通过getter的返回值类型来判断参数类型
          propertyType = metaClass.getGetterType(property);
        } else { // 直接引用实参
          propertyType = Object.class;
        }
      }
      /*
       * 这里传入了propertyType,会被设置为javaType,如果没有显示指定,那么就是使用的这个自动推断出来的javaType。
       * 如果显式指定了,那么下面解析时会覆盖这里设置的。
       */
      ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
      Class<?> javaType = propertyType;
      String typeHandlerAlias = null;
      /*
       * 遍历占位符中的每部分
       * 常见和常用的就是javaType,jdbcType以及typeHandler
       * 另外mode主要用于存储过程,numericScale用来调整小数的精度;
       * resultMap不清楚什么场景会被用到!
       */
      for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
        String name = entry.getKey();
        String value = entry.getValue();
        if ("javaType".equals(name)) {
          javaType = resolveClass(value);
          builder.javaType(javaType);
        } else if ("jdbcType".equals(name)) {
          builder.jdbcType(resolveJdbcType(value));
        } else if ("mode".equals(name)) {
          builder.mode(resolveParameterMode(value));
        } else if ("numericScale".equals(name)) {
          builder.numericScale(Integer.valueOf(value));
        } else if ("resultMap".equals(name)) {
          builder.resultMapId(value);
        } else if ("typeHandler".equals(name)) {
          typeHandlerAlias = value;
        } else if ("jdbcTypeName".equals(name)) {
          builder.jdbcTypeName(value);
        } else if ("property".equals(name)) {
          // Do Nothing
        } else if ("expression".equals(name)) {
          throw new BuilderException("Expression based parameters are not supported yet");
        } else {
          throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}.  Valid properties are " + PARAMETER_PROPERTIES);
        }
      }
      if (typeHandlerAlias != null) {
        /*
         * 解析并设置指定的typeHandler,
         * 如果没有指定,那么在下面的build过程中会尝试查找默认的typeHandler,一般常见的类型都是默认支持的,具体参考TypeHandlerRegistry。
         * 如果最后没有找到typeHandler,则会抛出异常。
         */
        builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
      }
      return builder.build();
    }

    private Map<String, String> parseParameterMapping(String content) {
      try {
        return new ParameterExpression(content);
      } catch (BuilderException ex) {
        throw ex;
      } catch (Exception ex) {
        throw new BuilderException("Parsing error was found in mapping #{" + content + "}.  Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex);
      }
    }
  }

}
java
mybatis-3/src/main/java/org/apache/ibatis/parsing/GenericTokenParser.java
public class GenericTokenParser {

  private final String openToken;
  private final String closeToken;
  // 处理openToken和closeToken中间内容的处理器
  private final TokenHandler handler;

  public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
    this.openToken = openToken;
    this.closeToken = closeToken;
    this.handler = handler;
  }

  public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    // 找到第一个占位符
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    do {
      if (start > 0 && src[start - 1] == '\\') { // 转义字符的情况
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        // 跳过占位符
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          // 重置expression,便于处理下一个占位符
          expression.setLength(0);
        }
        // 将上次处理的末尾到当前这个开始占位符中间的内容拷贝
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        // 找到结束占位符
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\\') { // 转义符的情况
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            // 跳过该占位符
            offset = end + closeToken.length();
            // 寻找下一个结束占位符
            end = text.indexOf(closeToken, offset);
          } else {
            // 取得占位符中的参数名,并保存到expression中
            expression.append(src, offset, end - offset);
            break;
          }
        }
        /*
         * 如果没有找到结束占位符,则将剩下的内容直接全部拷贝,并退出循环,结束寻找
         * 所以说,如果只定义了开始占位符,没有定义结束占位符,并不会在MyBatis层面报错,而是让SQL本身就变成有错误的SQL。
         */
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          /*
           * 取得参数对应的值,并拼接在sql中,占位符中可能并不是简单的参数名(比如还会设置jdbcType,typeHandler等),所以需要handler来处理
           */
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      // 从offset处开始往后找下一个开始占位符
      start = text.indexOf(openToken, offset);
    } while (start > -1);
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
}

在该类的parse方法中,会处理#{},用?来替换参数,并构建参数的ParameterMapping对象。后续在Executor中执行设置SQL参数时会被用到。 一般在#{}中除了指定参数名称外,还会指定其他属性,这些属性的处理也是在这里进行的,会被一起封装在ParameterMapping中。一般常用的是javaTypejdbcTypetypeHandler。 值得一提的是,typeHandler是必须的,否则报错找不到类型处理器。但开发中一般不会显示指定这三个参数(除了真实需要),那为什么没有报错?原因是MyBatis中针对常用数据类型预定义好了一些typeHandler。 另外如果没有指定javaType,那么MyBatis会根据实参类型来推断,即buildParameterMapping方法中根据多种情况来推断了propertyType,默认会以该类型作为javaType;当然如果显式指定了,会覆盖该默认机制。 如果是RawSqlSource,是在启动解析时构建的SQL,此时没有实参,那么是怎么推断的propertyType呢?分为两种情况:

  • 如果指定了parameterType,而且该类型注册有typeHandler,那么则推断为指定类型;否则使用兜底的Object.class
  • 如果没有指定parameterType,那么RawSqlSource会将parameterType指定为Object.class

处理typeHandler

解析显式配置的typeHandler

如果显示指定了typeHandler属性,那么在这一步会加载和解析。

v3.x
BaseBuilder
TypeHandlerRegistry
<
>
java
mybatis-3/src/main/java/org/apache/ibatis/builder/BaseBuilder.java
protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, String typeHandlerAlias) {
  if (typeHandlerAlias == null) {
    return null;
  }
  // 获取指定的类型处理器的Class
  Class<?> type = resolveClass(typeHandlerAlias);
  if (type != null && !TypeHandler.class.isAssignableFrom(type)) {
    throw new BuilderException("Type " + type.getName() + " is not a valid TypeHandler because it does not implement TypeHandler interface");
  }
  @SuppressWarnings("unchecked") // already verified it is a TypeHandler
  Class<? extends TypeHandler<?>> typeHandlerType = (Class<? extends TypeHandler<?>>) type;
  return resolveTypeHandler(javaType, typeHandlerType);
}

protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, Class<? extends TypeHandler<?>> typeHandlerType) {
  if (typeHandlerType == null) {
    return null;
  }
  // javaType ignored for injected handlers see issue #746 for full detail
  // 判断该类型的类型处理器是否已经被处理过了
  TypeHandler<?> handler = typeHandlerRegistry.getMappingTypeHandler(typeHandlerType);
  if (handler == null) {
    // not in registry, create a new one
    // 还没注册过,则创建并注册一个新的类型处理器实例
    handler = typeHandlerRegistry.getInstance(javaType, typeHandlerType);
  }
  return handler;
}
java
mybatis-3/src/main/java/org/apache/ibatis/type/TypeHandlerRegistry.java

// jdbcType和typeHandler的映射
private final Map<JdbcType, TypeHandler<?>> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);
// 两级映射,一级key是javaType,二级key是jdbcType
private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
private final TypeHandler<Object> unknownTypeHandler;
// key是typeHandler的类型,value是typeHandler实例
private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();

private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();

private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
public TypeHandler<?> getMappingTypeHandler(Class<? extends TypeHandler<?>> handlerType) {
  return allTypeHandlersMap.get(handlerType);
}
@SuppressWarnings("unchecked")
public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
  if (javaTypeClass != null) {
    try {
      Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
      return (TypeHandler<T>) c.newInstance(javaTypeClass);
    } catch (NoSuchMethodException ignored) {
      // ignored
    } catch (Exception e) {
      throw new TypeException("Failed invoking constructor for handler " + typeHandlerClass, e);
    }
  }
  try {
    Constructor<?> c = typeHandlerClass.getConstructor();
    return (TypeHandler<T>) c.newInstance();
  } catch (Exception e) {
    throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e);
  }
}

TypeHandlerRegistry是管理typeHandler的工具类,这里也是通过工具类来创建typeHandler实例,稍后会注册;如果是有注册过的,那么则不会重复创建;否则每次解析时新建对象,因为这种情况不会在新建后进行注册。

获取默认的typeHandler

如果没有显示指定typeHandler,那么会尝试获取默认的typeHandler,如果找不到则会报错。

v3.x
ParameterMapping$Builder
TypeHandlerRegistry
<
>
java
mybatis-3/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java
public ParameterMapping build() {
  // 处理typeHandler
  resolveTypeHandler();
  validate();
  return parameterMapping;
}
private void resolveTypeHandler() {
  // 如果没有设置typeHandler,这里javaType应该是能保证不为null的
  if (parameterMapping.typeHandler == null && parameterMapping.javaType != null) {
    Configuration configuration = parameterMapping.configuration;
    // 获取registry对象
    TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    // 根据javaType和jdbcType来查找类型处理器
    parameterMapping.typeHandler = typeHandlerRegistry.getTypeHandler(parameterMapping.javaType, parameterMapping.jdbcType);
  }
}
private void validate() {
  if (ResultSet.class.equals(parameterMapping.javaType)) {
    if (parameterMapping.resultMapId == null) {
      throw new IllegalStateException("Missing resultmap in property '"
          + parameterMapping.property + "'.  "
          + "Parameters of type java.sql.ResultSet require a resultmap.");
    }
  } else {
    /*
     * 不能找到或没设置参数处理器,抛出异常
     * 一般常见大部分类型都有默认的类型处理器,但是自定义的类型则需要设置自定义的参数处理器
     */
    if (parameterMapping.typeHandler == null) {
      throw new IllegalStateException("Type handler was null on parameter mapping for property '"
        + parameterMapping.property + "'. It was either not specified and/or could not be found for the javaType ("
        + parameterMapping.javaType.getName() + ") : jdbcType (" + parameterMapping.jdbcType + ") combination.");
    }
  }
}
java
mybatis-3/src/main/java/org/apache/ibatis/type/TypeHandlerRegistry.java

// jdbcType和typeHandler的映射
private final Map<JdbcType, TypeHandler<?>> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);
// 两级映射,一级key是javaType,二级key是jdbcType
private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
private final TypeHandler<Object> unknownTypeHandler;
// key是typeHandler的类型,value是typeHandler实例
private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();

private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();

private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
  public <T> TypeHandler<T> getTypeHandler(Class<T> type) {
    return getTypeHandler((Type) type, null);
  }

  public <T> TypeHandler<T> getTypeHandler(TypeReference<T> javaTypeReference) {
    return getTypeHandler(javaTypeReference, null);
  }

  public TypeHandler<?> getTypeHandler(JdbcType jdbcType) {
    return jdbcTypeHandlerMap.get(jdbcType);
  }

  public <T> TypeHandler<T> getTypeHandler(Class<T> type, JdbcType jdbcType) {
    return getTypeHandler((Type) type, jdbcType);
  }

  public <T> TypeHandler<T> getTypeHandler(TypeReference<T> javaTypeReference, JdbcType jdbcType) {
    return getTypeHandler(javaTypeReference.getRawType(), jdbcType);
  }

  @SuppressWarnings("unchecked")
  private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
    if (ParamMap.class.equals(type)) {
      return null;
    }
    // 先根据JavaType找到映射关系
    Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
    TypeHandler<?> handler = null;
    // 再根据jdbcType查找typeHandler
    if (jdbcHandlerMap != null) {
      handler = jdbcHandlerMap.get(jdbcType);
      if (handler == null) {
        // 没找到对应jdbcType的typeHandler,则查找这一javaType默认的typeHandler
        handler = jdbcHandlerMap.get(null);
      }
      if (handler == null) {
        // 还是没找到,如果这一javaType只注册了一个typeHandler,则返回它,否则还是返回null。
        // #591
        handler = pickSoleHandler(jdbcHandlerMap);
      }
    }
    // type drives generics here
    return (TypeHandler<T>) handler;
  }
  private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMap(Type type) {
    // 根据java类型来存两级映射中获取jdbcType和typeHandler的映射
    Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = typeHandlerMap.get(type);
    if (jdbcHandlerMap != null) {
      return NULL_TYPE_HANDLER_MAP.equals(jdbcHandlerMap) ? null : jdbcHandlerMap;
    }
    if (type instanceof Class) {
      Class<?> clazz = (Class<?>) type;
      if (Enum.class.isAssignableFrom(clazz)) {
        Class<?> enumClass = clazz.isAnonymousClass() ? clazz.getSuperclass() : clazz;
        jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(enumClass, enumClass);
        if (jdbcHandlerMap == null) {
          register(enumClass, getInstance(enumClass, defaultEnumTypeHandler));
          return typeHandlerMap.get(enumClass);
        }
      } else {
        jdbcHandlerMap = getJdbcHandlerMapForSuperclass(clazz);
      }
    }
    typeHandlerMap.put(type, jdbcHandlerMap == null ? NULL_TYPE_HANDLER_MAP : jdbcHandlerMap);
    return jdbcHandlerMap;
  }
  private TypeHandler<?> pickSoleHandler(Map<JdbcType, TypeHandler<?>> jdbcHandlerMap) {
    TypeHandler<?> soleHandler = null;
    for (TypeHandler<?> handler : jdbcHandlerMap.values()) {
      if (soleHandler == null) { // 如果只存在一个typeHandler,则返回
        soleHandler = handler;
      } else if (!handler.getClass().equals(soleHandler.getClass())) { // 如果存在多个,则返回null
        // More than one type handlers registered.
        return null;
      }
    }
    return soleHandler;
  }

自动注册的typeHandler

MyBatis会默认注册一些typeHandler,极大地方便了开发。

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/type/TypeHandlerRegistry.java
/**
 * The default constructor.
 */
public TypeHandlerRegistry() {
  this(new Configuration());
}

/**
 * The constructor that pass the MyBatis configuration.
 *
 * @param configuration a MyBatis configuration
 * @since 3.5.4
 */
public TypeHandlerRegistry(Configuration configuration) {
  this.unknownTypeHandler = new UnknownTypeHandler(configuration);

  register(Boolean.class, new BooleanTypeHandler());
  register(boolean.class, new BooleanTypeHandler());
  register(JdbcType.BOOLEAN, new BooleanTypeHandler());
  register(JdbcType.BIT, new BooleanTypeHandler());

  register(Byte.class, new ByteTypeHandler());
  register(byte.class, new ByteTypeHandler());
  register(JdbcType.TINYINT, new ByteTypeHandler());

  register(Short.class, new ShortTypeHandler());
  register(short.class, new ShortTypeHandler());
  register(JdbcType.SMALLINT, new ShortTypeHandler());

  register(Integer.class, new IntegerTypeHandler());
  register(int.class, new IntegerTypeHandler());
  register(JdbcType.INTEGER, new IntegerTypeHandler());

  register(Long.class, new LongTypeHandler());
  register(long.class, new LongTypeHandler());

  register(Float.class, new FloatTypeHandler());
  register(float.class, new FloatTypeHandler());
  register(JdbcType.FLOAT, new FloatTypeHandler());

  register(Double.class, new DoubleTypeHandler());
  register(double.class, new DoubleTypeHandler());
  register(JdbcType.DOUBLE, new DoubleTypeHandler());

  register(Reader.class, new ClobReaderTypeHandler());
  register(String.class, new StringTypeHandler());
  register(String.class, JdbcType.CHAR, new StringTypeHandler());
  register(String.class, JdbcType.CLOB, new ClobTypeHandler());
  register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
  register(String.class, JdbcType.LONGVARCHAR, new StringTypeHandler());
  register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler());
  register(String.class, JdbcType.NCHAR, new NStringTypeHandler());
  register(String.class, JdbcType.NCLOB, new NClobTypeHandler());
  register(JdbcType.CHAR, new StringTypeHandler());
  register(JdbcType.VARCHAR, new StringTypeHandler());
  register(JdbcType.CLOB, new ClobTypeHandler());
  register(JdbcType.LONGVARCHAR, new StringTypeHandler());
  register(JdbcType.NVARCHAR, new NStringTypeHandler());
  register(JdbcType.NCHAR, new NStringTypeHandler());
  register(JdbcType.NCLOB, new NClobTypeHandler());

  register(Object.class, JdbcType.ARRAY, new ArrayTypeHandler());
  register(JdbcType.ARRAY, new ArrayTypeHandler());

  register(BigInteger.class, new BigIntegerTypeHandler());
  register(JdbcType.BIGINT, new LongTypeHandler());

  register(BigDecimal.class, new BigDecimalTypeHandler());
  register(JdbcType.REAL, new BigDecimalTypeHandler());
  register(JdbcType.DECIMAL, new BigDecimalTypeHandler());
  register(JdbcType.NUMERIC, new BigDecimalTypeHandler());

  register(InputStream.class, new BlobInputStreamTypeHandler());
  register(Byte[].class, new ByteObjectArrayTypeHandler());
  register(Byte[].class, JdbcType.BLOB, new BlobByteObjectArrayTypeHandler());
  register(Byte[].class, JdbcType.LONGVARBINARY, new BlobByteObjectArrayTypeHandler());
  register(byte[].class, new ByteArrayTypeHandler());
  register(byte[].class, JdbcType.BLOB, new BlobTypeHandler());
  register(byte[].class, JdbcType.LONGVARBINARY, new BlobTypeHandler());
  register(JdbcType.LONGVARBINARY, new BlobTypeHandler());
  register(JdbcType.BLOB, new BlobTypeHandler());

  register(Object.class, unknownTypeHandler);
  register(Object.class, JdbcType.OTHER, unknownTypeHandler);
  register(JdbcType.OTHER, unknownTypeHandler);

  register(Date.class, new DateTypeHandler());
  register(Date.class, JdbcType.DATE, new DateOnlyTypeHandler());
  register(Date.class, JdbcType.TIME, new TimeOnlyTypeHandler());
  register(JdbcType.TIMESTAMP, new DateTypeHandler());
  register(JdbcType.DATE, new DateOnlyTypeHandler());
  register(JdbcType.TIME, new TimeOnlyTypeHandler());

  register(java.sql.Date.class, new SqlDateTypeHandler());
  register(java.sql.Time.class, new SqlTimeTypeHandler());
  register(java.sql.Timestamp.class, new SqlTimestampTypeHandler());

  register(String.class, JdbcType.SQLXML, new SqlxmlTypeHandler());

  register(Instant.class, new InstantTypeHandler());
  register(LocalDateTime.class, new LocalDateTimeTypeHandler());
  register(LocalDate.class, new LocalDateTypeHandler());
  register(LocalTime.class, new LocalTimeTypeHandler());
  register(OffsetDateTime.class, new OffsetDateTimeTypeHandler());
  register(OffsetTime.class, new OffsetTimeTypeHandler());
  register(ZonedDateTime.class, new ZonedDateTimeTypeHandler());
  register(Month.class, new MonthTypeHandler());
  register(Year.class, new YearTypeHandler());
  register(YearMonth.class, new YearMonthTypeHandler());
  register(JapaneseDate.class, new JapaneseDateTypeHandler());

  // issue #273
  register(Character.class, new CharacterTypeHandler());
  register(char.class, new CharacterTypeHandler());
}

封装SQL构建结果

SqlSourceBuilderparse方法会把处理结果封装为StaticSqlSource,然后会调用其getBoundSql对象,将解析的结果封装为BoundSql,供后续Executor使用。

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/builder/StaticSqlSource.java
/*
 * 用于描述ProviderSqlSource、DynamicSqlSource及RawSqlSource解析后得到的静态SQL资源。
 */
public class StaticSqlSource implements SqlSource {

  private final String sql;
  private final List<ParameterMapping> parameterMappings;
  private final Configuration configuration;

  public StaticSqlSource(Configuration configuration, String sql) {
    this(configuration, sql, null);
  }

  public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
    this.sql = sql;
    this.parameterMappings = parameterMappings;
    this.configuration = configuration;
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    // 不管哪种情况,executor得到的BoundSql对象都是这里创建的。
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }

}

设置SQL参数

最后在Executor中执行时,会通过typeHandler替换SQL中的参数占位符。

v3.x
PreparedStatementHandler
setParameters
<
>
java
mybatis-3/src/main/java/org/apache/ibatis/executor/statement/PreparedStatementHandler.java
@Override
public void parameterize(Statement statement) throws SQLException {
  // 通过参数处理器来设置参数
  parameterHandler.setParameters((PreparedStatement) statement);
}
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java
@Override
public void setParameters(PreparedStatement ps) {
  ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
  // 获取SQL中的参数占位符
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings != null) {
    for (int i = 0; i < parameterMappings.size(); i++) {
      ParameterMapping parameterMapping = parameterMappings.get(i);
      /*
       * 这个mode很少用,一般与存储过程有关,当然没用时getMode返回null,所以这里也是满足条件的
       */
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        // 获取TypeHandler
        TypeHandler typeHandler = parameterMapping.getTypeHandler();
        JdbcType jdbcType = parameterMapping.getJdbcType();
        if (value == null && jdbcType == null) {
          jdbcType = configuration.getJdbcTypeForNull();
        }
        try {
          // 通过typeHandler来设置参数
          typeHandler.setParameter(ps, i + 1, value, jdbcType);
        } catch (TypeException | SQLException e) {
          throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
        }
      }
    }
  }
}

这里在遍历前面解析到的ParameterMapping,并通过其内部封装的typeHandler来设置参数。

总结

本文全面分析了MyBatis中SQL语句的处理过程和执行原理。从SqlSessionFactory的构建,到触发mapper文件的解析;从SqlSourceMappedStatement的构建,到将XML的各标签和文本解析成各种SqlNode;从#{}占位符到ParameterMapping的转变。 在容易上手开发的背后,是MyBatis为我们铺垫了太多太多。