前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >源码分析MyBatis中#{}与${}的解析

源码分析MyBatis中#{}与${}的解析

作者头像
码农飞哥
发布2021-08-18 10:23:34
2.1K0
发布2021-08-18 10:23:34
举报
文章被收录于专栏:好好学习

前言

在面试中我们经常会被到MyBatis中 #{} 占位符与{}占位符的区别。大多数的小伙伴都可以脱口而出#{} 会对值进行转义,防止SQL注入。而{}则会原样输出传入值,不会对传入值做任何处理。本文将通过源码层面分析为啥#{} 可以防止SQL注入。

#{}占位符的解析与参数的设置过程梳理

  1. 假如我们有如下SQL语句。
代码语言:javascript
复制
SELECT * FROM author WHERE name = #{name} AND age = #{age}
  1. 这个SQL语句中包含两个#{}占位符,在运行时这两个占位符会被解析成两个ParameterMapping 对象。如下:
代码语言:javascript
复制
ParameterMapping{property='name', mode=IN, javaType=class java.lang.String, jdbcType=null, ...}

代码语言:javascript
复制
ParameterMapping{property='age', mode=IN, javaType=class java.lang.Integer, jdbcType=null, ...}

#{}占位符解析完毕后,得到的SQL如下:

代码语言:javascript
复制
SELECT * FROM Author WHERE name = ? AND age = ?

这里假设下面这个方法与上面的 SQL 对应:

代码语言:javascript
复制
Author findByNameAndAge(@Param("name") String name, @Param("age") Integer age)

该方法的参数列表会被ParamNameResolver解析成一个map 如下:

代码语言:javascript
复制
{
    0: "name",
    1: "age"
}

假设该方法在运行时有如下的调用:

代码语言:javascript
复制
findByNameAndAge("张三", 30)    

此时,需要再次借助 ParamNameResolver 力量。这次我们将参数名和运行时的参数值绑定起来,得到如下的映射关系。

代码语言:javascript
复制
{
    "name": "张三",
    "age": 30,
    "param1": "张三",
    "param2": 30
}

下一步,我们要将运行时参数设置到 SQL 中。由于原 SQL 经过解析后,占位符信息已经被擦除掉了,我们无法直接将运行时参数 SQL 中。不过好在,这些占位符信息被记录在了 ParameterMapping 中了,MyBatis 会将 ParameterMapping 会按照 #{} 的解析顺序存入到 List 中。这样我们通过 ParameterMapping 在列表中的位置确定它与 SQL 中的哪个 ? 占位符相关联。同时通过 ParameterMapping 中的 property 字段,我们到“参数名与参数值”映射表中查找具体的参数值。这样,我们就可以将参数值准确的设置到 SQL 中了,此时 SQL 如下:

代码语言:javascript
复制
SELECT * FROM Author WHERE name = "张三" AND age = 30

解析${}占位符

在MyBatis中,当SQL配置中包含${}或者<if>,<set> 等标签时,会被认定为是动态SQL,使用 DynamicSqlSource 存储 SQL 片段,而RawSqlSource 是对原始的SQL 进行解析,而StaticSqlSource 是对静态SQL进行解析。这里我们重点介绍下DynamicSqlSource。话不多说,直接看源码。

代码语言:javascript
复制
  public BoundSql getBoundSql(Object parameterObject) {
    //生成一个动态上下文
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    //这里SqlNode.apply只是将${}这种参数替换掉,并没有替换#{}这种参数
    rootSqlNode.apply(context);
    //调用SqlSourceBuilder
    SqlSourceBuilder sqlSourceParser =  new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    //SqlSourceBuilder.parse,注意这里返回的是StaticSqlSource,解析完了就把那些参数都替换成?了,也就是最基本的JDBC的SQL写法
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    //看似是又去递归调用SqlSource.getBoundSql,其实因为是StaticSqlSource,所以没问题,不是递归调用
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
//  将DynamicContext的ContextMap中的内容拷贝到BoundSql中
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }

TextSqlNode用于存储带有${}占位符的文本

代码语言:javascript
复制
//***TextSqlNode
  public boolean apply(DynamicContext context) {
//    创建${} 占位符解析器
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
//   解析${} 占位符,并将解析结果添加到DynamicContext中
    context.appendSql(parser.parse(text));
    return true;
  }

  private GenericTokenParser createParser(TokenHandler handler) {
//    创建占位符解析器,GenericTokenParser 是一个通用解析器,并非只能解析${}
    return new GenericTokenParser("${", "}", handler);
  }

例如;SQL语句

代码语言:javascript
复制
SELECT * FROM article WHERE author = '${author}'

加入我们传入值为 张三 ,则替换之后的结果是

代码语言:javascript
复制
SELECT * FROM article WHERE author = '张三'

当用这些恶意的参数替换 ${author} 时就会出现灾难性问题 – SQL 注入。比如我们构建这样一个参数 author = 李四'; DELETE FROM article;#,然后我们把这个参数传给 TextSqlNode 进行解析。得到的结果如下:

代码语言:javascript
复制
SELECT * FROM article WHERE author = '张三'; DELETE FROM article;#'

看到没,由于传入的参数没有经过转义,最终导致了一条 SQL 被恶意参数拼接成了两条 SQL。更要命的是,第二天 SQL 会把 article 表的数据清空。

解析`#{}`占位符

经过前面的解析,我们已经能够从DynamicContext 中获取到完整的SQL语句了。但是这并不意味着解析工作就结束了。我们还有#{}占位符没有处理。#{}占位符不同于${}占位符的处理方式。MyBatis 并不会直接将#{}占位符替换成相应的参数值。 #{}的解析过程封装在SqlSourceBuilder 的parse方法中。解析后的结果交给StaticSqlSource处理。话不多说,来看看源码吧。

代码语言:javascript
复制
//*SqlSourceBuilder
  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
//   创建#{} 占位符处理器
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    //替换#{}中间的部分,如何替换,逻辑在ParameterMappingTokenHandler
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
//  解析#{}占位符,并返回解析结果
    String sql = parser.parse(originalSql);
    //封装解析结果到StaticSqlSource中,并返回
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

如上源码,该解析过程主要有四步,核心步骤就是解析#{}占位符,并返回结果。GenericTokenParser 类在前面已经解析过了,下面我们重点看看SqlSourceBuilder的内部类ParameterMappingTokenHandler。该类的核心方法是handleToken方法。该方法的主要作用是将#{}替换成 并返回。然后就是构建参数映射。ParameterMappingTokenHandler 该类同样实现了TokenHandler 接口,所以GenericTokenParser 类的parse方法可以调用到。

代码语言:javascript
复制
//参数映射记号处理器,静态内部类
  private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {

    private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
    private Class<?> parameterType;
    private 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) {
      //获取context的对应的ParameterMapping
      parameterMappings.add(buildParameterMapping(content));
      //如何替换很简单,永远是一个问号,但是参数的信息要记录在parameterMappings里面供后续使用
      return "?";
    }

    //构建参数映射
    private ParameterMapping buildParameterMapping(String content) {
        //#{favouriteSection,jdbcType=VARCHAR}
        //先解析参数映射,就是转化成一个hashmap
      /*
     * parseParameterMapping 内部依赖 ParameterExpression 对字符串进行解析,ParameterExpression 的
     */
      Map<String, String> propertiesMap = parseParameterMapping(content);
      String property = propertiesMap.get("property");
      Class<?> propertyType;
      // metaParameters 为 DynamicContext 成员变量 bindings 的元信息对象
      if (metaParameters.hasGetter(property)) {
        /*
     * parameterType 是运行时参数的类型。如果用户传入的是单个参数,比如 Article 对象,此时
     * parameterType 为 Article.class。如果用户传入的多个参数,比如 [id = 1, author = "coolblog"],
     * MyBatis 会使用 ParamMap 封装这些参数,此时 parameterType 为 ParamMap.class。如果
     * parameterType 有相应的 TypeHandler,这里则把 parameterType 设为 propertyType
     */
        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) {
        MetaClass metaClass = MetaClass.forClass(parameterType);
        if (metaClass.hasGetter(property)) {
          propertyType = metaClass.getGetterType(property);
        } else {
          // 如果 property 为空,或 parameterType 是 Map 类型,则将 propertyType 设为 Object.class
          propertyType = Object.class;
        }
      } else {
        propertyType = Object.class;
      }
      //      ----------------------------分割线---------------------------------
      ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
//     将propertyType赋值给javaType
      Class<?> javaType = propertyType;
      String typeHandlerAlias = null;
//      遍历propertiesMap
      for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
        String name = entry.getKey();
        String value = entry.getValue();
        if ("javaType".equals(name)) {
//         如果用户明确配置了javaType,则以用户的配置为准。
          javaType = resolveClass(value);
          builder.javaType(javaType);
        } else {
          throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}.  Valid properties are " + parameterProperties);
        }
      }
      //#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
      if (typeHandlerAlias != null) {
        builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
      }
      return builder.build();
    }

如上,buildParameterMapping方法,主要做了如下三件事

  1. 解析content,
  2. 解析propertyType,对应分割线上面的代码
  3. 构建ParameterMapping,对应分割下下面的代码。 最终的结果是将 #{xxx} 占位符中的内容解析成 Map。 例如:

{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}

上面占位符中的内容最终会被解析成如下的结果:

代码语言:javascript
复制
       {
           "property": "age",
           "typeHandler": "MyTypeHandler",
           "jdbcType": "NUMERIC",
           "javaType": "int"
       }

BoundSql的创建过程就此结束了。我们接着往下看。

参数设值

代码语言:javascript
复制
//*DefaultParameterHandler
public void setParameters(PreparedStatement ps) throws SQLException {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    /*
    * 从BoundSql中获取ParameterMapping列表,每个ParameterMapping
    * 与原始SQL中的#{xxx} 占位符一一对应
    * */
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      //循环设参数
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
//       检测参数类型,排除掉mode为OUT类型的parameterMapping
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          //如果不是OUT,才设进去
          Object value;
//          获取属性名
          String propertyName = parameterMapping.getProperty();
//         检测BoundSql的additionalParameter是否包含propertyName
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            //若有额外的参数, 设为额外的参数
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            //若参数为null,直接设null
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            //若参数有相应的TypeHandler,直接设object
            value = parameterObject;
          } else {
            //除此以外,MetaObject.getValue反射取得值设进去
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
//          之上,获取#{xxx}占位符属性所对应的运行时参数
//          -------------------分割线-----------------------
//      之下,获取#{xxx}占位符属性对应的TypeHandler,并在最后通过TypeHandler将运行时参数值设置到
//          PreparedStatement中。
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            //不同类型的set方法不同,所以委派给子类的setParameter方法
            jdbcType = configuration.getJdbcTypeForNull();
          }
//        由类型处理器typeHandler向ParameterHandler设置参数
          typeHandler.setParameter(ps, i + 1, value, jdbcType);
        }
      }
    }
  }

如上代码,分割线以上的大段代码用于获取 #{xxx} 占位符属性所对应的运行时参数。分割线以下的代码则是获取 #{xxx} 占位符属性对应的 TypeHandler,并在最后通过 TypeHandler 将运行时参数值设置到 PreparedStatement 中。

总结

#{}和{}的解析过程至此完成,解析方式不同,设置方式也不同,{}会通过TextSqlNode直接将传入的参数进行替换,存在SQL注入的风险。而每个#{}占位符都会解析成一个ParameterMapping对象,最后通过DefaultParameterHandler的setParameters方法进行设值,此时已经完成了预编译。。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-06-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农飞哥 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • #{}占位符的解析与参数的设置过程梳理
  • 解析${}占位符
  • 解析`#{}`占位符
  • 参数设值
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档