mybatis中#{}与${}的区别详解
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语句导致语法错误。