源碼解讀Mybatis占位符#和$的區(qū)別
Mybatis 作為國內(nèi)開發(fā)中常用到的半自動 orm 框架,相信大家都很熟悉,它提供了簡單靈活的xml映射配置,方便開發(fā)人員編寫簡單、復雜SQL,在國內(nèi)互聯(lián)網(wǎng)公司使用眾多。
本文針對筆者日常開發(fā)中對 Mybatis
占位符 #{}
和 ${}
使用時機結(jié)合源碼,思考總結(jié)而來
Mybatis
版本 3.5.11Spring boot
版本 3.0.2mybatis-spring
版本 3.0.1- github地址:https://github.com/wayn111 歡迎大家關(guān)注,點個star
一. 啟動時,mybatis-spring 解析xml文件流程圖
Spring
項目啟動時,mybatis-spring
自動初始化解析xml文件核心流程
Mybatis
在 buildSqlSessionFactory()
會遍歷所有 mapperLocations(xml文件)
調(diào)用 xmlMapperBuilder.parse()
解析,源碼如下
在 parse() 方法中, Mybatis
通過 configurationElement(parser.evalNode("/mapper"))
方法解析xml文件中的各個標簽
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等各種標簽 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); } } }
最后會把 namespace、cache-ref、cache、parameterMap、resultMap、select、insert、update、delete
等標簽內(nèi)容解析結(jié)果放到 builderAssistant 對象中,將sql標簽解析結(jié)果放到sqlFragments對象中,其中 由于 builderAssistant 對象會保存select、insert、update、delete
標簽內(nèi)容解析結(jié)果我們對 builderAssistant 對象進行深入了解
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 對象繼承至 BaseBuilder,BaseBuilder 類中包含一個 configuration 對象屬性, configuration 對象中會保存xml文件標簽解析結(jié)果至自身對應屬性mappedStatements、caches、resultMaps、sqlFragments
。
這里有個問題上面提到的sql標簽結(jié)果會放到 XMLMapperBuilder 類的 sqlFragments 對象中,為什么 Configuration 類中也有個 sqlFragments 屬性?
這里回看上文 buildSqlSessionFactory()
方法最后
原來 XMLMapperBuilder 類中的 sqlFragments 屬性就來自Configuration類??
回到主題,在 buildStatementFromContext(context.evalNodes("select|insert|update|delete"))
方法中會通過如下調(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
標簽內(nèi)容將結(jié)果保存在 MixedSqlNode 對象中的 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 是一個接口,有10個實現(xiàn)類如下
可以看出我們的 select、insert、update、delete
標簽中包含的各個文本(包含占位符 #{} 和 ${})、子標簽都有對應的 SqlNode 實現(xiàn)類,后續(xù)運行中, Mybatis
對于 select、insert、update、delete
標簽的 sql 語句處理都與這里的 SqlNode 各個實現(xiàn)類相關(guān)。自此我們 mybatis-spring
初始化流程中相關(guān)的重要代碼都過了一遍。
二. 運行中,sql語句占位符 #{} 和 ${} 的處理
這里直接給出xml文件查詢方法標簽內(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>
運行時 Mybatis
動態(tài)代理 MapperProxy
對象的調(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)
方法對 select、insert、update、delete
標簽內(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)
中會將 SqlNode 集合拼接成實際要執(zhí)行的 sql 語句
保存在 DynamicContext 對象中。這里給出 SqlNode 集合的調(diào)試截圖
可以看出我們的 ${}
占位符文本的 SqlNode 實現(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); } // 劃重點,${}占位符替換邏輯在就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(); } }
劃重點,${}
占位符處理如下
handleToken(String content) 方法中, Mybatis
會通過 ognl 表達式將 ${}
的結(jié)果直接拼接在 sql 語句中,由此我們得知 ${}
占位符拼接的字段就是我們傳入的原樣字段,有著 Sql 注入風險
2.2 #{} 占位符處理
#{}
占位符文本的 SqlNode 實現(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 會直接將節(jié)點內(nèi)容拼接在 sql 語句中,也就是說在 rootSqlNode.apply(context)
方法執(zhí)行完畢后,此時的 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
會通過上面提到 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
對于 #{}
占位符的處理就在 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 { ... // 劃重點,#{}占位符替換邏輯在就SqlSourceBuilder.handleToken(String content)方法中 @Override public String handleToken(String content) { parameterMappings.add(buildParameterMapping(content)); return "?"; } }
劃重點,#{}
占位符處理如下
handleToken(String content) 方法中, Mybatis
會直接將我們的傳入?yún)?shù)轉(zhuǎn)換成問號(就是 jdbc 規(guī)范中的問號),也就是說我們的 sql 語句是預處理的。能夠避免 sql 注入問題
三. 總結(jié)
由上經(jīng)過源碼分析,我們知道 Mybatis
對 #{}
占位符是直接轉(zhuǎn)換成問號,拼接預處理 sql。 ${}
占位符是原樣拼接處理,有sql注入風險,最好避免由客戶端傳入此參數(shù)。
到此這篇關(guān)于Mybatis占位符#和$的區(qū)別 源碼解讀的文章就介紹到這了,更多相關(guān)Mybatis占位符#和$的區(qū)別內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于SpringMVC接受JSON參數(shù)詳解及常見錯誤總結(jié)
下面小編就為大家分享一篇基于SpringMVC接受JSON參數(shù)詳解及常見錯誤總結(jié),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-03-03Java中使用JavaMail多發(fā)郵件及郵件的驗證和附件實現(xiàn)
這篇文章主要介紹了Java中使用Java Mail多發(fā)郵件及郵件的驗證和附件實現(xiàn),包括在郵件中加入圖片等功能的實現(xiàn)講解,需要的朋友可以參考下2016-02-02Springboot內(nèi)置的工具類之CollectionUtils示例講解
這篇文章主要介紹了Springboot內(nèi)置的工具類之CollectionUtils,本文通過示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-12-12- 本章具體介紹了HashMap、TreeMap兩種集合的基本使用方法和區(qū)別,圖解穿插代碼實現(xiàn)。?JAVA成仙路從基礎(chǔ)開始講,后續(xù)會講到JAVA高級,中間會穿插面試題和項目實戰(zhàn),希望能給大家?guī)韼椭?/div> 2022-03-03
開源項目ERM模型轉(zhuǎn)jpa實體maven插件使用
這篇文章主要為大家介紹了開源項目ERM模型轉(zhuǎn)jpa實體maven插件的使用說明,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步2022-03-03最新評論