1. 程式人生 > >mybatis學習之路----#{}, ${}兩種傳引數方式的區別--附原始碼解讀

mybatis學習之路----#{}, ${}兩種傳引數方式的區別--附原始碼解讀

點滴記載,點滴進步,願自己更上一層樓。

首先下個結論,

${} 會將傳入的引數完全拼接到sql語句中,也就是相當於一個拼接符號。

也就是,最後的處理方式就相當於 

String sql = select * from user where id=${value}....

mybatis會將 ${value} 完全替換為引數 value 的值  相當於replace("${value}", value)的過程。

實際上mybatis 是先將sql轉成char陣列

然後擷取 "${"前頭的部分放入到容器,替換  以"${"開頭 以 "}"結尾的內容。所以說它的作用相當於拼接符號。拼接後直接作為sql語句的一部分,所以如果引數是可執行程式碼,sql是會直接執行的。這就是為什麼它會導致sql注入。

        #{} 是一個佔位符, mybatis最後會將這個佔位符,替換成?,

最後才進行prepareStatement的相應位置的?的替換,也就是  state.setString(序號,值),setInt(序號,值)....

熟悉jdbc的人對這段應該不陌生。

下面來進行原始碼級別的論證。

首先寫一個帶有${} #{} 兩種引數方式的sql

    <!--模糊查詢 demo2 直接用concat拼字串 此種可以有效防止sql注入 -->
    <select id="findUserByName4" parameterType="com.soft.mybatis.model.User" resultMap="userMap">
        select * from t_user where username like concat('%', #{username} ,'%') and password=#{password}
    </select>
介面
    /**
     * 驗證${} #{} 兩種傳參處理方式
     * @return
     */
    List<User> findUserByName04(User user);
實現
   /**
     * 模糊查詢第三種方式  直接接收拼好的字串
     * @return
     */
    public List<User> findUserByName04(User user) {
        String statementId = "test.findUserByName4";
        return findUserList(statementId,user);
    }

    /**
     * 由於都需要三種方式查詢除了兩個地方不一樣其他的處理都相同,此處抽取相同部分
     * @param statementId
     * @param param
     * @return
     */
    private List<User> findUserList(String statementId, Object param){
        SqlSession sqlSession = null;
        try {
            sqlSession = SqlsessionUtil.getSqlSession();
            return sqlSession.selectList(statementId,param);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            SqlsessionUtil.closeSession(sqlSession);
        }
        return new ArrayList<User>();
    }
測試程式碼:
    @Test
    public void findUserByName04() throws Exception {
        User user = new User();
        user.setUsername("小黃");
        user.setPassword("123456");
        List<User> users = dao.findUserByName04(user);
        for(User userss:users){
            System.out.println("findUserByName04:" + userss);
        }
    }

首先說明 mybatis處理 "${}" 和 "#{}" 的主要處理邏輯的原始碼位於

GenericTokenParser.java 的parse方法中。

原始碼奉上:

    public String parse(String text) {
        StringBuilder builder = new StringBuilder();
        if(text != null && text.length() > 0) {
            char[] src = text.toCharArray();
            int offset = 0;

            for(int start = text.indexOf(this.openToken, offset); start > -1; start = text.indexOf(this.openToken, offset)) {
                if(start > 0 && src[start - 1] == 92) {
                    builder.append(src, offset, start - offset - 1).append(this.openToken);
                    offset = start + this.openToken.length();
                } else {
                    int end = text.indexOf(this.closeToken, start);
                    if(end == -1) {
                        builder.append(src, offset, src.length - offset);
                        offset = src.length;
                    } else {
                        builder.append(src, offset, start - offset);
                        offset = start + this.openToken.length();
                        String content = new String(src, offset, end - offset);
                        builder.append(this.handler.handleToken(content));
                        offset = end + this.closeToken.length();
                    }
                }
            }

            if(offset < src.length) {
                builder.append(src, offset, src.length - offset);
            }
        }

        return builder.toString();
    }

mybatis對這兩種引數的處理分為兩個階段,

首先為構建sqlsessionFactory的時候,這個時候的處理為,如果sql中含有${}則該條sql不做處理,如果sql中全是#{}則替換為 ? 。

然後是sqlSession執行sql階段,該階段首先會將${value} 原封不動的替換為 value傳過來的值,然後在將sql中的#{} 替換為 ? 

最後才是preparestatement 將sql中的?替換為引數值,最後執行sql。

大概的處理邏輯就是這個,下面是原始碼跟蹤與說明。讓人更明瞭。

首先第一階段,構建sqlsessionFactory階段。

入口    SqlSessionFactoryBuilder  的 方法

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {

 var5 = this.build(e.parse());  

然後會進入 XMLConfigBuilder 的

    private void parseConfiguration(XNode root) {
        try {
            this.propertiesElement(root.evalNode("properties"));
            this.typeAliasesElement(root.evalNode("typeAliases"));
            this.pluginElement(root.evalNode("plugins"));
            this.objectFactoryElement(root.evalNode("objectFactory"));
            this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            this.settingsElement(root.evalNode("settings"));
            this.environmentsElement(root.evalNode("environments"));
            this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            this.typeHandlerElement(root.evalNode("typeHandlers"));
            this.mapperElement(root.evalNode("mappers"));
        } catch (Exception var3) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
        }
    }
這個方法處理各種節點,看到this.mapperElement(root.evalNode("mappers"));是不是很熟悉,這裡就是處理mapper檔案的地方,

繼續深入會到XMLMapperBuilder的

    private void configurationElement(XNode context) {
        try {
            String e = context.getStringAttribute("namespace");
            if(e.equals("")) {
                throw new BuilderException("Mapper\'s namespace cannot be empty");
            } else {
                this.builderAssistant.setCurrentNamespace(e);
                this.cacheRefElement(context.evalNode("cache-ref"));
                this.cacheElement(context.evalNode("cache"));
                this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));
                this.resultMapElements(context.evalNodes("/mapper/resultMap"));
                this.sqlElement(context.evalNodes("/mapper/sql"));
                this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
            }
        } catch (Exception var3) {
            throw new BuilderException("Error parsing Mapper XML. Cause: " + var3, var3);
        }
    }
看到  this.buildStatementFromContext(context.evalNodes("select|insert|update|delete")); 又眼熟,這裡就是處理sql的入口。

繼續深入會進入到 XMLStatementBuilder的

    public void parseStatementNode() {
        String id = this.context.getStringAttribute("id");
        String databaseId = this.context.getStringAttribute("databaseId");
        if(this.databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
            Integer fetchSize = this.context.getIntAttribute("fetchSize");
            Integer timeout = this.context.getIntAttribute("timeout");
            String parameterMap = this.context.getStringAttribute("parameterMap");
            String parameterType = this.context.getStringAttribute("parameterType");
            Class parameterTypeClass = this.resolveClass(parameterType);
            String resultMap = this.context.getStringAttribute("resultMap");
            String resultType = this.context.getStringAttribute("resultType");
            String lang = this.context.getStringAttribute("lang");
            LanguageDriver langDriver = this.getLanguageDriver(lang);
            Class resultTypeClass = this.resolveClass(resultType);
            String resultSetType = this.context.getStringAttribute("resultSetType");
            StatementType statementType = StatementType.valueOf(this.context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
            ResultSetType resultSetTypeEnum = this.resolveResultSetType(resultSetType);
            String nodeName = this.context.getNode().getNodeName();
            SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
            boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
            boolean flushCache = this.context.getBooleanAttribute("flushCache", Boolean.valueOf(!isSelect)).booleanValue();
            boolean useCache = this.context.getBooleanAttribute("useCache", Boolean.valueOf(isSelect)).booleanValue();
            boolean resultOrdered = this.context.getBooleanAttribute("resultOrdered", Boolean.valueOf(false)).booleanValue();
            XMLIncludeTransformer includeParser = new XMLIncludeTransformer(this.configuration, this.builderAssistant);
            includeParser.applyIncludes(this.context.getNode());
            this.processSelectKeyNodes(id, parameterTypeClass, langDriver);
            SqlSource sqlSource = langDriver.createSqlSource(this.configuration, this.context, parameterTypeClass);
            String resultSets = this.context.getStringAttribute("resultSets");
            String keyProperty = this.context.getStringAttribute("keyProperty");
            String keyColumn = this.context.getStringAttribute("keyColumn");
            String keyStatementId = id + "!selectKey";
            keyStatementId = this.builderAssistant.applyCurrentNamespace(keyStatementId, true);
            Object keyGenerator;
            if(this.configuration.hasKeyGenerator(keyStatementId)) {
                keyGenerator = this.configuration.getKeyGenerator(keyStatementId);
            } else {
                keyGenerator = this.context.getBooleanAttribute("useGeneratedKeys", Boolean.valueOf(this.configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))).booleanValue()?new Jdbc3KeyGenerator():new NoKeyGenerator();
            }

            this.builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, (KeyGenerator)keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
        }
    }
一看一大段,都是幹啥的呀,其實大眼一掃其實也不嚇人,就是在處理一些引數型別,statementId等等。我們把眼光放到
SqlSource sqlSource = langDriver.createSqlSource(this.configuration, this.context, parameterTypeClass);

繼續深入,XMLScriptBuilder

   private List<SqlNode> parseDynamicTags(XNode node) {
        ArrayList contents = new ArrayList();
        NodeList children = node.getNode().getChildNodes();

        for(int i = 0; i < children.getLength(); ++i) {
            XNode child = node.newXNode(children.item(i));
            String nodeName;
            if(child.getNode().getNodeType() != 4 && child.getNode().getNodeType() != 3) {
                if(child.getNode().getNodeType() == 1) {
                    nodeName = child.getNode().getNodeName();
                    XMLScriptBuilder.NodeHandler var8 = (XMLScriptBuilder.NodeHandler)this.nodeHandlers.get(nodeName);
                    if(var8 == null) {
                        throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
                    }

                    var8.handleNode(child, contents);
                    this.isDynamic = true;
                }
            } else {
                nodeName = child.getStringBody("");
                TextSqlNode handler = new TextSqlNode(nodeName);
                if(handler.isDynamic()) {
                    contents.add(handler);
                    this.isDynamic = true;
                } else {
                    contents.add(new StaticTextSqlNode(nodeName));
                }
            }
        }

        return contents;
    }
到此很接近目標了,通過 if(handler.isDynamic()) { 進入到 TextSqlNode 的
    public boolean isDynamic() {
        TextSqlNode.DynamicCheckerTokenParser checker = new TextSqlNode.DynamicCheckerTokenParser();
        GenericTokenParser parser = this.createParser(checker);
        parser.parse(this.text);
        return checker.isDynamic();
    }
它的createParser
    private GenericTokenParser createParser(TokenHandler handler) {
        return new GenericTokenParser("${", "}", handler);
    }
原來他首先校驗的是${}  

然後 parser.parse(this.text); 就到了我門的目的地了。
終於到了最上面的GenericTokenParser 的解析部分了。

可以看到,

1 首先弄了個容器,Stringbuilder用來裝替換後的sql。

2 然後是判斷sql非空等等。然後將sql轉成 char[] src 陣列

3 開始迴圈,拿到 this.openToken 的位置,這裡估計會問,this.openToken是什麼鬼,通過debug發現這個就是 “${”.也就是如果sql語句裡面包含有${} 的話 會進入迴圈

4 來看看迴圈體幹了什麼,

if(start > 0 && src[start - 1] == 92) { 首先判斷  this.openToken開始位置 然後看其前以為是不是 "\"   92 對應的char就是反斜槓。顯然不進,

int end = text.indexOf(this.closeToken, start);

if(end == -1) { 然後拿到 this.closeToken 的位置  顯然也不會進這個,那就只能進另外的else了。

// 將 ${ 前面的字串放入到builder
builder.append(src, offset, start - offset);

// 記錄this.openToken的位置座標
offset = start + this.openToken.length();

// 拿到this.openTokenthis.closeToken中間的字串
String content = new String(src, offset, end - offset);

// 替換該字串
builder.append(this.handler.handleToken(content));

// 記錄this.closeToken 所在位置
offset = end + this.closeToken.length();

如果還有${}內容,繼續迴圈,

注意上面程式碼中的this.openToken 可能為${ 也可能為 #{

走完了這步之後如果發現sql中確實有${},就將isDynamic標識為true,這個標識很有用,具體可以看XMLScriptBuilder

    public SqlSource parseScriptNode() {
        List contents = this.parseDynamicTags(this.context);
        MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
        Object sqlSource = null;
        if(this.isDynamic) {
            sqlSource = new DynamicSqlSource(this.configuration, rootSqlNode);
        } else {
            sqlSource = new RawSqlSource(this.configuration, rootSqlNode, this.parameterType);
        }

        return (SqlSource)sqlSource;
    }
該標識導致兩種結果,如果為true就直接返回了一個DynamicSqlSource例項,它的建構函式僅僅做了簡單的事情
    public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
        this.configuration = configuration;
        this.rootSqlNode = rootSqlNode;
    }
但是如果sql中沒有${}的話就會返回,RawSqlSource的例項看它的建構函式
    public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class clazz = parameterType == null?Object.class:parameterType;
        this.sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap());
    }
它會呼叫 SqlSourceBuilder 的parse方法,最後又會進入GenericTokenParser的parse方法 進行#{}的替換工作,具體自己debug吧,不然沒完了。
    public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
        SqlSourceBuilder.ParameterMappingTokenHandler handler = new SqlSourceBuilder.ParameterMappingTokenHandler(this.configuration, parameterType, additionalParameters);
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        String sql = parser.parse(originalSql);
        return new StaticSqlSource(this.configuration, sql, handler.getParameterMappings());
    }
上面的過程僅僅是

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resource);的執行過程。

sqlsession的執行sql過程,還要進入到那個parse方法中進行替換操作,

這個時候要分為兩種情況,如果構建SqlSessionFactory 的時候,

SqlSource 的例項為DynamicSqlSource 的話,因為sql還是xml中的形態,所以它會做兩件事,

第一件就是將${}替換為對應的value值,

第二件事就是將#{}替換為?

也就是 DynamicSqlSource 的

    public BoundSql getBoundSql(Object parameterObject) {
        DynamicContext context = new DynamicContext(this.configuration, parameterObject);
        this.rootSqlNode.apply(context);//第一次替換${}
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(this.configuration);
        Class parameterType = parameterObject == null?Object.class:parameterObject.getClass();
        SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());// 替換#{}->?
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        Iterator i$ = context.getBindings().entrySet().iterator();

        while(i$.hasNext()) {
            Entry entry = (Entry)i$.next();
            boundSql.setAdditionalParameter((String)entry.getKey(), entry.getValue());
        }

        return boundSql;
    }


最後sql的樣子就是
select * from t_user where username like '%黃%' and password=?

最後才是statement執行sql,返回查詢結果。注意上面的替換後的sql,並不是將${}替換為?,而是替換為傳過來的引數值。

本來想簡單弄弄算了,結果弄得這麼繁瑣,都有點mybatis原始碼解讀的意思了。

上面對#{} ${}的區別,在原始碼層次做了講解,希望有所幫助,反正我現在是印象深刻了。

-_-       -_-        -_-       -_-       -_-    -_-         -_-                    -_-