mybatis中#{}和${}的區(qū)別詳解
一、MyBatis中${}和#{}的區(qū)別
1.1 ${}和#{}演示
數(shù)據(jù)庫數(shù)據(jù):

dao接口:
List<User> findByUsername(String username); List<User> findByUsername2(String username);
Mapper.xml:
<!-- 使用#{} -->
<select id="findByUsername" parameterType="java.lang.String" resultType="com.lscl.entity.User">
select * from user where username like #{username}
</select>
<!-- 使用${},注意${}中的值必須要填value -->
<select id="findByUsername2" parameterType="java.lang.String" resultType="com.lscl.entity.User">
select * from user where username like '%${value}%'
</select>
執(zhí)行測試代碼:
@Test
public void findByUsername() throws Exception {
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
// true:自動提交
SqlSession session = factory.openSession(true);
UserDao userDao = session.getMapper(UserDao.class);
List<User> userList = userDao.findByUsername("%小%");
List<User> userList2 = userDao.findByUsername2("小");
System.out.println("userList: ");
for (User user : userList) {
System.out.println(user);
}
System.out.println("userList2: ");
for (User user : userList2) {
System.out.println(user);
}
session.close();
in.close();
}
查看執(zhí)行結果:

發(fā)現(xiàn)都能夠查詢出來
1.2 SQL注入問題
${}會產(chǎn)生SQL注入,#{}不會產(chǎn)生SQL注入問題
我們做一個測試:
List<User> userList2 = userDao.findByUsername2(" aaa' or 1=1 -- ");
System.out.println("userList2: ");
for (User user : userList2) {
System.out.println(user);
}
查詢生成的SQL語句:

我們傳遞的參數(shù)是aaa' or 1=1 --,導致查詢出來了全部的數(shù)據(jù)。
大家可以想象一下,如果我是要根據(jù)id刪除呢?
delete from user where id='${value}'
如果我傳遞的是:1' or 1=1; --,結果會是什么樣,我想大家應該已經(jīng)知道了。
我這里id是Integer類型,不好測試,就不帶大家測試了,大家有興趣可以自己私下測試。
如果上面使用的是#{}就不會出現(xiàn)SQL注入的問題了

1.3 ${}和#{}的區(qū)別
#{}匹配的是一個占位符,相當于JDBC中的一個?,會對一些敏感的字符進行過濾,編譯過后會對傳遞的值加上雙引號,因此可以防止SQL注入問題。
${}匹配的是真實傳遞的值,傳遞過后,會與sql語句進行字符串拼接。${}會與其他sql進行字符串拼接,不能預防sql注入問題。
查看#{}和${}生成的SQL語句:

String abc=“123”;
#{abc}="123"
${value}=123;
1.4 #{}底層是如何防止SQL注入的?
1.4.1 網(wǎng)上的答案
網(wǎng)上關于這類問題非常多,總結出來就兩個原因:
1)#{}底層采用的是PreparedStatement,會預編譯,因此不會產(chǎn)生SQL注入問題;
其實預編譯是MySQL自己本身的功能,和PreparedStatement沒關系;而且預編譯也不是咱們理解的那個預編譯,再者PreparedStatement底層默認根本沒有用到預編譯(要我們手動開啟)!詳細往下看
2)#{}不會產(chǎn)生字符串拼接,${}會產(chǎn)生字符串拼接,因此${}會出現(xiàn)SQL注入問題;
這兩個答案都經(jīng)不起深究,最終答案也只是停留在表面,也沒人知道具體是為什么。
1.4.2 為什么能防止SQL注入?
我們翻開MySQL驅(qū)動的源碼一看究竟;
打開PreparedStatement類的setString()方法(MyBatis在#{}傳遞參數(shù)時,是借助setString()方法來完成,${}則不是):

setString()方法全部源碼:
public void setString(int parameterIndex, String x) throws SQLException {
synchronized(this.checkClosed().getConnectionMutex()) {
if (x == null) {
this.setNull(parameterIndex, 1);
} else {
this.checkClosed();
int stringLength = x.length();
StringBuilder buf;
if (this.connection.isNoBackslashEscapesSet()) {
boolean needsHexEscape = this.isEscapeNeededForString(x, stringLength);
Object parameterAsBytes;
byte[] parameterAsBytes;
if (!needsHexEscape) {
parameterAsBytes = null;
buf = new StringBuilder(x.length() + 2);
buf.append('\'');
buf.append(x);
buf.append('\'');
if (!this.isLoadDataQuery) {
parameterAsBytes = StringUtils.getBytes(buf.toString(), this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());
} else {
parameterAsBytes = StringUtils.getBytes(buf.toString());
}
this.setInternal(parameterIndex, parameterAsBytes);
} else {
parameterAsBytes = null;
if (!this.isLoadDataQuery) {
parameterAsBytes = StringUtils.getBytes(x, this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());
} else {
parameterAsBytes = StringUtils.getBytes(x);
}
this.setBytes(parameterIndex, parameterAsBytes);
}
return;
}
String parameterAsString = x;
boolean needsQuoted = true;
if (this.isLoadDataQuery || this.isEscapeNeededForString(x, stringLength)) {
needsQuoted = false;
buf = new StringBuilder((int)((double)x.length() * 1.1D));
buf.append('\'');
for(int i = 0; i < stringLength; ++i) { //遍歷字符串,獲取到每個字符
char c = x.charAt(i);
switch(c) {
case '\u0000':
buf.append('\\');
buf.append('0');
break;
case '\n':
buf.append('\\');
buf.append('n');
break;
case '\r':
buf.append('\\');
buf.append('r');
break;
case '\u001a':
buf.append('\\');
buf.append('Z');
break;
case '"':
if (this.usingAnsiMode) {
buf.append('\\');
}
buf.append('"');
break;
case '\'':
buf.append('\\');
buf.append('\'');
break;
case '\\':
buf.append('\\');
buf.append('\\');
break;
case '¥':
case '?':
if (this.charsetEncoder != null) {
CharBuffer cbuf = CharBuffer.allocate(1);
ByteBuffer bbuf = ByteBuffer.allocate(1);
cbuf.put(c);
cbuf.position(0);
this.charsetEncoder.encode(cbuf, bbuf, true);
if (bbuf.get(0) == 92) {
buf.append('\\');
}
}
buf.append(c);
break;
default:
buf.append(c);
}
}
buf.append('\'');
parameterAsString = buf.toString();
}
buf = null;
byte[] parameterAsBytes;
if (!this.isLoadDataQuery) {
if (needsQuoted) {
parameterAsBytes = StringUtils.getBytesWrapped(parameterAsString, '\'', '\'', this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());
} else {
parameterAsBytes = StringUtils.getBytes(parameterAsString, this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());
}
} else {
parameterAsBytes = StringUtils.getBytes(parameterAsString);
}
this.setInternal(parameterIndex, parameterAsBytes);
this.parameterTypes[parameterIndex - 1 + this.getParameterIndexOffset()] = 12;
}
}
}
我們執(zhí)行#{}的查詢語句,打斷點觀察:

最終傳遞的參數(shù)如下:

最終傳遞的參數(shù)為:'aaa\' or 1=1 --
咱們在數(shù)據(jù)庫中執(zhí)行如下SQL語句(肯定是查詢不到數(shù)據(jù)的):
select * from user where username like 'aaa\' or 1=1 -- '

如果把PreparedStatement加的那根"/"去掉呢?我們執(zhí)行SQL試試:
select * from user where username like 'aaa' or 1=1 -- '

我們也可以通過MySQL的日志來觀察#{}和${}產(chǎn)生的SQL語句來分析問題:
1)開啟MySQL日志:
在MySQL配置文件中的[mysqld]下增加如下配置:
# 是否開啟mysql日志 0:關閉(默認值) 1:開啟 general-log=1 # mysql 日志的存放位置 general_log_file="D:/query.log"

2)重啟MySQL服務(要以管理員身份運行):

net stop mysqlnet start mysql
使用mybatis分別執(zhí)行如下兩條SQL語句:

查看MySQL日志:

1.5 #{}和${}的應用場景
既然#{}比${}好那么多,那為什么還要有${}這個東西存在呢?干脆都用#{}不就萬事大吉嗎?
其實不是的,${}也有用武之地,我們都知道${}會產(chǎn)生字符串拼接,來生成一個新的字符串
1.5.1 ${}和#{}用法上的區(qū)別
例如現(xiàn)在要進行模糊查詢,查詢user表中姓張的所有員工的信息
sql語句為:select * from user where name like '張%'
此時如果傳入的參數(shù)是 “張”
如果使用${}:select * from user where name like '${value}%'
生成的sql語句:select * from user where name like '張%'
如果使用#{}:select * from user where name like #{value}"%"
生成的sql語句:select * from user where name like '張'"%"
如果傳入的參數(shù)是 “張%”
使用#{}:select * from user where name like #{value}
生成的sql語句:select * from user where name like '張%'
使用${}:select * from user where name like '${value}'
生成的sql語句:select * from user where name like '張%'
通過上面的SQL語句我們能夠發(fā)現(xiàn)
#{}是會加上雙引號,而${}匹配的是真實的值。
還有一點就是如果使用${}的話,里面必須要填value,即:${value},#{}則隨意
1.5.2 什么情況下用${}?
場景舉例:

代碼測試:

執(zhí)行之后,發(fā)現(xiàn)執(zhí)行成功

我們可以切換一下,把${}改成#{},會出現(xiàn)SQL語法錯誤的異常

1.6 總結
1.6.1 SQL注入問題
MyBatis的#{}之所以能夠預防SQL注入是因為底層使用了PreparedStatement類的setString()方法來設置參數(shù),此方法會獲取傳遞進來的參數(shù)的每個字符,然后進行循環(huán)對比,如果發(fā)現(xiàn)有敏感字符(如:單引號、雙引號等),則會在前面加上一個'/'代表轉義此符號,讓其變?yōu)橐粋€普通的字符串,不參與SQL語句的生成,達到防止SQL注入的效果。
其次${}本身設計的初衷就是為了參與SQL語句的語法生成,自然而然會導致SQL注入的問題(不會考慮字符過濾問題)。
1.6.2 #{}和${}用法總結
1)#{}在使用時,會根據(jù)傳遞進來的值來選擇是否加上雙引號,因此我們傳遞參數(shù)的時候一般都是直接傳遞,不用加雙引號,${}則不會,我們需要手動加
2)在傳遞一個參數(shù)時,我們說了#{}中可以寫任意的值,${}則必須使用value;即:${value}
3)#{}針對SQL注入進行了字符過濾,${}則只是作為普通傳值,并沒有考慮到這些問題
4)#{}的應用場景是為給SQL語句的where字句傳遞條件值,${}的應用場景是為了傳遞一些需要參與SQL語句語法生成的值。
到此這篇關于MyBaits中#{}和${}的區(qū)別詳解的文章就介紹到這了,更多相關MyBaits #{}和${}內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java?Collections.sort()實現(xiàn)List排序的默認方法和自定義方法
這篇文章主要介紹了Java?Collections.sort()實現(xiàn)List排序的默認方法和自定義方法,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2017-06-06
Java中驗證 Mybatis 數(shù)據(jù)分片可以減輕GC壓力的操作方法
這篇文章主要介紹了Java中驗證 Mybatis 數(shù)據(jù)分片可以減輕GC壓力的操作方法,本文使用 Spock(可集成Spring Boot項目) 編寫測試用例,基于 Groovy (JVM語言),感興趣的朋友跟隨小編一起看看吧2024-12-12
Java編程rabbitMQ實現(xiàn)消息的收發(fā)
RabbitMQ是一個在AMQP基礎上完成的,可復用的企業(yè)消息系統(tǒng),本文通過實例來給大家分享通過操作rabbitMQ實現(xiàn)消息的收發(fā),感興趣的朋友可以參考下。2017-09-09
SpringCloud集成Sleuth和Zipkin的思路講解
Zipkin 是 Twitter 的一個開源項目,它基于 Google Dapper 實現(xiàn),它致力于收集服務的定時數(shù)據(jù),以及解決微服務架構中的延遲問題,包括數(shù)據(jù)的收集、存儲、查找和展現(xiàn),這篇文章主要介紹了SpringCloud集成Sleuth和Zipkin,需要的朋友可以參考下2022-11-11
Java循環(huán)調(diào)用多個timer實現(xiàn)定時任務
這篇文章主要介紹了Java循環(huán)調(diào)用多個timer實現(xiàn)定時任務,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-07-07

