Mybatis源碼解析-DynamicSqlSource和RawSqlSource的區別


XMLLanguageDriver是ibatis的默認解析sql節點幫助類,其中的方法會調用生成DynamicSqlSourceRawSqlSource這兩個幫助類,本文將對此作下簡單的簡析

應用場景

我們在編寫mybatis的sql語句的時候,經常用到的是#{}的字符去替代其中的查詢入參,偶爾也會在網上看到${}這樣的字符使用。
經過筆者分析源碼得知,其實前者調用的為RawSqlSource幫助類進行生成具體的sql,而后者則是通過DynamicSqlSource幫助類來實現的。

選用邏輯

我們還是回到XMLLanguageDriver解析類,不管是xml方式還是注解方式解析sql,都會用到TextSqlNode這個包裝類,我們可以作下簡單的分析。
這里就以注解方式的解析sql為例

public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    //此處兼容XML方式的解析,條件以<script>為頭結點
    if (script.startsWith("<script>")) { // issue #3
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
      //①解析Configuration#variable變量
      script = PropertyParser.parse(script, configuration.getVariables()); // issue #127
      TextSqlNode textSqlNode = new TextSqlNode(script);
      // ②根據TextSqlNode的內部屬性isDynamic來進行解析幫助類的分配
      if (textSqlNode.isDynamic()) {
        return new DynamicSqlSource(configuration, textSqlNode);
      } else {
        return new RawSqlSource(configuration, script, parameterType);
      }
    }
  }

PropertyParser#parse() ①

廢話不講,直接上源碼

public static String parse(String string, Properties variables) {
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    // 此處專門查找`${}`關鍵字符,並替換為相應的variable值   
    return parser.parse(string);
  }

附屬其中的VariableTokenHandler內部靜態類-對指定字符進行處理,此處特指${}

	private Properties variables;

    public VariableTokenHandler(Properties variables) {
      this.variables = variables;
    }
	
    public String handleToken(String content) {
      //如果variables屬性中存在則直接替換,沒有則返回原來的內容
      if (variables != null && variables.containsKey(content)) {
        return variables.getProperty(content);
      }
      return "${" + content + "}";
    }
  1. VariableTokenHandler的替換原則為查找variables中是否含有${}內的key,沒有則返回原來的格式

  2. 此處插一句,這里的variables是如何配置呢,spring方面主要通過創建SqlsessionFactoryBean時設置configurationProperties屬性即可;
    也可通過ibatis主配置文件的properties節點進行配置

TextSqlNode#isDynamic() ②

我們直接看源碼

public boolean isDynamic() {
    // 只要找到${}這樣的字符則直接設置其內部屬性isDynamic=true
    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
    GenericTokenParser parser = createParser(checker);
    parser.parse(text);
    // 返回是否為${}方式解析
    return checker.isDynamic();
  }

TextSqlNode看來是用來校驗其中的sql語句是否含有${}字符,有則便用DynamicSqlSource方式解析,反之則用RawSqlSource方式解析

DynamicSqlSource

筆者此處針對含${}的SQL語句方式進行簡單的分析,而#{}的方式讀者可自行研究。也可查閱此篇簡單了解>>>Mybatis源碼解析-BoundSql

DynamicSqlSource#getBoundSql()

我們關注下其是如何組裝BoundSql對象的,源碼奉上

public BoundSql getBoundSql(Object parameterObject) {
	//sql語句解析,優先解析${}字符
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    rootSqlNode.apply(context);
    //創建StaticSqlSource,其也會去解析#{}字符。
    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);
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }

此處我們只關注TextSqlNode#apply()方法,看下其是如何解析${}

TextSqlNode#apply()

解析${}字符串

	public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context));
    //將解析得到的sql直接存到context對象中
    context.appendSql(parser.parse(text));
    return true;
  }
  
  private GenericTokenParser createParser(TokenHandler handler) {
    return new GenericTokenParser("${", "}", handler);
  }

附屬BindingTokenParser解析靜態內部類部分源碼

public String handleToken(String content) {
	  //此處拿取的相當於為方法調用的入參對象,比如dao接口query(List list),指代的便是list對象
	  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);
      }
      //ognl表達式獲取值
      Object value = OgnlCache.getValue(content, context.getBindings());
      return (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
    }
  1. 由上面得知,${}主要就是對dao接口的參數的內部屬性的直接調用,其不會像#{}那樣將入參拼裝成?預表達式,而是直接生成表達式執行SQL語句,那么這里就會涉及到sql惡意注入的風險

  2. 此處插一句,ognl指的是Object-Graph Navigation Language,其為強大的表達式語言,使用過Struct2/Freemaker等的就會熟悉其中的語法

小結

  1. DynamicSqlSource解析含有${}的sql語句,而RawSqlSource解析含有#{}的sql語句

  2. DynamicSqlSource涵蓋的操作比RawSqlSource多了一步,便是優先處理${}字符,其本身也會調用去解析#{}字符

  3. ${}語句的解析是最終轉換為Statement直接執行,其中的參數賦值是直接賦值,不做字符串引號拼裝;而#{}語句的解析是最終轉換為PrepareStatement預表達式來進行sql執行,安全性很高
    舉個例子:sql查詢語句為select * from user where name='admin' and pwd=${pwd}

  • 如果pwd參數傳入aaa or 1=1,則上述拼裝后的結果為select * from user where name='admin' and pwd=aaa or 1=1。這個表達式會恆為真,直接會繞過驗證,風險賊高
  • 如果上述采用#{pwd},則傳入aaa or 1=1,則最后的生成語句為select * from user='admin' and pwd='aaa or 1=1'。這個表達式驗證通不過,有較高的安全性,防止sql惡意注入
  1. 優推#{}方式操作入參,不建議使用${}方式


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM