1. 程式人生 > 其它 >Gradle Kotlin DSL 多模組專案案例

Gradle Kotlin DSL 多模組專案案例

### mybatis中#{}與${}的區別詳解 #### 版本 **此處分析基於mybatis-3.4.6完成。** #### 介紹-猜想 網上的很多資料都表示,#{}表示式寫入引數時將表示式替換為?,而${}表示式寫入引數時是直接寫入。本來以為#{}利用的是jdbc中PreparedStatement的方式,而${}是直接使用Statement,其實不然。開發同學都知道PreparedStatement其預編譯的特性可以在操作大量SQL時有顯著的效能提升,並且可以防止SQL注入安全問題。Statement相比更加簡單,少了預編譯和SQL注入安全防範,因此在少量的SQL執行上,其效能要更高,只是這點效能一般來說忽略不計了,因此開發中往往都是隻會使用#{}。實際官網上也表明了,#{}使用PreparedStatement操作,但是沒明確說${}使用的Statement,只是表示${}表示式中的引數會被直接替換,下圖為截選自mybatis官網。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20190930160933829.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3R1ZmVuZzE5OTI=,size_16,color_FFFFFF,t_70) ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20190930160959822.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3R1ZmVuZzE5OTI=,size_16,color_FFFFFF,t_70) #### 驗證-原始碼分析 回到主題,猜想了#{}與${}的區別後,現在開始從原始碼方面驗證一下。 ##### 更新流程分析階段 要了解#{}與${}的區別需要知道mybatis的初始化與SQL的執行階段邏輯,下面會簡單介紹一下。 mybatis的初始化入口如下: ```java SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); ``` 下面是mybatis的更新執行流程圖: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20190930160610388.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3R1ZmVuZzE5OTI=,size_16,color_FFFFFF,t_70) 可以從流程圖中發現,mybatis的更新操作在Executor#doUpdate()處執行。 ```java public int doUpdate(MappedStatement ms, Object parameter) throws SQLException { Statement stmt = null; int var6; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, (ResultHandler)null, (BoundSql)null); stmt = this.prepareStatement(handler, ms.getStatementLog()); var6 = handler.update(stmt); } finally { this.closeStatement(stmt); } return var6; } ``` 跟蹤原始碼可以發現,在Executor執行操作時,SQL已經被初始化了(#{}表示式標識的引數已經被替換為了?),而往上跟蹤可以發現初始化操作在SQLSessionFactory初始化階段,那麼回到初始化階段。 ##### mybatis初始化階段 ```java XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); var5 = this.build(parser.parse()); ``` 在初始化時,mybatis會parse我們的配置檔案和mapper檔案。XmlConfigBuilder#mapperElement()做對映構建、XMLMapperBuilder#parse()解析mapper、#configurationElement()配置mapper元素、#buildStatementFromContext()配置select|insert|update|delete語句、XmlStatementBuilder#parseStatementNode()語句構建,直到開始解析SQL語句。 ```java //解析語句(select|insert|update|delete) // public void parseStatementNode() { //...忽略一系列邏輯 //官網可以查到,這裡的langDriver預設為:XMLLanguageDriver。 LanguageDriver langDriver = getLanguageDriver(lang); //注意,此處為顯示指定PreParedStatement、Statement或者是CallableStatement的地方,預設情況下為PreparedStatement。 StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); //Mapper中的SQL對映初始化,#{}表示式被替換為?,${}表示式不變化 // Parse the SQL (pre: and were parsed and removed) SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); } ``` 那麼接下來進入到XmlLanguageDriver中,XMLScriptBuilder#parseScriptNode(),通過呼叫parseDynamicTags() 來根據當前xml的tag拿到childTag(也就是select中包含的SQL語句),通過isDynamic方法來判斷。跟蹤到isDynamic方法中可以看到,new了一個GenericTokenParser預設以${}解析方式,而判斷在parse方法中,以String.indexOf判斷拿到的SQL語句中是否存在${,最後確認是否為DynamicSqlSource(#{})或者RawSqlSource(${}),而如果是RawSQLSource,則在初始化中通過SqlSourceBuilder#parse()中,將SQL引數替換為了?。 ```java //isDynamic的判斷 public boolean isDynamic() { DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser(); GenericTokenParser parser = createParser(checker); parser.parse(text); return checker.isDynamic(); } ``` ```java //非isDynamic情況下,#{}解析。 public SqlSource parse(String originalSql, Class parameterType, Map additionalParameters) { ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); String sql = parser.parse(originalSql); return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); } ``` ##### 更新操作執行階段 在SQLSource初始化完畢後,#{}的方法會替換為"?",而${}的方式SQL語句不變,在後面具體執行時才會動態設定引數。這個結果可以在SQLSessionFactory.configuration.mappedStatements中看到。 再次來到Executor#doUpdate()#prepareStatement(),在這裡#{}設定引數的核心方法入口為DefaultParameterHandler -> setParameters,其實就是原生jdbc中PreparedStatement的setParameter。 ```java /** * BaseTypeHandler的抽象方法,其中在此處可以看到,mybatis對不同的型別封裝了不同的typeHandler來做。 */ public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException { //...省略部分邏輯 try { setNonNullParameter(ps, i, parameter, jdbcType); } catch (Exception e) { throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . " + "Try setting a different JdbcType for this parameter or a different configuration property. " + "Cause: " + e, e); } } } ``` 而關於${}設定引數,在parameterize()會發現其已經設定完成了,那麼回到構建doUpdate()方法中。在這裡newStatementHandler()完成的statementHandler的構建與SQL引數的注入。 ```java public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { //可以看到其new了一個RoutingStatementHandler StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } ``` RoutingStatementHandler的構造: ```java public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { //這裡的statementType為Prepared,這個在SQLSessionFactory中完成初始化 switch (ms.getStatementType()) { case STATEMENT: delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case PREPARED: delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case CALLABLE: delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; default: throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); } } ``` 往下跟蹤會發現其呼叫super的構造器,因此來到BaseStatementHandler: ```java protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { this.configuration = mappedStatement.getConfiguration(); this.executor = executor; this.mappedStatement = mappedStatement; this.rowBounds = rowBounds; this.typeHandlerRegistry = configuration.getTypeHandlerRegistry(); this.objectFactory = configuration.getObjectFactory(); //前面在doUpdate中,boundSQL傳入的為null,因此進入getBoundSql中 if (boundSql == null) { // issue #435, get the key before calculating the statement generateKeys(parameterObject); boundSql = mappedStatement.getBoundSql(parameterObject); } this.boundSql = boundSql; this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql); this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql); } ``` mappedStatement.getBoundSql程式碼如下: ```java public BoundSql getBoundSql(Object parameterObject) { //SQL的初始化邏輯入口,這裡的SQLSource為DynamicSqlSource,這是在mybatis初始化時構建的,回想一下isDynamic就明白了。 BoundSql boundSql = sqlSource.getBoundSql(parameterObject); //...忽略其他邏輯 return boundSql; } ``` DynamicSqlSource#getBoundSql程式碼如下: ```java 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); for (Map.Entry entry : context.getBindings().entrySet()) { boundSql.setAdditionalParameter(entry.getKey(), entry.getValue()); } return boundSql; } ``` #### 結論 自此,#{}與${}的分析完畢,可以發現#{}與${}的具體操作都是通過PreparedStatement來執行的,只是#{}與${}的引數注入上,一個是動態注入,一個是靜態注入。具體Statement的型別由開發者手動配置,預設情況下為PreparedStatement。**但是要注意,如果使用#{}表示式是不能配置statementType為:Statement的,這裡個prepareStatement()方法中就能體現出來,最終結果會去執行一個帶有?的SQL語句導致語法錯誤。**