我的编程空间,编程开发者的网络收藏夹
学习永远不晚

源码解读Mybatis占位符#和$的区别

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

源码解读Mybatis占位符#和$的区别

Mybatis 作为国内开发中常用到的半自动 orm 框架,相信大家都很熟悉,它提供了简单灵活的xml映射配置,方便开发人员编写简单、复杂SQL,在国内互联网公司使用众多。

本文针对笔者日常开发中对 Mybatis 占位符 #{}${} 使用时机结合源码,思考总结而来

  • Mybatis 版本 3.5.11
  • Spring boot 版本 3.0.2
  • mybatis-spring 版本 3.0.1
  • github地址:https://github.com/wayn111 欢迎大家关注,点个star

一. 启动时,mybatis-spring 解析xml文件流程图

Spring 项目启动时,mybatis-spring 自动初始化解析xml文件核心流程

MybatisbuildSqlSessionFactory() 会遍历所有 mapperLocations(xml文件) 调用 xmlMapperBuilder.parse()解析,源码如下

在 parse() 方法中, Mybatis 通过 configurationElement(parser.evalNode("/mapper")) 方法解析xml文件中的各个标签

public class XMLMapperBuilder extends BaseBuilder {
  ...
  private final MapperBuilderAssistant builderAssistant;
  private final Map<String, XNode> sqlFragments;
  ...
  
    public void parse() {
      if (!configuration.isResourceLoaded(resource)) {
        // xml文件解析逻辑
        configurationElement(parser.evalNode("/mapper"));
        configuration.addLoadedResource(resource);
        bindMapperForNamespace();
      }

      parsePendingResultMaps();
      parsePendingCacheRefs();
      parsePendingStatements();
    }

    private void configurationElement(XNode context) {
      try {
        // 解析xml文件内的namespace、cache-ref、cache、parameterMap、resultMap、sql、select、insert、update、delete等各种标签
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.isEmpty()) {
          throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        sqlElement(context.evalNodes("/mapper/sql"));
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
      } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
      }
    }
}

最后会把 namespace、cache-ref、cache、parameterMap、resultMap、select、insert、update、delete等标签内容解析结果放到 builderAssistant 对象中,将sql标签解析结果放到sqlFragments对象中,其中 由于 builderAssistant 对象会保存select、insert、update、delete标签内容解析结果我们对 builderAssistant 对象进行深入了解

public class MapperBuilderAssistant extends BaseBuilder {
...
}

public abstract class BaseBuilder {
  protected final Configuration configuration;
  ...
}  

public class Configuration {
  ...
  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
      .conflictMessageProducer((savedValue, targetValue) ->
          ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
  protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
  protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
  protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
  protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");
  protected final Set<String> loadedResources = new HashSet<>();
  protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
  ...
}

builderAssistant 对象继承至 BaseBuilder,BaseBuilder 类中包含一个 configuration 对象属性, configuration 对象中会保存xml文件标签解析结果至自身对应属性mappedStatements、caches、resultMaps、sqlFragments

这里有个问题上面提到的sql标签结果会放到 XMLMapperBuilder 类的 sqlFragments 对象中,为什么 Configuration 类中也有个 sqlFragments 属性?

这里回看上文 buildSqlSessionFactory() 方法最后

原来 XMLMapperBuilder 类中的 sqlFragments 属性就来自Configuration类?

回到主题,在 buildStatementFromContext(context.evalNodes("select|insert|update|delete")) 方法中会通过如下调用

buildStatementFromContext(List<XNode> list, String requiredDatabaseId) 
-> parseStatementNode()
-> createSqlSource(Configuration configuration, XNode script, Class<?> parameterType)
-> parseScriptNode()
-> parseDynamicTags(context)

最后通过parseDynamicTags(context) 方法解析 select、insert、update、delete 标签内容将结果保存在 MixedSqlNode 对象中的 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;
  }
}

SqlNode 是一个接口,有10个实现类如下

可以看出我们的 select、insert、update、delete 标签中包含的各个文本(包含占位符 #{} 和 ${})、子标签都有对应的 SqlNode 实现类,后续运行中, Mybatis 对于 select、insert、update、delete 标签的 sql 语句处理都与这里的 SqlNode 各个实现类相关。自此我们 mybatis-spring 初始化流程中相关的重要代码都过了一遍。

二. 运行中,sql语句占位符 #{} 和 ${} 的处理

这里直接给出xml文件查询方法标签内容

<select id="findNewBeeMallOrderList" parameterType="Map" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List"/>
    from tb_newbee_mall_order
    <where>
        <if test="orderNo!=null and orderNo!=''">
            and order_no = #{orderNo}
        </if>
        <if test="userId!=null and userId!=''">
            and user_id = #{userId}
        </if>
        <if test="payType!=null and payType!=''">
            and pay_type = #{payType}
        </if>
        <if test="orderStatus!=null and orderStatus!=''">
            and order_status = #{orderStatus}
        </if>
        <if test="isDeleted!=null and isDeleted!=''">
            and is_deleted = #{isDeleted}
        </if>
        <if test="startTime != null and startTime.trim() != ''">
            and create_time &gt; #{startTime}
        </if>
        <if test="endTime != null and endTime.trim() != ''">
            and create_time &lt; #{endTime}
        </if>
    </where>
    <if test="sortField!=null and order!=null">
        order by ${sortField} ${order}
    </if>
    <if test="start!=null and limit!=null">
        limit #{start},#{limit}
    </if>
</select>

运行时 Mybatis 动态代理 MapperProxy 对象的调用流程,如下:

-> newBeeMallOrderMapper.findNewBeeMallOrderList(pageUtil);
-> MapperProxy.invoke(Object proxy, Method method, Object[] args)
-> MapperProxy.invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession)
-> MapperMethod.execute(SqlSession sqlSession, Object[] args)
-> MapperMethod.executeForMany(SqlSession sqlSession, Object[] args)
-> SqlSessionTemplate.selectList(String statement, Object parameter)
-> SqlSessionInterceptor.invoke(Object proxy, Method method, Object[] args)
-> DefaultSqlSession.selectList(String statement, Object parameter)
-> DefaultSqlSession.selectList(String statement, Object parameter, RowBounds rowBounds)
-> DefaultSqlSession.selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler)
-> CachingExecutor.query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)
-> MappedStatement.getBoundSql(Object parameterObject)
-> DynamicSqlSource.getBoundSql(Object parameterObject)
-> MixedSqlNode.apply(DynamicContext context) // ${} 占位符处理
-> SqlSourceBuilder.parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) // #{} 占位符处理

Mybatis 通过 DynamicSqlSource.getBoundSql(Object parameterObject) 方法对 select、insert、update、delete 标签内容做 sql 转换处理,代码如下:

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

2.1 ${} 占位符处理

rootSqlNode.apply(context) -> MixedSqlNode.apply(DynamicContext context) 中会将 SqlNode 集合拼接成实际要执行的 sql 语句
保存在 DynamicContext 对象中。这里给出 SqlNode 集合的调试截图

可以看出我们的 ${} 占位符文本的 SqlNode 实现类为 TextSqlNode,apply方法相关操作如下

public class TextSqlNode implements SqlNode {
    ...
    @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);
    }

    // 划重点,${}占位符替换逻辑在就handleToken(String content)方法中
    @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;
    }
}

public class GenericTokenParser {
    public String parse(String text) {
        ...
        do {
            ...
            if (end == -1) {
              ...
            } else {
              builder.append(handler.handleToken(expression.toString()));
              offset = end + closeToken.length();
            }
          }
          ...
        } while (start > -1);
        ...
        return builder.toString();
    }
}   

划重点,${} 占位符处理如下

handleToken(String content) 方法中, Mybatis 会通过 ognl 表达式将 ${} 的结果直接拼接在 sql 语句中,由此我们得知 ${} 占位符拼接的字段就是我们传入的原样字段,有着 Sql 注入风险

2.2 #{} 占位符处理

#{} 占位符文本的 SqlNode 实现类为 StaticTextSqlNode,查看源码

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

}

StaticTextSqlNode 会直接将节点内容拼接在 sql 语句中,也就是说在 rootSqlNode.apply(context) 方法执行完毕后,此时的 sql 语句如下

select order_id, order_no, user_id, total_price, 
pay_status, pay_type, pay_time, order_status, 
extra_info, user_name, user_phone, user_address, 
is_deleted, create_time, update_time 
from tb_newbee_mall_order
order by create_time desc
limit #{start},#{limit}

Mybatis 会通过上面提到 getBoundSql(Object parameterObject) 方法中的

sqlSourceParser.parse() 方法完成 #{} 占位符的处理,代码如下:

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
  ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
  GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
  String sql;
  if (configuration.isShrinkWhitespacesInSql()) {
    sql = parser.parse(removeExtraWhitespaces(originalSql));
  } else {
    sql = parser.parse(originalSql);
  }
  return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

看到了熟悉的 #{ 占位符没有,哈哈?, Mybatis 对于 #{} 占位符的处理就在 GenericTokenParser类的 parse() 方法中,代码如下:

public class GenericTokenParser {
    public String parse(String text) {
        ...
        do {
            ...
            if (end == -1) {
              ...
            } else {
              builder.append(handler.handleToken(expression.toString()));
              offset = end + closeToken.length();
            }
          }
          ...
        } while (start > -1);
        ...
        return builder.toString();
    }
}

public class SqlSourceBuilder extends BaseBuilder {
    ... 
    // 划重点,#{}占位符替换逻辑在就SqlSourceBuilder.handleToken(String content)方法中
    @Override
    public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }
}

划重点,#{} 占位符处理如下

handleToken(String content) 方法中, Mybatis 会直接将我们的传入参数转换成问号(就是 jdbc 规范中的问号),也就是说我们的 sql 语句是预处理的。能够避免 sql 注入问题

三. 总结

由上经过源码分析,我们知道 Mybatis#{} 占位符是直接转换成问号,拼接预处理 sql。 ${} 占位符是原样拼接处理,有sql注入风险,最好避免由客户端传入此参数。

到此这篇关于Mybatis占位符#和$的区别 源码解读的文章就介绍到这了,更多相关Mybatis占位符#和$的区别内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

源码解读Mybatis占位符#和$的区别

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

源码解读Mybatis占位符#和$的区别

这篇文章主要介绍了Mybatis占位符#和$的区别通过源码解读,针对笔者日常开发中对 Mybatis 占位符 #{} 和 ${} 使用时机结合源码,思考总结而来,需要的朋友可以参考下
2023-02-10

.properties文件读取及占位符${...}替换源码解析

前言我们在开发中常遇到一种场景,Bean里面有一些参数是比较固定的,这种时候通常会采用配置的方式,将这些参数配置在.properties文件中,然后在Bean实例化的时候通过Spring将这些.properties文件中配置的参数使用占位符
2023-05-31

mybatis解析xml配置中${xxx}占位符的代码逻辑

本文主要介绍了mybatis解析xml配置中${xxx}占位符的代码逻辑,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧<BR>
2023-05-20

介绍一下TensorFlow的变量和占位符的区别和用途

TensorFlow中的变量和占位符都是用来存储数据的,但它们有不同的特点和用途。变量(Variable):变量是在模型训练过程中可被训练(优化)的参数,它们包含了模型的权重和偏置等可学习的参数。变量会在每次训练迭代中更新其数值,从而使模
介绍一下TensorFlow的变量和占位符的区别和用途
2024-03-01

一步步从Vue3.x源码上理解ref和reactive的区别

vue3的数据双向绑定,大家都明白是proxy数据代理,但是在定义响应式数据的时候,有ref和reactive两种方式,如果判断该使用什么方式,是大家一直不很清楚地问题,下面这篇文章主要给大家介绍了关于从Vue3.x源码上理解ref和reactive的区别的相关资料,需要的朋友可以参考下
2023-02-06

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录