成本人视频在线观看_99re这里只有国产中文精品视频88_国产无码合集_肉岳疯狂69式激情的高潮_亚洲狠狠爱综合影院婷婷

當(dāng)前位置: 首頁  >  >聚焦 > > 正文

Mybatis占位符#{}和${}的區(qū)別?源碼解讀(二)

2023-02-09 23:07:28    來源:騰訊云

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文件核心流程

流程圖 (2).jpg

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

image.png

在 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()方法最后

image.png

原來 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)類如下

image.png

可以看出我們的 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)試截圖

image.png

可以看出我們的 ${}占位符文本的 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)方法中的

image.png

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ù)。

關(guān)鍵詞: MyBatis Spring

?上一篇: ?下一篇: