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语句导致语法错误。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章