MyBatis何时替换SQL语句占位参数为“?”之静态SQL

问题

MyBatis何时把XML或annotation中SQL语句中的参数替换为?

第一阶段:

顺序调用逻辑:

  1. org.apache.ibatis.binding.MapperProxy#invoke
  2.  org.apache.ibatis.binding.MapperMethod#execute(根据sql语句的sqlCommandType进对应分支-增删改查)
  3.  org.apache.ibatis.session.defaults.DefaultSqlSession#insert(java.lang.String, java.lang.Object)(增删改查进入各自方法)
  4. org.apache.ibatis.session.defaults.DefaultSqlSession#update(java.lang.String, java.lang.Object)(上一步的增删改最后都进入此方法,查询进入select方法,此流程以增删改为例)

思路倒推:
以XML中一条插入语句为例,一路调用至DefaultSqlSession#update

 @Override
  public int update(String statement, Object parameter) {
    try {
      dirty = true;
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

调试查看 MappedStatement ms 属性发现其sqlSource.sql语句已经将参数占位替换“?”

MyBatis何时替换SQL语句占位参数为“?”之静态SQL_第1张图片

    也就是说在这之前sql语句已经被替换好,于是往回推理寻找SQL被替换的地方,在DefaultSqlSession#update方法中发现其在调用Executor的update方法之前取出了MappedStatement ,有取出必然有放入,于是我跳转到Configuration#getMappedStatement()类内部一探究竟。

  原来这个MappedStatement是存放在Configuration类的属性mappedStatements中,仔细看发现mappedStatements其实只是一个Map集合,定义如下:

Map mappedStatements

第二阶段

接下来我有两个思路:一、这个MappedStatement是何时放进这个Configuration的mappedStatements属性中的;二、MappedStatement中的SQL语句是何时放到其sqlSource.sql中的;答案肯定在这两步中之中;

先来查看第一步,Configuration#addMappedStatement这个方法是向mappedStatements添加MappedStatement,只要找到哪里调用了这个方法就找到第一步的答案。发现只有这里调用了此方法,org.apache.ibatis.builder.MapperBuilderAssistant#addMappedStatement()

  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;

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

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

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

打断点到此处,发现到这里是sql语句已被替换成“?”,不过也发现所有的SQL都是这里被装进去的。

继续向上追溯,发现下面类中调用了MapperBuilderAssistant#addMappedStatement()方法:

MyBatis何时替换SQL语句占位参数为“?”之静态SQL_第2张图片

先找个XMLStatementBuilder的进去看看,其中有两个方法调用了addMappedStatement

  1. XMLStatementBuilder#parseStatementNode(共有方法)
  2. XMLStatementBuilder#parseSelectKeyNode(私有方法)

先来看第一个方法

 public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    Class resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // Parse the SQL (pre:  and  were parsed and removed)
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    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;
    }

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

这个方法重点关注下面两行:

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

// Parse the SQL (pre:  and  were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

    第一行代码先调用了processSelectKeyNodes,接着processSelectKeyNodes又调用了parseSelectKeyNodes,再接着parseSelectKeyNodes调用了parseSelectKeyNode,这就回到刚才的问题,XMLStatementBuilder中有两个方法调用了addMappedStatement,实际上都是在parseStatementNode中调用的

进入到parseStatementNode方法中,关注如下代码:

SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
进入langDriver.createSqlSource中一探究竟,发现会弹出选择进入XMLLanguageDriver还是进入RawLanguageDriver;

这两个类是父子关系,我们先进入父类XMLLanguageDriver

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

发现其调用了XMLScriptBuilder#parseScriptNode方法;

  public SqlSource parseScriptNode() {
    List contents = parseDynamicTags(context);
    MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
    SqlSource sqlSource = null;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

这里就是构造SqlSource的地方了,调试如图:

MyBatis何时替换SQL语句占位参数为“?”之静态SQL_第3张图片

发现到这里Sql语句还是XML中定义的那样没有替换成?

继续往下执行,发现断点进入了else分支里,执行了

sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);

之后,SQL中参数就被替换为“?”了

MyBatis何时替换SQL语句占位参数为“?”之静态SQL_第4张图片

接下来我们就来看一下new RawSqlSource()里面都做了些什么。

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

  private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    DynamicContext context = new DynamicContext(configuration, null);
    rootSqlNode.apply(context);
    return context.getSql();
  }

 public RawSqlSource(Configuration configuration, String sql, Class parameterType) {
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class clazz = parameterType == null ? Object.class : parameterType;
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap());
  }

执行了上述三个方法,继续打断点跟踪getSql,getSql只是将Sql取出,再关注如下代码:

sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap());

发现又进入SqlSourceBuilder#parse方法:

  public SqlSource parse(String originalSql, Class parameterType, Map additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

再到GenericTokenParser#parse方法,走到这里感觉快要破案了,是不是有点小激动,parse方法如下,内容有点长耐心一行行看

public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    //1.将SQL语句转为char数组
    char[] src = text.toCharArray();
    int offset = 0;
    // search open token
    //检查SQL中是否有“#{”,openToken的只是在SqlSourceBuilder#parse中设置的
    int start = text.indexOf(openToken, offset);
    if (start == -1) {
      return text;
    }
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
      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.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.append(src, offset, end - offset);
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
           //重点关注这句话
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }

           //重点关注这句话
          builder.append(handler.handleToken(expression.toString()));
handler.handleToken点击进去后发现如下:MyBatis何时替换SQL语句占位参数为“?”之静态SQL_第5张图片

表明有四个类实现了TokenHandler#handleToken方法,一个个点进去发现

  private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
   
 @Override
    public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }
}

  private static class VariableTokenHandler implements TokenHandler {
    @Override
    public String handleToken(String content) {
      if (variables != null) {
        String key = content;
        if (enableDefaultValue) {
          final int separatorIndex = content.indexOf(defaultValueSeparator);
          String defaultValue = null;
          if (separatorIndex >= 0) {
            key = content.substring(0, separatorIndex);
            defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
          }
          if (defaultValue != null) {
            return variables.getProperty(key, defaultValue);
          }
        }
        if (variables.containsKey(key)) {
          return variables.getProperty(key);
        }
      }
      return "${" + content + "}";
    }
  }

  private static class DynamicCheckerTokenParser implements TokenHandler {

 @Override
    public String handleToken(String content) {
      this.isDynamic = true;
      return null;
    }
}

这四个内部类的handlerToken方法功能如下:

ParameterMappingTokenHandler:返回“?”
VariableTokenHandler:返回"${" + content + "}"
DynamicCheckerTokenParser:只是设置isDynamic=true
BindingTokenParser:只是设置isDynamic=true

结案

org.apache.ibatis.parsing.GenericTokenParser#parse方法就是替换参数的地方。

调用时序图

MyBatis何时替换SQL语句占位参数为“?”之静态SQL_第6张图片

说明

1. 本文中只是通过一条最简单的insert语句进行跟踪来探究SQL中的参数何时被替换为“?”,并不能覆盖所有语句,只是作为一个思路。

2.在探索过程中意外发现了$符号的藏身地。

发散点

  1. 探索其他复杂SQL(尤其是select)的替换过程
  2. 探索$符号的替换过程
  3. 探索annotation的替换过程
  4. 探索SQL语句是何时被从“?”替换成实际输入参数的。

 

你可能感兴趣的:(源码分析,MyBatis)