18-Mybatis源码分析(ParameterHandler参数读取)

文章目录

  • Mybatis参数读取源码分析
    • 一、ParameterHandler
    • 二、DefaultParameterHandler
    • 三、流程调试
      • 3.1 SimpleExecutor#doQuery
      • 3.2 SimpleExecutor#prepareStatement
      • 3.3 PreparedStatementHandler#parameterize
      • 3.4 DefaultParameterHandler#setParameters
      • 3.5 BaseTypeHandler#setParameter
      • 3.6 PreparedStatementLogger#invoke
    • 四、小结
      • 4.1 调试流程图
      • 4.2 小结
    • 五、参考

Mybatis参数读取源码分析

  • Mybatis框架最底层需要访问数据库,本质是到数据库去执行sql语句,那么给sql语句传递参数是必不可少的一个环节,sql语句在预编译阶段会用占位符替换参数的位置,在需要执行的时候需要将参数放到对应的位置上,这个将参数替换占位符的工作就是ParameterHandler来完成的,本文我们来看看这个对象处理的相关知识。

一、ParameterHandler

  • ParameterHandler在Mybatis四大对象中负责将sql中的占位符替换为真正的参数,它是一个接口,有且只有一个实现类DefaultParameterHandler
public interface ParameterHandler {

  Object getParameterObject();

  void setParameters(PreparedStatement ps)
      throws SQLException;

}
  • setParameters是处理参数最核心的方法。

二、DefaultParameterHandler

  • DefaultParameterHandler是接口的唯一实现类,代码不多,主要是setParameters方法,通过注释我们可以看到其大体的功能主流程
public class DefaultParameterHandler implements ParameterHandler {

  //1.类型处理器注册中心
  private final TypeHandlerRegistry typeHandlerRegistry;

  //2.MappedStatement是保存sql语句的数据结构
  private final MappedStatement mappedStatement;
  //3.参数对象
  private final Object parameterObject;
  //4.BoundSql对象是sql语句和相关信息的封装
  private BoundSql boundSql;
  //5.全局配置对象
  private Configuration configuration;

  //构造方法
  public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    this.mappedStatement = mappedStatement;
    this.configuration = mappedStatement.getConfiguration();
    this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
    this.parameterObject = parameterObject;
    this.boundSql = boundSql;
  }

  @Override
  public Object getParameterObject() {
    return parameterObject;
  }

  /**
   * 将占位符替换为参数值
   * */
  @Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    //1.获取sql语句的参数,ParameterMapping里面包含参数的名称类型等详细信息,还包括类型处理器
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      //2.遍历依次处理
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        //3.OUT类型参数不处理
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          //4.获取参数名称
          String propertyName = parameterMapping.getProperty();
          //5.如果propertyName是动态参数,就会从动态参数中取值。(当使用的时候,MyBatis会自动生成额外的动态参数)
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
              //6.如果参数是null,不管属性名是什么,都会返回null。
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
              //7.判断类型处理器是否有参数类型,如果参数是一个简单类型,或者是一个注册了typeHandler的对象类型,就会直接使用该参数作为返回值,和属性名无关。
            value = parameterObject;
          } else {
            //8.这种情况下是复杂对象或者Map类型,通过反射方便的取值。通过MetaObject操作
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          //9.获取对应的数据库类型
          JdbcType jdbcType = parameterMapping.getJdbcType();
          //空类型
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          //10.对PreparedStatement的占位符设置值(类型处理器可以给PreparedStatement设值)
          try {
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          } catch (SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }
}
  • 核心流程
1.sql语句中的占位符?都对应了BoundSql#parameterMappings集合中的一个元素,在该对象中记录了对应的参数名称和参数的相关属性。
在SimpleExecutor的doQuery方法中调用prepareStatement方法处理参数占位符,方法里面调用的就是PreparedStatementHandler的parameterize
方法,最终调用的是DefaultParameterHandler#setParameters,在PreparedStatementHandler内部持有ParameterHandler对象。

PS:简单来说就是SimpleExecutor#doQuery方法内部通过配置对象创建StatementHandler -> 调用PreparedStatementHandler#parameterize方
法(内部持有ParameterHandler) ->DefaultParameterHandler#setParameters来完成sql语句执行之前的参数替换占位符

三、流程调试

  • 本节我们通过调试来看看前面说的核心流程,看看ParameterHandler参数处理在整个查询的哪一个步骤完成。断点在:SimpleExecutor#doQuery方法的prepareStatement(handler, ms.getStatementLog()),如下,
    然后执行一次查询操作:
mapper.findMemberById(1);

3.1 SimpleExecutor#doQuery

  • SimpleExecutor#doQuery方法是一次查询的入口,查询处理和返回结果集都在这里。在第2步调用prepareStatement方法获取Statement,里面就包含参数的预处理。(这里还可以参考 14-Mybatis源码和设计模式-5(Executor组件与模板模式,装饰器模式))
/**
   * 查询的实现
   * */
  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      //1.创建StatementHandler
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      //2.用StatementHandler对象创建stmt,并使用StatementHandler对占位符进行处理
      stmt = prepareStatement(handler, ms.getStatementLog());
      //3.通过statementHandler对象调用ResultSetHandler将结果集转化为指定对象返回
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

18-Mybatis源码分析(ParameterHandler参数读取)_第1张图片

3.2 SimpleExecutor#prepareStatement

  • prepareStatement方法是获取Statement,默认是PrepareStatement。(其实是一个增强了的代理对象,具备日志能力的代理对象),其中第三步就是进行参数处理。
  /**
   * 创建Statement
   * */
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    //1.获取connection对象的动态代理,添加日志能力;(这里参考日志模块的代理模式)
    Connection connection = getConnection(statementLog);
    //2.使用StatementHandler,利用connection创建(prepare)Statement
    stmt = handler.prepare(connection, transaction.getTimeout());
    //3.使用StatementHandler处理占位符
    handler.parameterize(stmt);
    return stmt;
  }

18-Mybatis源码分析(ParameterHandler参数读取)_第2张图片

3.3 PreparedStatementHandler#parameterize

  • SimpleExecutor#prepareStatement 的方法首先会走到RoutingStatementHandler#parameterize,但是RoutingStatementHandler其实是一个静态代理,实际上走的是其内部的PreparedStatementHandler(参考:17-Mybatis源码分析(StatementHandler数据库访问))。
    后面就到了PreparedStatementHandler#parameterize,方法很简单,直接调用ParameterHandler的setParameters方法,直接走到DefaultParameterHandler#setParameters
  @Override
  public void parameterize(Statement statement) throws SQLException {
    parameterHandler.setParameters((PreparedStatement) statement);
  }

18-Mybatis源码分析(ParameterHandler参数读取)_第3张图片

3.4 DefaultParameterHandler#setParameters

  • 参考前面的注释和下面的调试信息,这里会直接使用typeHandler进行设值操作,调用之前实际上已经通过参数类型获取到了对应的typeHandler。(parameterMapping.getTypeHandler())

18-Mybatis源码分析(ParameterHandler参数读取)_第4张图片

3.5 BaseTypeHandler#setParameter

  • 这里实际上走的是BaseTypeHandler的子类IntegerTypeHandler,不过setParameter是BaseTypeHandler的一个模板方法,里面的一个流程方法setNonNullParameter会调用子类IntegerTypeHandler的,在IntegerTypeHandler的setNonNullParameter里面直接调用PreparedStatement的setInt方法。到这里其实就走到了PreparedStatement的代理类的invoke方法(我们前面提到过,其实四大对象运行时都是一个代理对象,是一个具备日志能力的代理对象,这里参考:11-Mybatis源码和设计模式-2(日志模块和适配器模式,代理模式)),然后就到了PreparedStatementLogger类。(下面文字写错了,因为参数是1,所以走下面一个逻辑)

  //BaseTypeHandler
  @Override
  public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
     //省略...
      try {
        setNonNullParameter(ps, i, parameter, jdbcType);
      //省略...
      }
  }

 //IntegerTypeHandler
  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setInt(i, parameter);
  }

18-Mybatis源码分析(ParameterHandler参数读取)_第5张图片

18-Mybatis源码分析(ParameterHandler参数读取)_第6张图片

3.6 PreparedStatementLogger#invoke

  • 最后通过method.invoke(statement, params),终于到了真实的PreparedStatement对象,调用的是PreparedStatement的setInt方法。我们发现绕了一圈已经到了JDBC的包下了,是的,Mybatis封装了很多最后底层参数设值还是JDBC那一套。

18-Mybatis源码分析(ParameterHandler参数读取)_第7张图片

18-Mybatis源码分析(ParameterHandler参数读取)_第8张图片

18-Mybatis源码分析(ParameterHandler参数读取)_第9张图片

四、小结

4.1 调试流程图

18-Mybatis源码分析(ParameterHandler参数读取)_第10张图片

4.2 小结

  • Executor是sql语句的执行器,Executor通过配置对象创建StatementHandler,继而得到了StatementHandler,StatementHandler是整个数据库访问过程的控制关键,它的内部持有ParameterHandler,因此StatementHandler可以通过后者来处理参数。在StatementHandler处理参数的过程中会通过参数类型来找到对应的typeHandler来处理参数,整个过程中Statement对象都作为参数在传递,到了typeHandler他会调用Statement的setInt来设置值,其实整个过程中Statement对象都在传递,Mybatis通过封装,但是还是在使用JDBC的API。

  • 关于Mybatis中数据访问的整体流程,可以阅读参考文章[3]

五、参考

  • [1] 深入学习MyBatis中的参数(推荐)
  • [2] 带注释源码
  • [3] [23-Mybatis 核心流程03-数据读取阶段]

你可能感兴趣的:(Mybatis)