mybatis中#{}與${}的區別詳解
版本
此處分析基於mybatis-3.4.6完成。
介紹-猜想
網上的很多資料都表示,#{}表達式寫入參數時將表達式替換爲?,而${}表達式寫入參數時是直接寫入。本來以爲#{}利用的是jdbc中PreparedStatement的方式,而${}是直接使用Statement,其實不然。開發同學都知道PreparedStatement其預編譯的特性可以在操作大量SQL時有顯著的性能提升,並且可以防止SQL注入安全問題。Statement相比更加簡單,少了預編譯和SQL注入安全防範,因此在少量的SQL執行上,其性能要更高,只是這點性能一般來說忽略不計了,因此開發中往往都是隻會使用#{}。實際官網上也表明了,#{}使用PreparedStatement操作,但是沒明確說${}使用的Statement,只是表示${}表達式中的參數會被直接替換,下圖爲截選自mybatis官網。
驗證-源碼分析
回到主題,猜想了#{}與${}的區別後,現在開始從源碼方面驗證一下。
更新流程分析階段
要了解#{}與${}的區別需要知道mybatis的初始化與SQL的執行階段邏輯,下面會簡單介紹一下。
mybatis的初始化入口如下:
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
下面是mybatis的更新執行流程圖:
可以從流程圖中發現,mybatis的更新操作在Executor#doUpdate()處執行。
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初始化階段
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語句。
//解析語句(select|insert|update|delete)
//<select
// id="selectPerson"
// parameterType="int"
// parameterMap="deprecated"
// resultType="hashmap"
// resultMap="personResultMap"
// flushCache="false"
// useCache="true"
// timeout="10000"
// fetchSize="256"
// statementType="PREPARED"
// resultSetType="FORWARD_ONLY">
// SELECT * FROM PERSON WHERE ID = #{id}
//</select>
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: <selectKey> and <include> 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參數替換爲了?。
//isDynamic的判斷
public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
GenericTokenParser parser = createParser(checker);
parser.parse(text);
return checker.isDynamic();
}
//非isDynamic情況下,#{}解析。
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> 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。
/**
* 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參數的注入。
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的構造:
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:
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代碼如下:
public BoundSql getBoundSql(Object parameterObject) {
//SQL的初始化邏輯入口,這裏的SQLSource爲DynamicSqlSource,這是在mybatis初始化時構建的,回想一下isDynamic就明白了。
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
//...忽略其他邏輯
return boundSql;
}
DynamicSqlSource#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, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
結論
自此,#{}與${}的分析完畢,可以發現#{}與${}的具體操作都是通過PreparedStatement來執行的,只是#{}與${}的參數注入上,一個是動態注入,一個是靜態注入。具體Statement的類型由開發者手動配置,默認情況下爲PreparedStatement。但是要注意,如果使用#{}表達式是不能配置statementType爲:Statement的,這裏個prepareStatement()方法中就能體現出來,最終結果會去執行一個帶有?的SQL語句導致語法錯誤。