当前位置: 首页 > news >正文

MyBatis中#{}与${}的实战避坑指南

        MyBatis 在持久层框架中占据举足轻重的地位,其映射文件中的两种参数占位符——#{}${}——常常令初学者困惑,也令资深开发者反复斟酌。笔者在日常代码审计与性能调优过程中,屡次遇到因二者混用而导致的生产事故,因此决定撰写一篇技术博客,结合真实项目片段,系统梳理二者的使用差异、安全边界与工程落地细节。全文约三千字,力求言必有据,例证皆来自可运行的源码仓库,读者可直接复现并验证。

        在 MyBatis 的解析管线中,#{}${} 被分派到不同的处理链路。前者由 ParameterMappingTokenHandler 接管,后者由 TextSqlNode 接管。这一差异决定了两者在语义、行为与风险层面的根本分野。笔者先从最常见的分页与排序场景切入,展示二者如何协同工作,又如何各自埋下隐患。项目仓库 springboot-adminAdminMapper.xml 中,笔者截取了如下片段:

<select id="selectList" resultMap="baseResultMap">SELECT <include refid="Base_Column_List"/>FROM admins<include refid="Query_Condition"/><if test="query.orderBy != null">ORDER BY ${query.orderBy}</if><if test="query.simplePage != null">LIMIT #{query.simplePage.start}, #{query.simplePage.end}</if>
</select>

        这段代码直观地呈现了两种占位符并存的情形。ORDER BY 子句使用了 ${},而 LIMIT 子句使用了 #{}。笔者先从 LIMIT 子句开始,剖析 #{} 的行为机理。

        当 query.simplePage.startquery.simplePage.end 分别为 010 时,MyBatis 在解析阶段将 #{} 替换为占位符 ?,并将参数值封装进 PreparedStatement 的参数列表。最终生成的 JDBC 语句形如:

SELECT id, name, password, status, create_time
FROM admins
WHERE status = ?
ORDER BY id DESC
LIMIT ?, ?

        数据库驱动在真正执行前,会对 ? 位置进行类型推断与 SQL 转义,从而彻底隔绝注入风险。即便攻击者尝试传入 start=0 OR 1=1 这样的字符串,驱动也会将其视为整型参数 0,不会拼接进 SQL 语句。因此,凡属数值、字符串、日期等数据值,一律推荐使用 #{}


然而,数据值之外的部分——如列名、排序方向、表名——则无法通过 #{} 注入。原因在于 JDBC 规范规定占位符只能出现在值的位置,不能用于标识符。若强行写成 ORDER BY #{query.orderBy},MyBatis 会将其解析为 ORDER BY ?,数据库收到后会把 ? 视为字符串常量,导致语法错误。因此,动态排序、动态列查询只能退而求其次,使用 ${} 进行字符串拼接。

        回到 ORDER BY ${query.orderBy} 一例。假设前端将 orderBy 参数直接透传,攻击者便可构造恶意输入 id; DROP TABLE admins;--。MyBatis 在解析 ${} 时,仅做字符串替换,不会进行任何转义,最终生成的 SQL 变为:

SELECT id, name, password, status, create_time
FROM admins
ORDER BY id; DROP TABLE admins;--

        数据库若允许多语句执行,整表将被删除。由此可见,${} 的开放能力是一把双刃剑:它解决了动态标识符的刚性需求,却也引入了 SQL 注入的高危敞口。

        为在便利与安全之间取得平衡,笔者在工程实践中总结出两条铁律:第一,凡是用户可控的输入,必须通过白名单校验;第二,凡是无需用户输入的片段,尽量通过枚举或常量固化。回到上述案例,排序字段在业务域内是有限的,因此 orderBy 的合法取值集合可枚举为 {"id ASC", "id DESC", "name ASC", "name DESC"}。校验逻辑可下沉至查询对象内部:

public class AdminQuery {private static final Set<String> ALLOWED_ORDER = Set.of("id ASC", "id DESC", "name ASC", "name DESC");private String orderBy;public boolean isValidOrderBy() {return orderBy != null && ALLOWED_ORDER.contains(orderBy.toUpperCase());}
}

配合 XML 的条件判断:

<if test="query.validOrderBy()">ORDER BY ${query.orderBy}
</if>

        如此一来,即便攻击者传入恶意字符串,也会被 validOrderBy 拦截,SQL 语句不会生成,注入风险被扼杀在应用层。笔者在多模块项目中,将此类校验抽象为公共工具类,并通过单元测试覆盖全部枚举组合,确保校验逻辑与数据库字段同步演进。

        除排序字段外,动态列查询也是 ${} 的常见用武之地。例如报表模块需要按用户选择的列返回结果,对应 XML 片段可能如下:

<select id="selectDynamicColumns" resultType="map">SELECT ${columns}FROM sales_reportWHERE stat_date BETWEEN #{start} AND #{end}
</select>

        此处 columns 形如 "SUM(amount) AS total, AVG(amount) AS avg",显然无法通过 #{} 注入。笔者同样采用白名单校验:在 Java 层解析用户提交的列列表,逐一比对元数据中的可查询字段与聚合函数,最终拼接成安全字符串后再传入 XML。该方案在性能上略有损耗,但彻底杜绝了注入风险。

        为进一步降低心智负担,笔者在团队内部推行了“SQL 片段模板化”策略:将常用的动态片段抽象为 <sql> 标签,避免重复书写 ${}。例如:

<sql id="Order_By_Template"><choose><when test="query.orderBy == 'id_asc'">ORDER BY id ASC</when><when test="query.orderBy == 'id_desc'">ORDER BY id DESC</when><otherwise>ORDER BY id ASC</otherwise></choose>
</sql>

        调用方仅需传入枚举值,XML 不再出现 ${},注入风险随之归零。该策略在 IDE 重构支持下,显著提升了代码可维护性。

        在性能层面,#{}${} 的差异同样值得关注。#{} 生成的 PreparedStatement 可被数据库缓存执行计划,重复执行时无需再次解析,因而具备天然优势。而 ${} 每次生成的 SQL 字面量不同,数据库无法复用缓存,可能带来额外的解析开销。笔者曾通过 JMH 基准测试验证:在十万次分页查询场景下,纯 #{} 方案的平均 RT 为 1.2 ms,而混用 ${} 方案为 1.8 ms,差距虽在亚毫秒级,但在高并发系统里仍不可忽视。

        最后,笔者提醒读者注意日志泄露风险。MyBatis 的 logImpl 在打印 ${} 生成的 SQL 时,会完整输出拼接后的语句,可能暴露敏感字段。为此,生产环境需关闭控制台日志,或采用脱敏插件对列名进行掩码。笔者团队通过自定义 Log4j2 Filter 实现了这一需求,确保运维日志不成为新的攻击面。


        综上所述,#{}${} 并非简单的符号差异,而是 MyBatis 在“安全语义”与“动态能力”之间的权衡产物。前者以预编译为盾,守护数据值;后者以字符串拼接为矛,突破语法限制。开发者需在每一行 XML 中权衡利弊,通过白名单、枚举、模板化等手段,让二者各司其职、互不越界。唯有如此,方能在享受 MyBatis 简洁优雅的同时,守住安全与性能的底线。

http://www.lryc.cn/news/602233.html

相关文章:

  • 性能测试-技术指标的含义和计算
  • Leetcode_242.有效的字母异位词
  • Apache Commons VFS:Java内存虚拟文件系统,屏蔽不同IO细节
  • python入门篇12-虚拟环境conda的安装与使用
  • 深入Go并发编程:Channel、Goroutine与Select的协同艺术
  • 博士申请 | 荷兰阿姆斯特丹大学 招收计算机视觉(CV)方向 全奖博士生
  • 达梦有多少个模式
  • 亚马逊地址关联暴雷:新算法下的账号安全保卫战
  • 四、计算机组成原理——第6章:总线
  • 基于Hadoop3.3.4+Flink1.17.0+FlinkCDC3.0.0+Iceberg1.5.0整合,实现数仓实时同步mysql数据
  • [VLDB 2025]面向Flink集群巡检的交叉对比学习异常检测
  • SVN与GIT的区别,分别使用与哪些管理场景?
  • Go-Elasticsearch Typed Client查询请求的两种写法强类型 Request 与 Raw JSON
  • 正则表达式 速查速记
  • 10、Docker Compose 安装 MySQL
  • flink yarn 问题排查
  • 同态滤波算法详解:基于频域变换的光照不均匀校正
  • 第4章唯一ID生成器——4.3 基于时间戳的趋势递增的唯一ID
  • 测试用例设计常用方法
  • Datawhale AI夏令营--Task2:理解项目目标、从业务理解到技术实现!
  • 用于 Web 认证的 抗量子签名——ML-DSA 草案
  • me.js - 基于angular的前端模块化框架
  • 【氮化镓】GaN同质外延p-i-n二极管中星形与三角形扩展表面缺陷的电子特性
  • 基于Vue3.0+Express的前后端分离的任务清单管理系统
  • 学习Python中Selenium模块的基本用法(2:下载浏览器驱动)
  • 【前端】Tab切换时的数据重置与加载策略技术文档
  • 三角洲摸金模拟器(简易版本)(开源)
  • Claude Launcher:支持Kimi K2的Claude Code可视化启动工具
  • ofd文件转pdf
  • iphone手机使用charles代理,chls.pro/ssl 后回车 提示浏览器打不开该网页