【Mybatis】Mybatis源码之SqlSource#getBoundSql获取预编译SQL

1,859 阅读2分钟

这是我参与8月更文挑战的第27天,活动详情查看:8月更文挑战

时序图

sequenceDiagram
participant A as MappedStatement
participant B as DynamicSqlSource
participant C as DynamicContext
participant D as Configuration
participant E as MetaObject
participant F as MixedSqlNode
participant J as SqlSourceBuilder
participant K as GenericTokenParser
participant L as ParameterMappingTokenHandler

A ->> B : getBoundSql
B ->> C : new
C ->> D : newMetaObject
D ->> E : forObject
E -->> C : MetaObject
C -->> B : DynamicContext
B ->> F : apply
B ->> J : new
J -->> B : SqlSourceBuilder
B ->> J : parse
J ->> K : parse
K ->> L : handleToken
K -->> J : String sql
J -->> B : StaticSqlSource
B -->> A : BoundSql

详细步骤

DynamicSqlSource#getBoundSql

/**
 * 获取一个BoundSql对象
 *
 * DynamicSqlSource和 RawSqlSource都会转化为 StaticSqlSource,然后才能给出一个 BoundSql对象。
 *
 * @param parameterObject 参数对象
 * @return BoundSql对象
 */
@Override
public BoundSql getBoundSql(Object parameterObject) {
    // 创建DynamicSqlSource的辅助类,用来记录DynamicSqlSource解析出来的SQL片段信息和参数信息
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 这里会从根节点开始,对节点逐层调用apply方法,经过这一步后,动态节点"${}"都被替换,这样 DynamicSqlSource便不再是动态的,而是静态的。
    rootSqlNode.apply(context);
    // 处理占位符,汇总参数信息
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    // 使用SqlSourceBuilder处理"#{}",将其转化为"?",然后创建ParameterMapping,最终生成了StaticSqlSource对象
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    // 把context.getBindings()的参数放到boundSql的metaParameters中进行保存
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
}

DynamicContext::new

/**
 * DynamicContext构造方法
 * @param configuration 配置信息
 * @param parameterObject 用户传入的查询参数对象
 */
public DynamicContext(Configuration configuration, Object parameterObject) {
    if (parameterObject != null && !(parameterObject instanceof Map)) {
        // 获取参数对象的元对象,包括参数对象的get/set方法、属性等
        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);
    // 把数据库id放入上下文信息
    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}

Reflector::new

/**
 * Reflector 构造方法
 *
 * @param clazz 需要被反射处理的目标类
 */
public Reflector(Class<?> clazz) {
    // 要被反射解析的类
    type = clazz;
    // 设置默认构造器属性
    addDefaultConstructor(clazz);
    // 解析所有的getter
    addGetMethods(clazz);
    // 解析所有的setter
    addSetMethods(clazz);
    // 解析所有的属性
    addFields(clazz);
    // 设置可读属性
    readablePropertyNames = getMethods.keySet().toArray(new String[0]);
    // 设置可写属性
    writablePropertyNames = setMethods.keySet().toArray(new String[0]);
    // 将可读可写属性放入大小写无关的属性映射表
    for (String propName : readablePropertyNames) {
        caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
    }
    for (String propName : writablePropertyNames) {
        caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
    }
}

SqlNode#apply

  • SqlNode不同的实现类,对于节点的处理方式不同。

ForEachSqlNode#apply

  • 对foreach标签进行迭代处理,并将对应的SQL处理后进行拼接
  • foreach标签在3.4.5以前的版本中会有变量名重复导致的问题,但从3.4.5版本开始就已经解决了
/**
 * 完成该节点自身的解析,如
 *
 *     <select id="selectUsers" resultMap="userMapFull">
 *         SELECT *
 *         FROM `user`
 *         WHERE `id` IN
 *         <foreach item="id" collection="array" open="(" separator="," close=")">
 *             #{id}
 *         </foreach>
 *     </select>
 *
 * 主要流程是解 析被迭代元素获得迭代对象,然后将迭代对象的信息添加到上下文中,
 * 之后再根据上下文信息拼接字符串。最后,在字符串拼接完成后,会对 此次操作产生的临时变量进行清理,以避免对上下文环境造成的影响。
 *
 * @param context 上下文环境,该节点自身的解析结果将合并到该上下文环境中
 * @return 解析是否成功
 */
@Override
public boolean apply(DynamicContext context) {
    // 获取上线文环境
    Map<String, Object> bindings = context.getBindings();
    // 交给表达式求值器解析collection表达式,从而获得迭代器
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
    if (!iterable.iterator().hasNext()) { // 没有可迭代的元素
        // 不需要拼接信息,直接返回
        return true;
    }
    boolean first = true;
    // 添加open字符串
    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) { // 被迭代对象是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++;
    }
    // 添加close字符串
    applyClose(context);
    // 清理此次操作对环境的影响
    context.getBindings().remove(item);
    context.getBindings().remove(index);
    return true;
}

IfSqlNode#apply

@Override
public boolean apply(DynamicContext context) {
    // 判断if条件是否成立
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
        // 将contents拼接到context
        contents.apply(context);
        return true;
    }
    return false;
}

StaticTextSqlNode#apply

@Override
public boolean apply(DynamicContext context) {
    // 静态SQL片段,直接拼装
    context.appendSql(text);
    return true;
}

TextSqlNode#apply

  • 主要用于处理SQL语句中包含的$符号,如果包含,则将变量替换为真实的参数值
@Override
public boolean apply(DynamicContext context) {
    // 创建通用占位符解析器
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    // 替换掉其中的 ${} 占位符
    context.appendSql(parser.parse(text));
    return true;
}

/**
 * 创建一个通用的占位符解析器,用来解析${}占位符
 * @param handler 用来处理${}占位符的专用处理器
 * @return 占位符解析器
 */
private GenericTokenParser createParser(TokenHandler handler) {
    return new GenericTokenParser("${", "}", handler);
}

BindingTokenParser#handleToken

@Override
public String handleToken(String content) {
    // 获取参数
    Object parameter = context.getBindings().get("_parameter");
    if (parameter == null) {
        // 参数为null,将$变量替换为null
        context.getBindings().put("value", null);
    } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
        // 判断是否为基本数据类型,如果是,则直接将值放入上下文环境中
        context.getBindings().put("value", parameter);
    }
    // 从上下文中获取参数值或从parameterMetaObject参数对象中获取参数值
    Object value = OgnlCache.getValue(content, context.getBindings());
    String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
    // 检查SQL注入
    checkInjection(srtValue);
    return srtValue;
}
ContextMap#get
/**
 * 根据索引键,会尝试从HashMap中寻找,失败后会再尝试从parameterMetaObject中寻找
 * @param key 键
 * @return 值
 */
@Override
public Object get(Object key) {
    String strKey = (String) key;
    // 如果Map中包含对应的键,直接返回
    if (super.containsKey(strKey)) {
        return super.get(strKey);
    }

    // 如果HashMap中不含有对应的键,则尝试从参数对象的原对象中获取
    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);
    }
}

TrimSqlNode#apply

  • 为什么只写where标签也能实现trim标签去除SQL片段开头多余的关键字AND|OR,那是因为where标签对应的WhereSqlNode继承了TrimSqlNode,并且自定义了prefixprefixesToOverride属性值,见Mybatis源码之获取SqlSource对象
@Override
public boolean apply(DynamicContext context) {
    // 包装成FilteredDynamicContext,使用一个单独的StringBuilder来保存生成的SQL片段
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    // 解析SQL片段
    boolean result = contents.apply(filteredDynamicContext);
    // 处理SQL片段的前缀与后缀
    filteredDynamicContext.applyAll();
    return result;
}

SqlSourceBuilder#parse

/**
 * 将DynamicSqlSource和RawSqlSource中的“#{}”符号替换掉,从而将他们转化为StaticSqlSource
 * @param originalSql sqlNode.apply()拼接之后的sql语句。已经不包含<if> <where>等节点,也不含有${}符号
 * @param parameterType 实参类型
 * @param additionalParameters 附加参数
 * @return 解析结束的StaticSqlSource
 */
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    // 用来完成#{}处理的处理器
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    // 通用的占位符解析器,用来进行占位符替换
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    // 将#{}替换为?的SQL语句,并创建ParameterMapping
    String sql = parser.parse(originalSql);
    // 生成新的StaticSqlSource对象
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
  • 构建GenericTokenParser时,设置标识符为#,解析到相应的标识符后,对解析结果使用handleToken进行处理

GenericTokenParser#parse

/**
 * 该方法主要 完成占位符的定位工作,然后把占位符的替换工作交给与其关联的 TokenHandler 处理
 * @param text
 * @return
 */
public String parse(String text) {
    if (text == null || text.isEmpty()) {
        return "";
    }
    // search open token
    // 查找openToken的位置
    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;
    // 当存在openToken时,才继续处理
    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);
            }
            // 拼接从0到openToken之前的字符
            builder.append(src, offset, start - offset);
            // 设置offset值为openToken结束的位置
            offset = start + openToken.length();
            // 从offset值之后开始找第一个closeToken的位置
            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();
                    // 继续查找当前closeToken之后的closeToken
                    end = text.indexOf(closeToken, offset);
                } else {
                    expression.append(src, offset, end - offset);
                    break;
                }
            }
            // 如果不存在
            if (end == -1) {
                // close token was not found.
                // 拼接剩余的字符
                builder.append(src, start, src.length - start);
                // 设置offset为字符数组的长度
                offset = src.length;
            } else {
                /**
                 * DynamicCheckerTokenParser:如果存在,则设置当前SQL为动态的
                 * BindingTokenParser:获取$变量的值
                 * ParameterMappingTokenHandler:将#替换为?,并构建参数映射ParameterMapping
                 */
                builder.append(handler.handleToken(expression.toString()));
                // 设置offset值为closeToken结束的位置
                offset = end + closeToken.length();
            }
        }
        start = text.indexOf(openToken, offset);
    }
    // 拼接剩余的字符
    if (offset < src.length) {
        builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
}
  • 处理解析结果时,使用的是ParameterMappingTokenHandler类

ParameterMappingTokenHandler#handleToken

@Override
public String handleToken(String content) {
    // 构建ParameterMapping
    parameterMappings.add(buildParameterMapping(content));
    // 替换解析结果为?号
    return "?";
}

以上便是Mybatis中通过SqlSource#getBoundSql获取预编译SQL的流程。在预编译SQL中,除了用#号标识的参数使用?代替以外,其他的部分和执行的SQL完全相同。