首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

MyBatis 中 #{ } 與 ${ } 核心差異深度解析:預編譯與字符串替換的本質區別

     編輯:Java 綜合教學

前言:一個容易踩坑的基礎知識點

在 MyBatis 映射文件編寫中,#{ } 與 ${ } 是最基礎也最容易混淆的兩種佔位符。搞清它們的本質區別,關係到 SQL 注入防護、查詢性能、動態 SQL 正確性 三大核心問題。本文從底層原理、適用場景、避坑指南三個維度深度解析。

一、 核心結論速覽

維度 #{ } (參數佔位符) ${ } (字符串替換)
本質機制 JDBC PreparedStatement 預編譯參數綁定 SQL 解析前的 純文本替換(類似模板引擎)
類型處理 自動根據參數類型設置值,字符串自動加單引號 直接拼接字面值,不加引號、不轉義
SQL 注入 完全防禦(參數與 SQL 結構分離) 極高風險(拼接後再編譯,惡意輸入可改變 SQL 結構)
緩存友好 SQL 結構固定,利於數據庫解析樹緩存、Statement 緩存 每次生成不同 SQL 文本,導致硬解析、緩存失效
動態表名/列名 不支持(參數不能替代標識符) 唯一支持(如 ORDER BY ${column})
IN 子句列表 需配合 <foreach> 展開為多個 ? 可直接拼接字符串列表(但極不推薦)

二、 底層原理圖解

2.1 #{ } —— 預編譯參數化流程

// 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 模板在數據庫層面只編譯一次,後續複用執行計劃;參數通過二進制協議傳輸,徹底隔離代碼與數據。

2.2 ${ } —— 文本替換流程


// 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',繞過鑒權。

三、 實戰場景決策指南

✅ 必須使用 #{ } 的場景(95%+ 情況)

  • 所有 WHERE 條件值、INSERT/UPDATE 值、存儲過程參數
  • 任何來自用戶輸入、外部接口的不可信數據
  • 追求查詢計劃緩存複用的高頻 SQL

⚠️ 僅在無法用 #{ } 時使用 ${ } 的場景

  1. 動態表名/Schema名:SELECT * FROM ${tablePrefix}_user
  2. 動態列名:ORDER BY ${sortColumn} ${sortOrder}
  3. 數據庫函數/關鍵字作為參數:SELECT ${aggregateFunc}(score) FROM ...

安全加固:使用 ${ } 時,務必在 Java 端做白名單校驗,絕不直接拼接用戶輸入。

💡 進階技巧:IN 子句的正確姿勢

<!-- 錯誤示範: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>

四、 類型處理的細節補充

關於原文提到的「#{ } 將傳入數據都當成字符串」的疑慮:

  • 非字符串類型:MyBatis 通過 TypeHandler 按實際類型調用 ps.setInt()、ps.setBoolean()、ps.setDate() 等,**不會** 轉為字符串再加引號。
  • 字符串類型:調用 ps.setString(),JDBC 驅動負責加單引號並轉義特殊字符。
  • null 值:調用 ps.setNull(),需指定 JDBC 類型(可通過 #{prop,jdbcType=VARCHAR} 顯式指定)。

五、 總結

默認用 #{ },不得已用 ${ } 且必加白名單校驗。這不僅是編碼規範,更是系統安全防線的第一道關卡。理解預編譯與文本替換的本質區別,才能在性能與安全的天平上做出正確抉擇。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved