在 MyBatis 映射文件編寫中,#{ } 與 ${ } 是最基礎也最容易混淆的兩種佔位符。搞清它們的本質區別,關係到 SQL 注入防護、查詢性能、動態 SQL 正確性 三大核心問題。本文從底層原理、適用場景、避坑指南三個維度深度解析。
| 維度 | #{ } (參數佔位符) |
${ } (字符串替換) |
|---|---|---|
| 本質機制 | JDBC PreparedStatement 預編譯參數綁定 | SQL 解析前的 純文本替換(類似模板引擎) |
| 類型處理 | 自動根據參數類型設置值,字符串自動加單引號 | 直接拼接字面值,不加引號、不轉義 |
| SQL 注入 | 完全防禦(參數與 SQL 結構分離) | 極高風險(拼接後再編譯,惡意輸入可改變 SQL 結構) |
| 緩存友好 | SQL 結構固定,利於數據庫解析樹緩存、Statement 緩存 | 每次生成不同 SQL 文本,導致硬解析、緩存失效 |
| 動態表名/列名 | 不支持(參數不能替代標識符) | 唯一支持(如 ORDER BY ${column}) |
| IN 子句列表 | 需配合 <foreach> 展開為多個 ? |
可直接拼接字符串列表(但極不推薦) |
// MyBatis 內部處理邏輯簡化版
String sql = "SELECT * FROM user WHERE id = ? AND name = ?"; // #{id}, #{name} 替換為 ?
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1, idValue); // 類型安全綁定
ps.setString(2, nameValue); // 自動處理引號、轉義
ResultSet rs = ps.executeQuery();
關鍵點:SQL 模板在數據庫層面只編譯一次,後續複用執行計劃;參數通過二進制協議傳輸,徹底隔離代碼與數據。
// MyBatis 內部處理邏輯簡化版
String sql = "SELECT * FROM user WHERE id = " + idValue + " AND name = '" + nameValue + "'"; // 直接拼接
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql); // 每次都是新 SQL 文本,需硬解析
風險點:若 nameValue = "admin' OR '1'='1",最終 SQL 變為 ... name = 'admin' OR '1'='1',繞過鑒權。
SELECT * FROM ${tablePrefix}_userORDER BY ${sortColumn} ${sortOrder}SELECT ${aggregateFunc}(score) FROM ...安全加固:使用 ${ } 時,務必在 Java 端做白名單校驗,絕不直接拼接用戶輸入。
<!-- 錯誤示範:IN ('1,2,3') 視為單個字符串 -->
SELECT * FROM user WHERE id IN (#{ids})
<!-- 正確:foreach 展開為 IN (?, ?, ?) -->
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
關於原文提到的「#{ } 將傳入數據都當成字符串」的疑慮:
TypeHandler 按實際類型調用 ps.setInt()、ps.setBoolean()、ps.setDate() 等,**不會** 轉為字符串再加引號。ps.setString(),JDBC 驅動負責加單引號並轉義特殊字符。ps.setNull(),需指定 JDBC 類型(可通過 #{prop,jdbcType=VARCHAR} 顯式指定)。默認用 #{ },不得已用 ${ } 且必加白名單校驗。這不僅是編碼規範,更是系統安全防線的第一道關卡。理解預編譯與文本替換的本質區別,才能在性能與安全的天平上做出正確抉擇。