MyBatis 映射檔中 #{} 與 ${} 看似相似,實則機制截然不同:前者生成預處理語句 (PreparedStatement),參數化查詢,徹底防 SQL 注入;後者為字符串直接替換 (Statement),拼接 SQL 片段,極高風險。本文將從原理、適用場景、安全性、實戰案例四個維度徹底釐清,並給出「能用 # 絕不用 $」的鐵律與例外處理指南。
| 維度 | #{param} (預處理) | ${param} (字符串替換) |
|---|---|---|
| 底層實現 | JDBC PreparedStatement 參數佔位符 ? | MyBatis 解析階段直接字符串替換 |
| SQL 結構 | 編譯期固定,參數傳遞不影響結構 | 替換後重新編譯,結構可被改變 |
| 類型處理 | 自動匹配 JDBC 類型,字符串自動加引號 | 原樣輸出,字符串需手動加引號 |
| SQL 注入防護 | ✅ 徹底防禦 (參數與代碼分離) | ❌ 完全無防護,極易注入 |
| 性能 | 可緩存執行計劃,重複執行高效 | 每次重新編譯,無法緩存計劃 |
| 適用場景 | 99% 參數傳遞 (WHERE 值、INSERT 值、UPDATE 值) | 動態 SQL 結構 (表名、欄位名、ORDER BY 欄位、分頁關鍵字) |
<!-- 安全:#{username} 生成 PreparedStatement --> <select id="getUser" resultType="User"> SELECT * FROM user WHERE username = #{username} </select> // 傳入 "admin' OR '1'=1" // 實際 SQL: SELECT * FROM user WHERE username = ? // 參數綁定: "admin' OR '1'=1" // 結果: 查詢字面用戶名為 "admin' OR '1'=1" 的記錄,無注入風險 <!-- 必須用 $:ORDER BY 欄位名無法參數化 --> <select id="listUsers" resultType="User"> SELECT * FROM user <if test="sortField != null"> ORDER BY ${sortField} ${sortOrder} </if> </select> // 傳入 sortField="create_time", sortOrder="DESC" // 實際 SQL: SELECT * FROM user ORDER BY create_time DESC // 注意:sortField 必須經過白名單驗證,防止注入! <select id="queryLog" resultType="Log"> SELECT * FROM ${tablePrefix}_log WHERE create_time > #{startTime} </select> // tablePrefix 經白名單驗證後傳入 <!-- 寫法 1:#{} + CONCAT (推薦,安全) --> SELECT * FROM user WHERE name LIKE CONCAT('%', #{keyword}, '%') <!-- 寫法 2:#{} 直接拼接 % (MyBatis 會自動處理) --> SELECT * FROM user WHERE name LIKE #{keyword} // Java 代碼傳入: "%張三%" // 寫法 3:${} (不推薦,需手動加引號且防注入) --> SELECT * FROM user WHERE name LIKE '%${keyword}%' #{} 絕不使用 ${}// Service 層驗證 public List<User> listUsers(String sortField, String sortOrder) { Set<String> allowedFields = Set.of("id", "name", "create_time", "status"); Set<String> allowedOrders = Set.of("ASC", "DESC"); if (!allowedFields.contains(sortField)) throw new IllegalArgumentException("非法排序欄位"); if (!allowedOrders.contains(sortOrder.toUpperCase())) throw new IllegalArgumentException("非法排序方向"); return mapper.listUsers(sortField, sortOrder); } log4j.logger.org.apache.ibatis=DEBUG) 觀察實際 SQL,確認 #{} 生成 ? 佔位符。#{} 會自動給字符串加單引號嗎?PreparedStatement.setString() 綁定參數,JDBC 驅動負責正確處理類型與轉義,從根本上分離代碼與數據。${} 只能用在 SELECT 嗎?LIKE '%#{keyword}%' 為什麼不行?#{} 會被解析為 ?,導致 SQL 變為 LIKE '?%' (引號包住佔位符),語法錯誤。正確寫法見案例 4。#{} 參數化,動態排序欄位同樣需白名單驗證。#{} = PreparedStatement 參數化 = 安全、高效、標準 (99% 場景)${} = 字符串模板替換 = 危險、僅限結構動態 (1% 場景 + 白名單)
牢記此鐵律,將 SQL 注入風險拒之門外。代碼審查時,看到 ${} 即為高危點,必須驗證白名單邏輯。