专注解决预编译什么情况有效、什么情况失效、拼接和占位符的本质区别作者简介CodeStats8年Java后端架构实践者、WWAIC全周AI编程范式创始人、开源自研Java全栈框架CodeStats作者深耕Java底层原理与框架源码拆解专注手写复刻主流框架、剖析技术底层本质拒绝黑盒式开发。致力于用通俗语言拆解晦涩技术通过自研项目实证AI编程价值帮助数万开发者从会用框架进阶到懂架构、懂原理。主打底层源码解析、手写框架实战、AI工程化落地干货。前言几乎所有Java开发者都听过MyBatis 的#{}预编译可以杜绝 SQL 注入。但日常开发中很多人都遇到过这些无解难题明明用了#{}为什么依然出现 SQL 注入漏洞排序场景ORDER BY不能用#{}只能用${}原理是什么预编译到底预了什么数据库底层究竟如何执行 SQL本文抛开空话、直击本质从 SQL 完整执行流程、预编译核心原理、源码底层差异、失效场景全方位拆解一次性彻底搞懂 MyBatis 防注入的底层逻辑。一、SQL 在数据库的完整执行流程一条 SQL 从字符串到最终返回结果会经历三大核心阶段这是理解预编译的基础。text原始SQL字符串 ↓ ┌─────────────────────────────────────────────┐ │ 1. 解析阶段Parsing—— 确定SQL结构 │ │ - 词法分析拆分关键字、标识符、运算符Token │ │ - 语法分析生成抽象语法树AST │ │ - 语义分析校验表、字段权限与合法性 │ └─────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────┐ │ 2. 编译优化阶段Compile Optimize │ │ - 重写SQL语句、合并视图/子查询 │ │ - 优选索引、JOIN顺序生成最优执行计划 │ └─────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────┐ │ 3. 执行阶段Execute—— 仅处理数据 │ │ - 调用存储引擎读取数据 │ │ - 完成过滤、排序、聚合返回结果集 │ └─────────────────────────────────────────────┘ ↓ 返回结果核心结论SQL 的语法结构、执行规则在解析和优化阶段就已固定执行阶段仅负责数据读取无法改变 SQL 逻辑。二、预编译的核心本质复用执行计划2.1 普通 SQL 执行无预编译每次请求都会完整执行「解析→优化→执行」全流程重复操作、性能低效且存在注入风险。textSQL字符串 → 解析 → 优化 → 执行 → 返回结果 每次请求全量重复执行2.2 预编译 SQL 执行PreparedStatement预编译的核心提前固化 SQL 结构仅动态替换数据。首次执行完成解析、优化并缓存执行计划后续仅绑定参数执行。textSQL模板带?占位符→ 解析 → 优化 → 缓存执行计划 ↓ 参数动态绑定 → 复用执行计划 → 直接执行2.3 JDBC 预编译核心源码这是所有框架预编译的底层根基MyBatis、JPA 均基于此实现。java// 1. 发送SQL模板完成预编译固化结构 String sql SELECT * FROM users WHERE id ?; PreparedStatement ps conn.prepareStatement(sql); // 2. 仅绑定纯数据不改变SQL结构 ps.setInt(1, 100); ResultSet rs ps.executeQuery(); // 3. 复用执行计划无需重新解析优化 ps.setInt(1, 200); ResultSet rs2 ps.executeQuery();2.4 数据库底层预编译逻辑sql-- 首次预编译模板缓存执行计划 PREPARE stmt FROM SELECT * FROM users WHERE id ?; -- 二次、三次执行仅绑定参数复用计划 SET id 100; EXECUTE stmt USING id; SET id 200; EXECUTE stmt USING id;三、预编译为什么能防 SQL 注入核心原理SQL 结构解析与参数数据绑定完全隔离用户输入的所有内容只会被当作纯数据处理永远不会被数据库解析为 SQL 语法。模拟恶意输入1 OR 11text1. 模板解析阶段固化结构 SQLSELECT * FROM users WHERE id ? 此时无用户输入SQL逻辑完全固定 2. 参数绑定阶段仅传数据 ? 1 OR 11 // 整体视为普通字符串数据 3. 最终执行SQL SELECT * FROM users WHERE id 1 OR 11关键OR、AND、--等 SQL 关键字在解析阶段不存在无法篡改 SQL 逻辑从根源杜绝注入。四、彻底分清占位符?能替换什么、不能替换什么很多人用错#{}核心是不懂预编译占位符仅支持「数据值」不支持「SQL 结构」。4.1 ✅ 可参数化预编译有效sql-- 条件值 SELECT * FROM users WHERE id ? SELECT * FROM users WHERE name LIKE ? -- 增改数值 INSERT INTO users (name, age) VALUES (?, ?) UPDATE users SET name ? WHERE id ? -- 批量、函数参数 SELECT * FROM users WHERE id IN (?, ?, ?) SELECT * FROM users WHERE created_at DATE(?)4.2 ❌ 不可参数化预编译失效sql-- 表名、列名 SELECT * FROM ? -- ❌ 语法错误 SELECT ? FROM users -- ❌ 列名不能参数化 -- 排序、分组 SELECT * FROM users ORDER BY ? -- ❌ SELECT * FROM users GROUP BY ? -- ❌ -- 运算符、SQL关键字 SELECT * FROM users WHERE age ? 20 -- ❌4.3 底层原因数据库解析阶段必须确认完整 SQL 结构表名、字段、排序规则、运算符结构缺失则无法生成合法执行计划因此结构类内容无法用占位符替代。五、MyBatis#{}与${}本质区别源码级拆解5.1 核心特性对比特性#{}${}底层实现PreparedStatement 预编译Statement 字符串直接拼接防注入能力✅ 安全彻底防注入❌ 高危存在注入漏洞解析时机先解析模板后绑定参数先拼接字符串后整体执行适用场景所有普通参数值动态表名、列名、排序字段类型处理自动适配数据类型纯字符串替换无类型适配5.2 底层源码差异java// #{} 底层安全预编译 String sql SELECT * FROM users WHERE id ?; PreparedStatement ps connection.prepareStatement(sql); ps.setInt(1, userId); // 仅绑定数据结构固定 // ${} 底层危险字符串拼接 String sql SELECT * FROM users WHERE id userId; Statement stmt connection.createStatement(); stmt.executeQuery(sql); // 拼接后直接执行无防护5.3 经典错误案例错误用法用#{}传递表名触发语法异常xml!-- 错误写法 -- select idselectFromTable resultTypemap SELECT * FROM #{tableName} /select !-- 最终错误SQL表名被加上引号语法报错 -- SELECT * FROM user_tablexml!-- 正确写法动态结构只能用${}必须配合安全校验 -- select idselectFromTable resultTypemap SELECT * FROM ${tableName} /select六、SQL 注入的本质一句话看穿漏洞根源6.1 注入成立的必要条件用户输入在数据库解析阶段参与了 SQL 结构拼接即可触发注入攻击。6.2 注入攻击原理演示text恶意输入1 OR 11 【拼接模式${}—— 被注入】 输入拼接后SQLSELECT * FROM users WHERE id 1 OR 11 解析阶段识别OR关键字篡改查询逻辑查询全部数据 【预编译模式#{}—— 安全】 SQL模板SELECT * FROM users WHERE id ? 参数绑定? 1 OR 11 最终执行仅匹配字符串数据无逻辑篡改七、MyBatis 完整执行链路源码层级从 Mapper 接口到数据库执行完整看懂预编译执行链路textMapper接口注解/XML SQL ↓ MapperProxy动态代理拦截方法、解析SQL ↓ SqlSession事务管理、调用执行器 ↓ Executor一级缓存、调度StatementHandler ↓ StatementHandler#{}转?、构建预编译SQL ↓ ParameterHandler参数类型处理、纯数据绑定 ↓ JDBC PreparedStatement底层预编译 ↓ 数据库解析模板、复用执行计划、执行查询核心源码逻辑PreparedStatementHandler负责生成预编译模板DefaultParameterHandler仅做数据绑定全程不修改 SQL 结构。八、预编译失效的所有场景避坑重点预编译失效唯一核心SQL 模板在预编译前已完成字符串拼接。8.1 原生 JDBC 失效案例java// 高危先拼接再预编译防护完全失效 String sql SELECT * FROM users WHERE id userInput; PreparedStatement ps conn.prepareStatement(sql); // 恶意输入会直接嵌入SQL结构预编译毫无作用8.2 MyBatis 常见失效场景普通参数错误使用${}拼接动态表名、列名、排序字段使用${}且无任何校验自定义 SQL 拼接用户输入未做过滤8.3 动态结构安全解决方案必须使用${}时强制搭配白名单校验杜绝用户可控输入javapublic ListUser selectByColumn(String columnName, String value) { // 白名单过滤仅允许合法字段 SetString allowedColumns Set.of(id, name, email, age); if (!allowedColumns.contains(columnName)) { throw new IllegalArgumentException(非法查询字段); } return userMapper.selectByColumn(columnName, value); }九、终极总结全文核心预编译防护的是「数据」不防护「结构」仅值类型参数可使用#{}防注入不是用了PreparedStatement就安全提前拼接 SQL 会直接让预编译失效表名、列名、排序、分组等 SQL 结构无法参数化只能用${}所有${}动态拼接场景必须做白名单/枚举映射安全校验#{}安全无隐患${}高危仅可用于可控固定场景一句话终极口诀值用井号防注入结构美元必校验先拼模板后绑参注入漏洞无处藏如果本文帮你彻底吃透 MyBatis 预编译与 SQL 注入原理欢迎点赞、收藏、转发助力更多开发者摆脱框架黑盒认知