源碼解讀Mybatis占位符#和$的區(qū)別
Mybatis 作為國(guó)內(nèi)開發(fā)中常用到的半自動(dòng) orm 框架,相信大家都很熟悉,它提供了簡(jiǎn)單靈活的xml映射配置,方便開發(fā)人員編寫簡(jiǎn)單、復(fù)雜SQL,在國(guó)內(nèi)互聯(lián)網(wǎng)公司使用眾多。
本文針對(duì)筆者日常開發(fā)中對(duì) Mybatis 占位符 #{} 和 ${} 使用時(shí)機(jī)結(jié)合源碼,思考總結(jié)而來
Mybatis版本 3.5.11Spring boot版本 3.0.2mybatis-spring版本 3.0.1- github地址:https://github.com/wayn111 歡迎大家關(guān)注,點(diǎn)個(gè)star
一. 啟動(dòng)時(shí),mybatis-spring 解析xml文件流程圖
Spring 項(xiàng)目啟動(dòng)時(shí),mybatis-spring 自動(dòng)初始化解析xml文件核心流程

Mybatis 在 buildSqlSessionFactory() 會(huì)遍歷所有 mapperLocations(xml文件) 調(diào)用 xmlMapperBuilder.parse()解析,源碼如下

在 parse() 方法中, Mybatis 通過 configurationElement(parser.evalNode("/mapper")) 方法解析xml文件中的各個(gè)標(biāo)簽
public class XMLMapperBuilder extends BaseBuilder {
...
private final MapperBuilderAssistant builderAssistant;
private final Map<String, XNode> sqlFragments;
...
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// xml文件解析邏輯
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
// 解析xml文件內(nèi)的namespace、cache-ref、cache、parameterMap、resultMap、sql、select、insert、update、delete等各種標(biāo)簽
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
}
最后會(huì)把 namespace、cache-ref、cache、parameterMap、resultMap、select、insert、update、delete等標(biāo)簽內(nèi)容解析結(jié)果放到 builderAssistant 對(duì)象中,將sql標(biāo)簽解析結(jié)果放到sqlFragments對(duì)象中,其中 由于 builderAssistant 對(duì)象會(huì)保存select、insert、update、delete標(biāo)簽內(nèi)容解析結(jié)果我們對(duì) builderAssistant 對(duì)象進(jìn)行深入了解
public class MapperBuilderAssistant extends BaseBuilder {
...
}
public abstract class BaseBuilder {
protected final Configuration configuration;
...
}
public class Configuration {
...
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
.conflictMessageProducer((savedValue, targetValue) ->
". please check " + savedValue.getResource() + " and " + targetValue.getResource());
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");
protected final Set<String> loadedResources = new HashSet<>();
protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
...
}builderAssistant 對(duì)象繼承至 BaseBuilder,BaseBuilder 類中包含一個(gè) configuration 對(duì)象屬性, configuration 對(duì)象中會(huì)保存xml文件標(biāo)簽解析結(jié)果至自身對(duì)應(yīng)屬性mappedStatements、caches、resultMaps、sqlFragments。
這里有個(gè)問題上面提到的sql標(biāo)簽結(jié)果會(huì)放到 XMLMapperBuilder 類的 sqlFragments 對(duì)象中,為什么 Configuration 類中也有個(gè) sqlFragments 屬性?
這里回看上文 buildSqlSessionFactory() 方法最后

原來 XMLMapperBuilder 類中的 sqlFragments 屬性就來自Configuration類??
回到主題,在 buildStatementFromContext(context.evalNodes("select|insert|update|delete")) 方法中會(huì)通過如下調(diào)用
buildStatementFromContext(List<XNode> list, String requiredDatabaseId) -> parseStatementNode() -> createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) -> parseScriptNode() -> parseDynamicTags(context)
最后通過parseDynamicTags(context) 方法解析 select、insert、update、delete 標(biāo)簽內(nèi)容將結(jié)果保存在 MixedSqlNode 對(duì)象中的 SqlNode 集合中
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
}SqlNode 是一個(gè)接口,有10個(gè)實(shí)現(xiàn)類如下

可以看出我們的 select、insert、update、delete 標(biāo)簽中包含的各個(gè)文本(包含占位符 #{} 和 ${})、子標(biāo)簽都有對(duì)應(yīng)的 SqlNode 實(shí)現(xiàn)類,后續(xù)運(yùn)行中, Mybatis 對(duì)于 select、insert、update、delete 標(biāo)簽的 sql 語句處理都與這里的 SqlNode 各個(gè)實(shí)現(xiàn)類相關(guān)。自此我們 mybatis-spring 初始化流程中相關(guān)的重要代碼都過了一遍。
二. 運(yùn)行中,sql語句占位符 #{} 和 ${} 的處理
這里直接給出xml文件查詢方法標(biāo)簽內(nèi)容
<select id="findNewBeeMallOrderList" parameterType="Map" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from tb_newbee_mall_order
<where>
<if test="orderNo!=null and orderNo!=''">
and order_no = #{orderNo}
</if>
<if test="userId!=null and userId!=''">
and user_id = #{userId}
</if>
<if test="payType!=null and payType!=''">
and pay_type = #{payType}
</if>
<if test="orderStatus!=null and orderStatus!=''">
and order_status = #{orderStatus}
</if>
<if test="isDeleted!=null and isDeleted!=''">
and is_deleted = #{isDeleted}
</if>
<if test="startTime != null and startTime.trim() != ''">
and create_time > #{startTime}
</if>
<if test="endTime != null and endTime.trim() != ''">
and create_time < #{endTime}
</if>
</where>
<if test="sortField!=null and order!=null">
order by ${sortField} ${order}
</if>
<if test="start!=null and limit!=null">
limit #{start},#{limit}
</if>
</select>
運(yùn)行時(shí) Mybatis 動(dòng)態(tài)代理 MapperProxy 對(duì)象的調(diào)用流程,如下:
-> newBeeMallOrderMapper.findNewBeeMallOrderList(pageUtil);
-> MapperProxy.invoke(Object proxy, Method method, Object[] args)
-> MapperProxy.invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession)
-> MapperMethod.execute(SqlSession sqlSession, Object[] args)
-> MapperMethod.executeForMany(SqlSession sqlSession, Object[] args)
-> SqlSessionTemplate.selectList(String statement, Object parameter)
-> SqlSessionInterceptor.invoke(Object proxy, Method method, Object[] args)
-> DefaultSqlSession.selectList(String statement, Object parameter)
-> DefaultSqlSession.selectList(String statement, Object parameter, RowBounds rowBounds)
-> DefaultSqlSession.selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler)
-> CachingExecutor.query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)
-> MappedStatement.getBoundSql(Object parameterObject)
-> DynamicSqlSource.getBoundSql(Object parameterObject)
-> MixedSqlNode.apply(DynamicContext context) // ${} 占位符處理
-> SqlSourceBuilder.parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) // #{} 占位符處理Mybatis 通過 DynamicSqlSource.getBoundSql(Object parameterObject) 方法對(duì) select、insert、update、delete 標(biāo)簽內(nèi)容做 sql 轉(zhuǎn)換處理,代碼如下:
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}2.1 ${} 占位符處理
在 rootSqlNode.apply(context) -> MixedSqlNode.apply(DynamicContext context) 中會(huì)將 SqlNode 集合拼接成實(shí)際要執(zhí)行的 sql 語句
保存在 DynamicContext 對(duì)象中。這里給出 SqlNode 集合的調(diào)試截圖

可以看出我們的 ${} 占位符文本的 SqlNode 實(shí)現(xiàn)類為 TextSqlNode,apply方法相關(guān)操作如下
public class TextSqlNode implements SqlNode {
...
@Override
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}
private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}
// 劃重點(diǎn),${}占位符替換邏輯在就handleToken(String content)方法中
@Override
public String handleToken(String content) {
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
context.getBindings().put("value", parameter);
}
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
checkInjection(srtValue);
return srtValue;
}
}
public class GenericTokenParser {
public String parse(String text) {
...
do {
...
if (end == -1) {
...
} else {
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
...
} while (start > -1);
...
return builder.toString();
}
} 劃重點(diǎn),${} 占位符處理如下
handleToken(String content) 方法中, Mybatis 會(huì)通過 ognl 表達(dá)式將 ${} 的結(jié)果直接拼接在 sql 語句中,由此我們得知 ${} 占位符拼接的字段就是我們傳入的原樣字段,有著 Sql 注入風(fēng)險(xiǎn)
2.2 #{} 占位符處理
#{} 占位符文本的 SqlNode 實(shí)現(xiàn)類為 StaticTextSqlNode,查看源碼
public class StaticTextSqlNode implements SqlNode {
private final String text;
public StaticTextSqlNode(String text) {
this.text = text;
}
@Override
public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
}
}StaticTextSqlNode 會(huì)直接將節(jié)點(diǎn)內(nèi)容拼接在 sql 語句中,也就是說在 rootSqlNode.apply(context) 方法執(zhí)行完畢后,此時(shí)的 sql 語句如下
select order_id, order_no, user_id, total_price,
pay_status, pay_type, pay_time, order_status,
extra_info, user_name, user_phone, user_address,
is_deleted, create_time, update_time
from tb_newbee_mall_order
order by create_time desc
limit #{start},#{limit}Mybatis 會(huì)通過上面提到 getBoundSql(Object parameterObject) 方法中的

sqlSourceParser.parse() 方法完成 #{} 占位符的處理,代碼如下:
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql;
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);
}
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}看到了熟悉的 #{ 占位符沒有,哈哈??, Mybatis 對(duì)于 #{} 占位符的處理就在 GenericTokenParser類的 parse() 方法中,代碼如下:
public class GenericTokenParser {
public String parse(String text) {
...
do {
...
if (end == -1) {
...
} else {
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
...
} while (start > -1);
...
return builder.toString();
}
}
public class SqlSourceBuilder extends BaseBuilder {
...
// 劃重點(diǎn),#{}占位符替換邏輯在就SqlSourceBuilder.handleToken(String content)方法中
@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
}劃重點(diǎn),#{} 占位符處理如下
handleToken(String content) 方法中, Mybatis 會(huì)直接將我們的傳入?yún)?shù)轉(zhuǎn)換成問號(hào)(就是 jdbc 規(guī)范中的問號(hào)),也就是說我們的 sql 語句是預(yù)處理的。能夠避免 sql 注入問題
三. 總結(jié)
由上經(jīng)過源碼分析,我們知道 Mybatis 對(duì) #{} 占位符是直接轉(zhuǎn)換成問號(hào),拼接預(yù)處理 sql。 ${} 占位符是原樣拼接處理,有sql注入風(fēng)險(xiǎn),最好避免由客戶端傳入此參數(shù)。
到此這篇關(guān)于Mybatis占位符#和$的區(qū)別 源碼解讀的文章就介紹到這了,更多相關(guān)Mybatis占位符#和$的區(qū)別內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java Map 通過 key 或者 value 過濾的實(shí)例代碼
這篇文章主要介紹了Java Map 通過 key 或者 value 過濾的實(shí)例代碼,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-06-06
RSA加密的方式和解密方式實(shí)現(xiàn)方法(推薦)
下面小編就為大家?guī)硪黄猂SA加密的方式和解密方式實(shí)現(xiàn)方法(推薦)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-06-06
在Ubuntu上部署SpringBoot應(yīng)用的操作步驟
隨著云計(jì)算和容器化技術(shù)的普及,Linux 服務(wù)器已成為部署 Web 應(yīng)用程序的主流平臺(tái)之一,Java 作為一種跨平臺(tái)的編程語言,具有廣泛的應(yīng)用場(chǎng)景,本文將詳細(xì)介紹如何在 Ubuntu 服務(wù)器上部署 Java 應(yīng)用,需要的朋友可以參考下2025-01-01
詳解基于Spring Cloud幾行配置完成單點(diǎn)登錄開發(fā)
這篇文章主要介紹了詳解基于Spring Cloud幾行配置完成單點(diǎn)登錄開發(fā),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-02-02
SpringBoot詳解實(shí)現(xiàn)自定義異常處理頁面方法
SpringBoot是Spring全家桶的成員之一,是一種整合Spring技術(shù)棧的方式(或者說是框架),同時(shí)也是簡(jiǎn)化Spring的一種快速開發(fā)的腳手架2022-06-06
IDEA將Maven項(xiàng)目中指定文件夾下的xml等文件編譯進(jìn)classes的方法
這篇文章主要介紹了IDEA將Maven項(xiàng)目中指定文件夾下的xml等文件編譯進(jìn)classes的方法,幫助大家更好的利用IDEA進(jìn)行Java的開發(fā)學(xué)習(xí),感興趣的朋友可以了解下2021-01-01

