MyBatis 学习笔记(八)---源码分析篇--SQL 执行过程详细分析

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 在面试中我们经常会被到MyBatis中 #{} 占位符与${}占位符的区别。大多数的小伙伴都可以脱口而出#{} 会对值进行转义,防止SQL注入。而${}则会原样输出传入值,不会对传入值做任何处理。本文将通过源码层面分析为啥#{} 可以防止SQL注入。

前言

在面试中我们经常会被到MyBatis中 #{} 占位符与${}占位符的区别。大多数的小伙伴都可以脱口而出#{} 会对值进行转义,防止SQL注入。而${}则会原样输出传入值,不会对传入值做任何处理。本文将通过源码层面分析为啥#{} 可以防止SQL注入。

源码解析

首先我们来看看MyBatis 中SQL的解析过程,MyBatis 会将映射文件中的SQL拆分成一个个SQL分片段,然后在将这些分片段拼接起来。

例如:在映射文件中有如下SQL

SELECT * FROM student

<where>
            <if test="id!=null">
                id=${id}
            </if>
            <if test="name!=null">
                AND name =${name}
            </if>
        </where>

MyBatis 会将该SQL 拆分成如下几部分进行解析

第一部分 SELECT * FROM Author 由StaticTextSqlNode存储

第二部分 <where> 由WhereSqlNode 存储

第三部分 <if></if> 由IfSqlNode存储

第四部分 ${id} ${name} 占位符里的文本由TextSqlNode存储。


获取BoundSql

BoundSql 是用来存储一个完整的SQL 语句,存储参数映射列表以及运行时参数

public class BoundSql {
  /**
   * 一个完整的SQL语句,可能会包含问号?占位符
   */
  private String sql;
  /**
   * 参数映射列表,SQL中的每个#{xxx}
   * 占位符都会被解析成相应的ParameterMapping对象
   */
  private List<ParameterMapping> parameterMappings;
  /**
   * 运行时参数,即用户传入的参数,比如Article对象,
   * 或是其他的参数
   */
  private Object parameterObject;
    /**
   * 附加参数集合,用户存储一些额外的信息,比如databaseId等
   */
  private Map<String, Object> additionalParameters;
  /**
   * additionalParameters的元信息对象
   */
  private MetaObject metaParameters;
    .... 省略部分代码
  }

分析SQL的解析,首先从获取BoundSql说起。其代码源头在MappedStatement。

public BoundSql getBoundSql(Object parameterObject) {
  //其实就是调用sqlSource.getBoundSql
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    //剩下的可以暂时忽略,故省略代码
    return boundSql;
  }

如上,可以看出其内部就是调用的sqlSource.getBoundSql。 而我们sqlSource 接口又有如下几个实现类。

DynamicSqlSource

RawSqlSource

StaticSqlSource

ProviderSqlSource

VelocitySqlSource

其中DynamicSqlSource 是对动态SQL进行解析,当SQL配置中包含${}或者<if>,<set> 等标签时,会被认定为是动态SQL,此时使用 DynamicSqlSource 存储 SQL 片段,而RawSqlSource 是对原始的SQL 进行解析,而StaticSqlSource 是对静态SQL进行解析。这里我们重点介绍下DynamicSqlSource。话不多说,直接看源码。

public BoundSql getBoundSql(Object parameterObject) {
    //生成一个动态上下文
    DynamicContext context = new DynamicContext(configuration, parameterObject);
  //这里SqlNode.apply只是将${}这种参数替换掉,并没有替换#{}这种参数
    rootSqlNode.apply(context);
  //调用SqlSourceBuilder
    SqlSourceBuilder sqlSourceParser =  new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
  //SqlSourceBuilder.parse,注意这里返回的是StaticSqlSource,解析完了就把那些参数都替换成?了,也就是最基本的JDBC的SQL写法
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
  //看似是又去递归调用SqlSource.getBoundSql,其实因为是StaticSqlSource,所以没问题,不是递归调用
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
//  将DynamicContext的ContextMap中的内容拷贝到BoundSql中
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }

如上,该方法主要有如下几个过程:

1.生成一个动态上下文

2.解析SQL片段,替换${}类型的参数

3.解析SQL语句,并将参数都替换成?

4.调用StaticSqlSource的getBoundSql获取BoundSql

5.将DynamicContext的ContextMap中的内容拷贝到BoundSql中。

下面通过两个单元测试用例理解下。

@Test
  public void shouldMapNullStringsToNotEmptyStrings() {
    final String expected = "id=${id}";
    final MixedSqlNode sqlNode = mixedContents(new TextSqlNode(expected));
    final DynamicSqlSource source = new DynamicSqlSource(new Configuration(), sqlNode);
    String sql = source.getBoundSql(new Bean("12")).getSql();
    Assert.assertEquals("id=12", sql);
  }
    @Test
  public void shouldMapNullStringsToJINHAOEmptyStrings() {
    final String expected = "id=#{id}";
    final MixedSqlNode sqlNode = mixedContents(new TextSqlNode(expected));
    final DynamicSqlSource source = new DynamicSqlSource(new Configuration(), sqlNode);
    String sql = source.getBoundSql(new Bean("12")).getSql();
    Assert.assertEquals("id=?", sql);
  }

如上,${} 占位符经过DynamicSqlSource的getBoundSql 方法之后直接替换成立用户传入值,而#{} 占位符则仅仅只是只会被替换成?号,不会被设值。


DynamicContext

DynamicContext 是SQL语句的上下文,每个SQL片段解析完成之后会存入DynamicContext中。让我们来看看DynamicContext的相关代码。

public DynamicContext(Configuration configuration, Object parameterObject) {
  //绝大多数调用的地方parameterObject为null
    if (parameterObject != null && !(parameterObject instanceof Map)) {
      //如果不是map型
      MetaObject metaObject = configuration.newMetaObject(parameterObject);
      bindings = new ContextMap(metaObject);
    } else {
      bindings = new ContextMap(null);
    }
  //存储额外信息,如databaseId
    bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
  }

如上,在DynamicContext的构造函数中,根据传入的参数对象是否是Map类型,有两个不同构造ContextMap的方式,而ContextMap作为一个继承了HashMap的对象,作用就是用于统一参数的访问方式:用Map接口方法来访问数据。具体磊说,当传入的参数对象不是Map类型时,MyBatis会将传入的POJO对象用MetaObject 对象来封装,当动态计算sql过程需要获取数据时,用Map 接口的get方法包装 MetaObject对象的取值过程。

static class ContextMap extends HashMap<String, Object> {
    private MetaObject parameterMetaObject;
    public ContextMap(MetaObject parameterMetaObject) {
      this.parameterMetaObject = parameterMetaObject;
    }
    @Override
    public Object get(Object key) {
      String strKey = (String) key;
      //先去map里找
      if (super.containsKey(strKey)) {
        return super.get(strKey);
      }
      //如果没找到,再用ognl表达式去取值
      //如person[0].birthdate.year
      if (parameterMetaObject != null) {
        return parameterMetaObject.getValue(strKey);
      }
      return null;
    }
  }

DynamicContext 的解析到此完成。


解析SQL片段

正如前面所说,一个包含了${}, <if>,<where>等标签的SQL 会被分成很多SQL片段。由SqlNode 的子类进行存储。

263e0b00ccd54aa59b2110a469327908_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ1MzQ4MDg=,size_16,color_FFFFFF,t_70.png

如图所示,

1.StaticTextSqlNode 用于存储静态文本

2.TextSqlNode用于存储带有${}占位符的文本

3.ifSqlNode则用于存储<if>节点的内容

4.WhereSqlNode 用于增加WHERE 前缀,然后替换掉AND 和OR 等前缀。

5.MixedSqlNode内部维护了一个SqlNode集合,用于存储各种

各样的SqlNode。

首先我们来看看,MixedSqlNode 的很合SQL节点。

public class MixedSqlNode implements SqlNode {
  //组合模式,拥有一个SqlNode的List
  private List<SqlNode> contents;
  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }
  @Override
  public boolean apply(DynamicContext context) {
    //依次调用list里每个元素的apply
    for (SqlNode sqlNode : contents) {
      sqlNode.apply(context);
    }
    return true;
  }
}

如上,从构造函数中可以看出,MixedSqlNode 拥有一个SqlNode的集合。这里利用了组合模式。在此处我重点介绍下TextSqlNode 文本SQL节点 和IfSqlNode if SQL节点。

//***TextSqlNode
  public boolean apply(DynamicContext context) {
//    创建${} 占位符解析器
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
//   解析${} 占位符,并将解析结果添加到DynamicContext中
    context.appendSql(parser.parse(text));
    return true;
  }
  private GenericTokenParser createParser(TokenHandler handler) {
//    创建占位符解析器,GenericTokenParser 是一个通用解析器,并非只能解析${}
    return new GenericTokenParser("${", "}", handler);
  }

在TextSqlNode 类的内部持有了一个绑定记号解析器BindingTokenParser,用于解析标记内容,并将结果返回给GenericTokenParser。核心代码如下:

//***TextSqlNode.BindingTokenParser
    public String handleToken(String content) {
      Object parameter = context.getBindings().get("_parameter");
      if (parameter == null) {
        context.getBindings().put("value", null);
      } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
        context.getBindings().put("value", parameter);
      }
      //从缓存里取得值
//     通过ONGL从用户传入的参数中获取结果
      Object value = OgnlCache.getValue(content, context.getBindings());
      String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
//     通过正则表达式检测setValue有效性
      checkInjection(srtValue);
      return srtValue;
    }

而GenericTokenParser 则是一个通用的记号解析器,用户处理#{}和${}参数。核心代码如下:

//*GenericTokenParser
  public String parse(String text) {
    StringBuilder builder = new StringBuilder();
    if (text != null && text.length() > 0) {
      char[] src = text.toCharArray();
      int offset = 0;
      int start = text.indexOf(openToken, offset);
      //#{favouriteSection,jdbcType=VARCHAR}
      //这里是循环解析参数,参考GenericTokenParserTest,比如可以解析${first_name} ${initial} ${last_name} reporting.这样的字符串,里面有3个 ${}
      while (start > -1) {
       //判断一下 ${ 前面是否是反斜杠,这个逻辑在老版的mybatis中(如3.1.0)是没有的
        if (start > 0 && src[start - 1] == '\\') {
          // the variable is escaped. remove the backslash.
         //新版已经没有调用substring了,改为调用如下的offset方式,提高了效率
          //issue #760
          builder.append(src, offset, start - offset - 1).append(openToken);
          offset = start + openToken.length();
        } else {
          int end = text.indexOf(closeToken, start);
          if (end == -1) {
            builder.append(src, offset, src.length - offset);
            offset = src.length;
          } else {
            builder.append(src, offset, start - offset);
            offset = start + openToken.length();
            String content = new String(src, offset, end - offset);
            //得到一对大括号里的字符串后,调用handler.handleToken,比如替换变量这种功能
            builder.append(handler.handleToken(content));
            offset = end + closeToken.length();
          }
        }
        start = text.indexOf(openToken, offset);
      }
      if (offset < src.length) {
        builder.append(src, offset, src.length - offset);
      }
    }
    return builder.toString();
  }

接着我们来看看IfSqlNode,该Sql节点主要是判断<if></if>的条件是否成立,成立的话则调用其他节点的apply方法进行解析,并返回true。不成立的话则直接返回false。

public boolean apply(DynamicContext context) {
    //通过ONGL评估test 表达式的结果
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
//      若test表达式中的条件成立,则调用其它节点的apply方法进行解析。
      contents.apply(context);
      return true;
    }
    return false;
  }

解析#{}占位符

经过前面的解析,我们已经能够从DynamicContext 中获取到完整的SQL语句了。但是这并不意味着解析工作就结束了。我们还有#{}占位符没有处理。#{}占位符不同于${}占位符的处理方式。MyBatis 并不会直接将#{}占位符替换成相应的参数值。

#{}的解析过程封装在SqlSourceBuilder 的parse方法中。解析后的结果交给StaticSqlSource处理。话不多说,来看看源码吧。

//*SqlSourceBuilder
  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
//   创建#{} 占位符处理器
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    //替换#{}中间的部分,如何替换,逻辑在ParameterMappingTokenHandler
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
//  解析#{}占位符,并返回解析结果
    String sql = parser.parse(originalSql);
    //封装解析结果到StaticSqlSource中,并返回
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

如上源码,该解析过程主要有四部,核心步骤就是解析#{}占位符,并返回结果。GenericTokenParser 类在前面已经解析过了,下面我们重点看看SqlSourceBuilder的内部类ParameterMappingTokenHandler。该类的核心方法是handleToken方法。该方法的主要作用是将#{}替换成? 并返回。然后就是构建参数映射。ParameterMappingTokenHandler 该类同样实现了TokenHandler 接口,所以GenericTokenParser 类的parse方法可以调用到。

//参数映射记号处理器,静态内部类
  private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
    private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
    private Class<?> parameterType;
    private MetaObject metaParameters;
    public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) {
      super(configuration);
      this.parameterType = parameterType;
      this.metaParameters = configuration.newMetaObject(additionalParameters);
    }
    public List<ParameterMapping> getParameterMappings() {
      return parameterMappings;
    }
    @Override
    public String handleToken(String content) {
      //获取context的对应的ParameterMapping
      parameterMappings.add(buildParameterMapping(content));
      //如何替换很简单,永远是一个问号,但是参数的信息要记录在parameterMappings里面供后续使用
      return "?";
    }
    //构建参数映射
    private ParameterMapping buildParameterMapping(String content) {
        //#{favouriteSection,jdbcType=VARCHAR}
        //先解析参数映射,就是转化成一个hashmap
      /*
     * parseParameterMapping 内部依赖 ParameterExpression 对字符串进行解析,ParameterExpression 的
     */
      Map<String, String> propertiesMap = parseParameterMapping(content);
      String property = propertiesMap.get("property");
      Class<?> propertyType;
      // metaParameters 为 DynamicContext 成员变量 bindings 的元信息对象
      if (metaParameters.hasGetter(property)) {
        /*
     * parameterType 是运行时参数的类型。如果用户传入的是单个参数,比如 Article 对象,此时
     * parameterType 为 Article.class。如果用户传入的多个参数,比如 [id = 1, author = "coolblog"],
     * MyBatis 会使用 ParamMap 封装这些参数,此时 parameterType 为 ParamMap.class。如果
     * parameterType 有相应的 TypeHandler,这里则把 parameterType 设为 propertyType
     */
        propertyType = metaParameters.getGetterType(property);
      } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
        propertyType = parameterType;
      } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
        propertyType = java.sql.ResultSet.class;
      } else if (property != null) {
        MetaClass metaClass = MetaClass.forClass(parameterType);
        if (metaClass.hasGetter(property)) {
          propertyType = metaClass.getGetterType(property);
        } else {
          // 如果 property 为空,或 parameterType 是 Map 类型,则将 propertyType 设为 Object.class
          propertyType = Object.class;
        }
      } else {
        propertyType = Object.class;
      }
   //      ----------------------------分割线---------------------------------
      ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
//     将propertyType赋值给javaType
      Class<?> javaType = propertyType;
      String typeHandlerAlias = null;
//      遍历propertiesMap
      for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
        String name = entry.getKey();
        String value = entry.getValue();
        if ("javaType".equals(name)) {
//         如果用户明确配置了javaType,则以用户的配置为准。
          javaType = resolveClass(value);
          builder.javaType(javaType);
        } else {
          throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}.  Valid properties are " + parameterProperties);
        }
      }
      //#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
      if (typeHandlerAlias != null) {
        builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
      }
      return builder.build();
    }

如上,buildParameterMapping方法,主要做了如下三件事

1.解析content,

2.解析propertyType,对应分割线上面的代码

3.构建ParameterMapping,对应分割下下面的代码。

最终的结果是将 #{xxx} 占位符中的内容解析成 Map。

例如:

上面占位符中的内容最终会被解析成如下的结果:

{
           "property": "age",
           "typeHandler": "MyTypeHandler",
           "jdbcType": "NUMERIC",
           "javaType": "int"
       }

BoundSql的创建过程就此结束了。我们接着往下看。


创建StatementHandler

StatementHandler 是非常核心的接口,从代码分词的角度来说,StatementHandler是MyBatis源码的边界,再往下层就是JDBC层面的接口了。StatementHandler需要和JDBC层面的接口打交道。它要做的事情有很多,在执行SQL之前,StatementHandler 需要创建合适的Statement对象。然后填充参数值到Statement对象中,最后通过Statement 对象执行SQL。待SQL执行完毕,还需要去处理查询结果。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0cMuXOsM-1582632260584)(./images/1559481523011.png)]


设置运行时参数到SQL中

JDBC 提供了三种 Statement 接口,分别是 Statement、PreparedStatement 和 CallableStatement。他们的关系如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-25GvS01Y-1582632260586)(./images/1559481899968.png)]


上面三个接口的层级分明,其中 Statement 接口提供了执行 SQL,获取执行结果等基本功能。PreparedStatement 在此基础上,对 IN 类型的参数提供了支持。使得我们可以使用运行时参数替换 SQL 中的问号 ? 占位符,而不用手动拼接 SQL。CallableStatement 则是 在 PreparedStatement 基础上,对 OUT 类型的参数提供了支持,该种类型的参数用于保存存储过程输出的结果。


本节,我将分析 PreparedStatement 的创建,以及设置运行时参数到 SQL 中的过程。其他两种 Statement 的处理过程,大家请自行分析。Statement 的创建入口是在 SimpleExecutor 的 prepareStatement 方法中,下面从这个方法开始进行分析。

//*SimpleExecutor
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
//   获取数据库连接
    Connection connection = getConnection(statementLog);
    //创建Statement
    stmt = handler.prepare(connection);
    //为Statement设置IN参数
    handler.parameterize(stmt);
    return stmt;
  }

如上,上面代码的逻辑不复杂,总共包含三个步骤。如下:


获取数据库连接

创建 Statement

为 Statement 设置 IN 参数

上面三个步骤看起来并不难实现,实际上如果大家愿意写,也能写出来。不过 MyBatis 对着三个步骤进行拓展,实现上也相对复杂一下。以获取数据库连接为例,MyBatis 并未没有在 getConnection 方法中直接调用 JDBC DriverManager 的 getConnection 方法获取获取连接,而是通过数据源获取获取连接。MyBatis 提供了两种基于 JDBC 接口的数据源,分别为 PooledDataSource 和 UnpooledDataSource。创建或获取数据库连接的操作最终是由这两个数据源执行。限于篇幅问题,本节不打算分析以上两种数据源的源码,相关分析会在下一篇文章中展开。


接下来,我将分析 PreparedStatement 的创建,以及 IN 参数设置的过程。按照顺序,先来分析 PreparedStatement 的创建过程。如下:

//*PreparedStatementHandler
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    Statement statement = null;
    try {
        // 创建 Statement
        statement = instantiateStatement(connection);
        // 设置超时和 FetchSize
        setStatementTimeout(statement, transactionTimeout);
        setFetchSize(statement);
        return statement;
    } catch (SQLException e) {
        closeStatement(statement);
        throw e;
    } catch (Exception e) {
        closeStatement(statement);
        throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
}
protected Statement instantiateStatement(Connection connection) throws SQLException {
    //调用Connection.prepareStatement
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
        return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
      } else {
        return connection.prepareStatement(sql, keyColumnNames);
      }
    } else if (mappedStatement.getResultSetType() != null) {
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    } else {
      return connection.prepareStatement(sql);
    }
  }

如上,PreparedStatement 的创建过程没什么复杂的地方,就不多说了。下面分析运行时参数是如何被设置到 SQL 中的过程。

public void parameterize(Statement statement) throws SQLException {
    //通过参数处理器ParameterHandler设置运行时参数到PreparedStatement中
    parameterHandler.setParameters((PreparedStatement) statement);
  }

地方

public void setParameters(PreparedStatement ps) throws SQLException {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    /*
    * 从BoundSql中获取ParameterMapping列表,每个ParameterMapping
    * 与原始SQL中的#{xxx} 占位符一一对应
    * */
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      //循环设参数
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
//       检测参数类型,排除掉mode为OUT类型的parameterMapping
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          //如果不是OUT,才设进去
          Object value;
//          获取属性名
          String propertyName = parameterMapping.getProperty();
//         检测BoundSql的additionalParameter是否包含propertyName
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            //若有额外的参数, 设为额外的参数
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            //若参数为null,直接设null
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            //若参数有相应的TypeHandler,直接设object
            value = parameterObject;
          } else {
            //除此以外,MetaObject.getValue反射取得值设进去
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
//          之上,获取#{xxx}占位符属性所对应的运行时参数
//          -------------------分割线-----------------------
//      之下,获取#{xxx}占位符属性对应的TypeHandler,并在最后通过TypeHandler将运行时参数值设置到
//          PreparedStatement中。
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            //不同类型的set方法不同,所以委派给子类的setParameter方法
            jdbcType = configuration.getJdbcTypeForNull();
          }
//        由类型处理器typeHandler向ParameterHandler设置参数
          typeHandler.setParameter(ps, i + 1, value, jdbcType);
        }
      }
    }
  }

如上代码,分割线以上的大段代码用于获取 #{xxx} 占位符属性所对应的运行时参数。分割线以下的代码则是获取 #{xxx} 占位符属性对应的 TypeHandler,并在最后通过 TypeHandler 将运行时参数值设置到 PreparedStatement 中。关于 TypeHandler 的用途。


相关文章
|
2月前
|
SQL XML Java
mybatis-源码深入分析(一)
mybatis-源码深入分析(一)
|
13天前
|
SQL 缓存 Java
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
本文详细介绍了MyBatis的各种常见用法MyBatis多级缓存、逆向工程、分页插件 包括获取参数值和结果的各种情况、自定义映射resultMap、动态SQL
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
|
1月前
|
SQL Java 数据库连接
mybatis使用四:dao接口参数与mapper 接口中SQL的对应和对应方式的总结,MyBatis的parameterType传入参数类型
这篇文章是关于MyBatis中DAO接口参数与Mapper接口中SQL的对应关系,以及如何使用parameterType传入参数类型的详细总结。
30 10
|
2月前
|
SQL XML Java
mybatis复习03,动态SQL,if,choose,where,set,trim标签及foreach标签的用法
文章介绍了MyBatis中动态SQL的用法,包括if、choose、where、set和trim标签,以及foreach标签的详细使用。通过实际代码示例,展示了如何根据条件动态构建查询、更新和批量插入操作的SQL语句。
mybatis复习03,动态SQL,if,choose,where,set,trim标签及foreach标签的用法
|
1月前
|
SQL 存储 数据可视化
手机短信SQL分析技巧与方法
在手机短信应用中,SQL分析扮演着至关重要的角色
|
2月前
|
SQL XML Java
mybatis :sqlmapconfig.xml配置 ++++Mapper XML 文件(sql/insert/delete/update/select)(增删改查)用法
当然,这些仅是MyBatis功能的初步介绍。MyBatis还提供了高级特性,如动态SQL、类型处理器、插件等,可以进一步提供对数据库交互的强大支持和灵活性。希望上述内容对您理解MyBatis的基本操作有所帮助。在实际使用中,您可能还需要根据具体的业务要求调整和优化SQL语句和配置。
44 1
|
3月前
|
前端开发 Java JSON
Struts 2携手AngularJS与React:探索企业级后端与现代前端框架的完美融合之道
【8月更文挑战第31天】随着Web应用复杂性的提升,前端技术日新月异。AngularJS和React作为主流前端框架,凭借强大的数据绑定和组件化能力,显著提升了开发动态及交互式Web应用的效率。同时,Struts 2 以其出色的性能和丰富的功能,成为众多Java开发者构建企业级应用的首选后端框架。本文探讨了如何将 Struts 2 与 AngularJS 和 React 整合,以充分发挥前后端各自优势,构建更强大、灵活的 Web 应用。
58 0
|
SQL 安全 Java
MyBatis映射文件深入--动态sql
MyBatis映射文件深入--动态sql
105 0
|
4月前
|
SQL Java 数据库连接
mybatis动态SQL常用语法总结
MyBatis 使用 OGNL 表达式语言处理动态SQL,如 `if` 标签进行条件判断,`choose`、`when`、`otherwise` 实现多条件选择,`where`、`set` 管理SQL关键字,`trim` 提供通用修剪功能,`foreach` 遍历集合数据。`sql` 和 `include` 用于代码重用,`selectKey` 处理插入后的返回值。参数传递支持匿名、具名、列表、Map、Java Bean和JSON方式。注意SQL转义及使用合适的jdbcType映射Java类型。
90 7
|
5月前
|
SQL XML Java
MyBatis第四课动态SQL
MyBatis第四课动态SQL