Gradle Kotlin DSL 多模組專案案例
阿新 • • 發佈:2022-02-23
### 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語句導致語法錯誤。**