深度解析Mybatis中#{}與${}的區(qū)別

Q:#$的區(qū)別是什么捡需?
A:#會(huì)在sql中使用占位符坑傅,有效得防止了sql注入呜呐,$會(huì)把參數(shù)直接拼接到sql中可能會(huì)引發(fā)sql注入战得。
如果你只知道這些區(qū)別,或者想知道為什么兩種寫法會(huì)產(chǎn)生這些區(qū)別庸推,那么你就可以靜下來看看下面我寫的常侦。
DynamicSqlSource中有一個(gè)getBoundSql方法,如下:

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);
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }

注意其中的apply方法贬媒,這將是$被解析的地方聋亡。其中rootSqlNode是通過構(gòu)造函數(shù)傳遞過來的一般會(huì)是一個(gè)MixedSqlNode,看MixedSqlNodeapply方法:

public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : contents) {
      sqlNode.apply(context);
    }
    return true;
  }

繼續(xù)對(duì)內(nèi)部的SqlNode調(diào)用apply方法际乘,其中文本類型的sql會(huì)被解析為TextSqlNode坡倔。下面我們看一下TextSqlNodeapply方法:

public boolean apply(DynamicContext context) {
    GenericTokenParser parser = new GenericTokenParser("${", "}", new BindingTokenParser(context));
    context.appendSql(parser.parse(text));
    return true;
  }
private static class BindingTokenParser implements TokenHandler {

    private DynamicContext context;

    public BindingTokenParser(DynamicContext context) {
      this.context = context;
    }

    public String handleToken(String content) {
      try {
        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());
        return (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
      } catch (OgnlException e) {
        throw new BuilderException("Error evaluating expression '" + content + "'. Cause: " + e, e);
      }
    }
  }

TextSqlNode中的apply所有需要的內(nèi)部類BindingTokenParser也一并貼出來了。
看到這里我們應(yīng)該先看看GenericTokenParser中怎么為我們解析的:

public String parse(String text) {
    StringBuilder builder = new StringBuilder();
    if (text != null) {
      String after = text;
      int start = after.indexOf(openToken);
      int end = after.indexOf(closeToken);
      while (start > -1) {
        if (end > start) {
          String before = after.substring(0, start);
          String content = after.substring(start + openToken.length(), end);
          String substitution;

          // check if variable has to be skipped
          if (start > 0 && text.charAt(start - 1) == '\\') {
            before = before.substring(0, before.length() - 1);
            substitution = new StringBuilder(openToken).append(content).append(closeToken).toString();
          } else {
            substitution = handler.handleToken(content);
          }

          builder.append(before);
          builder.append(substitution);
          after = after.substring(end + closeToken.length());
        } else if (end > -1) {
          String before = after.substring(0, end);
          builder.append(before);
          builder.append(closeToken);
          after = after.substring(end + closeToken.length());
        } else {
          break;
        }
        start = after.indexOf(openToken);
        end = after.indexOf(closeToken);
      }
      builder.append(after);
    }
    return builder.toString();
  }

不出意料,這個(gè)類只是將sql中被openTokencloseToken所包圍的tokenTokenHandle類來解析罪塔,那我們就可以繼續(xù)回到TextSqlNode中了投蝉,可以看到上面的BindingTokenParser中有一個(gè)handleToken方法這就是產(chǎn)生最開始Q&A區(qū)別的地方,這個(gè)方法會(huì)直接將${}所包圍的token用傳遞進(jìn)來的參數(shù)解析出來并返回解析之后的value征堪,也就是這個(gè)BindingTokenParser會(huì)直接將sql中的${}部分用參數(shù)解析完并拼接回sql瘩缆。如果你是一個(gè)老手,估計(jì)都不會(huì)濫用${}佃蚜,那讓我們回到開始的DynamicSqlSource中吧庸娱,繼續(xù)看DynamicSqlSource中下面的代碼:

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);
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }

rootSqlNode.apply(context)之后,還會(huì)有一個(gè)SqlSourceBuilder這個(gè)類也有一個(gè)parse方法(在這個(gè)地方不得不說下即使Mybatis現(xiàn)在是最流行的ORM框架之一但是它的設(shè)計(jì)上確實(shí)不怎么樣谐算,現(xiàn)在隨便來一個(gè)類都有一個(gè)parse方法熟尉,為什么不直接抽象出一個(gè)接口來),這個(gè)parse方法就是處理#的地方了洲脂。下面我們來看看:

public SqlSource parse(String originalSql, Class<?> parameterType) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

  private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {

    private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
    private Class<?> parameterType;

    public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType) {
      super(configuration);
      this.parameterType = parameterType;
    }

    public List<ParameterMapping> getParameterMappings() {
      return parameterMappings;
    }

    public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }

    private ParameterMapping buildParameterMapping(String content) {
      Map<String, String> propertiesMap = parseParameterMapping(content);
      String property = propertiesMap.get("property");
      String jdbcType = propertiesMap.get("jdbcType");
      Class<?> propertyType;
      MetaClass metaClass = MetaClass.forClass(parameterType);
      if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
        propertyType = parameterType;
      } else if (JdbcType.CURSOR.name().equals(jdbcType)) {
        propertyType = java.sql.ResultSet.class;
      } else if (metaClass.hasGetter(property)) {
        propertyType = metaClass.getGetterType(property);
      } else {
        propertyType = Object.class;
      }
      ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
      if (jdbcType != null) {
        builder.jdbcType(resolveJdbcType(jdbcType));
      }
      Class<?> javaType = null;
      String typeHandlerAlias = null;
      for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
        String name = entry.getKey();
        String value = entry.getValue();
        if ("javaType".equals(name)) {
          javaType = resolveClass(value);
          builder.javaType(javaType);
        } else if ("jdbcType".equals(name)) {
          builder.jdbcType(resolveJdbcType(value));
        } else if ("mode".equals(name)) {
          builder.mode(resolveParameterMode(value));
        } else if ("numericScale".equals(name)) {
          builder.numericScale(Integer.valueOf(value));
        } else if ("resultMap".equals(name)) {
          builder.resultMapId(value);
        } else if ("typeHandler".equals(name)) {
          typeHandlerAlias = value;
        } else if ("jdbcTypeName".equals(name)) {
          builder.jdbcTypeName(value);
        }
      }
      if (typeHandlerAlias != null) {
        builder.typeHandler((TypeHandler<?>) resolveTypeHandler(javaType, typeHandlerAlias));
      }
      return builder.build();
    }

老規(guī)矩先上代碼再分析斤儿,上面是SqlSourceBuilderapply方法以及用到的TokenHandle的內(nèi)部實(shí)現(xiàn)類ParameterMappingTokenHandle
先看最簡(jiǎn)單的產(chǎn)生#$區(qū)別的地方腮考,ParameterMappingTokenHandlehandleToken方法中不管你傳過來的是什么都是直接返回一個(gè)占位符?雇毫。

接下來要介紹#$的第二個(gè)區(qū)別了##:看上面ParameterMappingTokenHandle類的parseParameterMapping方法我們可以發(fā)現(xiàn)在#{}中可以寫一些其它的東西,比如javaType踩蔚、jdbcType棚放、typeHandler等,所以我們可以寫出類似這種的sql:#{id,javaType=String,jdbcType=VARCHAR,typeHandler=cn.fay.mybatis.MyStringTypeHandler}馅闽,我們可以在sql中指定變量的類型以及設(shè)置這個(gè)變量時(shí)對(duì)應(yīng)所需要用到的TypeHandler當(dāng)然如果你用到了自定義的TypeHandler的話飘蚯,你要在mybatis的配置中聲明一下,如下:

<typeHandlers>
        <typeHandler handler="cn.fay.mybatis.MyStringTypeHandler" javaType="String" jdbcType="VARCHAR"/>
    </typeHandlers>

這里需要注意的是mybatis的配置文件中對(duì)typeAliases福也、typeHandlers局骤、plugingsmappers等元素的順序是有要求的不能亂暴凑,如果你在使用中遇到了問題解決不了峦甩,可以過來問我。
至此现喳,#$的區(qū)別應(yīng)該說得差不多了凯傲,有問題可以來溝通。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末嗦篱,一起剝皮案震驚了整個(gè)濱河市冰单,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌灸促,老刑警劉巖诫欠,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涵卵,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡荒叼,警方通過查閱死者的電腦和手機(jī)轿偎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來甩挫,“玉大人贴硫,你說我怎么就攤上這事∫琳撸” “怎么了英遭?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)亦渗。 經(jīng)常有香客問我挖诸,道長(zhǎng),這世上最難降的妖魔是什么法精? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任多律,我火速辦了婚禮,結(jié)果婚禮上搂蜓,老公的妹妹穿的比我還像新娘狼荞。我一直安慰自己,他們只是感情好帮碰,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布相味。 她就那樣靜靜地躺著,像睡著了一般殉挽。 火紅的嫁衣襯著肌膚如雪丰涉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天斯碌,我揣著相機(jī)與錄音一死,去河邊找鬼。 笑死傻唾,一個(gè)胖子當(dāng)著我的面吹牛投慈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播冠骄,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼伪煤,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了猴抹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤锁荔,失蹤者是張志新(化名)和其女友劉穎蟀给,沒想到半個(gè)月后蝙砌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡跋理,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年择克,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片前普。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡肚邢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拭卿,到底是詐尸還是另有隱情骡湖,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布峻厚,位于F島的核電站响蕴,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏惠桃。R本人自食惡果不足惜浦夷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望辜王。 院中可真熱鬧劈狐,春花似錦、人聲如沸呐馆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)摹恰。三九已至辫继,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間俗慈,已是汗流浹背姑宽。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留闺阱,地道東北人炮车。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像酣溃,于是被迫代替她去往敵國(guó)和親瘦穆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容