Mybatis 作為國內(nèi)開發(fā)中常用到的半自動(dòng) orm 框架,相信大家都很熟悉,它提供了簡單靈活的xml映射配置,方便開發(fā)人員編寫簡單、復(fù)雜SQL,在國內(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.1github地址: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 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 mappedStatements = new StrictMap("Mapped Statements collection") .conflictMessageProducer((savedValue, targetValue) -> ". please check " + savedValue.getResource() + " and " + targetValue.getResource()); protected final Map caches = new StrictMap<>("Caches collection"); protected final Map resultMaps = new StrictMap<>("Result Maps collection"); protected final Map parameterMaps = new StrictMap<>("Parameter Maps collection"); protected final Map keyGenerators = new StrictMap<>("Key Generators collection"); protected final Set loadedResources = new HashSet<>(); protected final Map 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 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 contents; public MixedSqlNode(List 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)容
運(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 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_orderorder by create_time desclimit #{start},#{limit}
Mybatis
會(huì)通過上面提到 getBoundSql(Object parameterObject)
方法中的
sqlSourceParser.parse()
方法完成 #{} 占位符的處理,代碼如下:
public SqlSource parse(String originalSql, Class> parameterType, Map 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ù)。