DefaultParameterHandler源码分析|8月更文挑战

598 阅读6分钟

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战


1. 前言

前面的文章说过,MyBatis在执行SQL时,首先需要通过MappedStatement.getBoundSql()得到BoundSql,这个方法会完成xml中SQL语句里${}/#{}的替换。${}直接根据参数值做字符串的简单替换,#{}会被替换成占位符?,BoundSql里面就包含可以用来执行的SQL了。 ​

如下SQL示例:

select * from user where id = #{id}

解析成BoundSql后,其sql如下:

select * from user where id = ?

得到了要执行的SQL语句,接下来就是Executor调用prepareStatement()方法得到可执行的Statement,该方法在创建相应的Statement的同时,还会自动设置Timeout和FetchSize属性。 ​

有了SQL,也创建了Statement,还差最后一步就可以执行Statement了,是哪一步?设置参数嘛,相应的JDBC接口为:

PreparedStatement.setXXX(参数下标,参数值)

StatementHandler是MyBatis提供的处理Statement的接口,它有一个parameterize()方法就是用来设置参数的。它有三大实现类,从名字上也可以看的出来:

  1. SimpleStatementHandler处理Statement。
  2. PreparedStatementHandler处理PreparedStatement。
  3. CallableStatementHandler处理CallableStatement。

对于Statement而言,是不支持设置参数的,因此SimpleStatementHandler.parameterize()方法是空的,什么也不做。 ​

只有PreparedStatement和CallableStatement才需要设置参数,CallableStatement是用来调用存储过程的,这里暂时跳过,重点看PreparedStatement的参数设置过程。 ​

2. DefaultParameterHandler

PreparedStatement的parameterize()源码非常的简单,它将参数设置的任务委托给了ParameterHandler。ParameterHandler是MyBatis提供的接口,它的目的就是用来给PreparedStatement设置参数的。

interface ParameterHandler {

  /**
   * 获取参数对象,就是前面说的ParamMap
   * 调用Mapper接口方法时,MyBatis会将参数转换成ParamMap
   * 得到参数名和参数值的映射关系
   */
  Object getParameterObject();

  /**
   * 给PreparedStatement设置参数
   * @param ps
   * @throws SQLException
   */
  void setParameters(PreparedStatement ps);

}

ParameterHandler接口很简单,职责很清晰,就两个方法,分别是获取参数对象、给PreparedStatement设置参数。 ​

DefaultParameterHandler是MyBatis提供的唯一实现类,我们先看它的属性:

  1. TypeHandlerRegistry是TypeHandler的注册器,例如你的参数是Long,那么对应的实现就是LongTypeHandler,MyBatis依赖TypeHandler来给PreparedStatement设置对应类型的参数。
  2. MappedStatement是SQL节点的描述对象,之前有说过,这里它唯一的作用就是根据StatementID来记录日志。
  3. ParameterObject是MyBatis解析的参数对象,一般来说是ParamMap,参数名称和参数值的映射。
  4. BoundSql是关联的SQL对象,通过它就知道要执行的SQL语句是什么。
  5. Configuration是全局的配置对象,这里需要依赖它创建MetaObject对象元数据。

属性介绍完了,接下来就是构造函数。DefaultParameterHandler的构造函数很简单,就是简单的赋值,这里就不贴代码了。

接下来要看的,就是DefaultParameterHandler最重要的方法setParameters()。 ​

它首先会向ErrorContext报告自己正在设置参数,这样一旦异常了就方便问题的定位。 之后它通过BoundSql获取ParameterMapping列表。ParameterMapping就是对xml中#{id}参数的描述对象,它记录了参数的属性名、JavaType、JdbcType等等。 ​

如下示例:

#{id,javaType=Long,jdbcType=BIGINT}

大部分情况下,我们可以省略javaType、jdbcType,MyBatis会自动进行类型的推导。

有了ParameterMapping就知道需要给SQL语句设置什么参数了,参数名是什么,参数类型是什么等等。接下来要做的就是从parameterObject中取出对应的参数值,然后根据JavaType找到对应的TypeHandler,再通过TypeHandler完成参数的设置。

void setParameters(PreparedStatement ps) {
  // 报告自己正在设置参数
  // 获取参数映射
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  // 遍历
  if (parameterMapping.getMode() != ParameterMode.OUT) {// 过滤输出类型参数
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          // 参数类型在TypeHandlerRegistry中,例如:形参只有一个,且为简单类型,如:Long,此时参数值就是parameterObject本身
          value = parameterObject;
        } else {
          // 获取对象元数据
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          /**
           * 根据属性名获取值
           * 对于ParamMap,直接调用get(key)方法
           * 对应自定义的Bean,反射调用属性的getter方法
           */
          value = metaObject.getValue(propertyName);
        }
        // 获取TypeHandler,我们的参数是Long,所以就是LongTypeHandler
        JdbcType jdbcType = parameterMapping.getJdbcType();
        if (value == null && jdbcType == null) {// 参数为空,JdbcType又没配置
          /*
          当参数不为空时,MyBatis可以根据Java类型推断出JdbcType。
          参数为空时,就无法推断了,此时会使用默认类型:JdbcType.OTHER
          Oracle数据库中,JdbcType.OTHER传入NULL会报异常:无效的列类型,此时必须指定JdbcType
           */
          jdbcType = configuration.getJdbcTypeForNull();
        }
        // 设置参数
        typeHandler.setParameter(ps, i + 1, value, jdbcType);
      }
   }
}

前面已经说过,如果方法的参数只有一个,且没有加@Param注解,则MyBatis不会将参数封装为ParamMap,而是直接返回。 ​

直接返回参数本身又分两种情况: 1、参数为简单类型,如Long,TypeHandlerRegistry已经存在对应的TypeHandler。 2、参数为自定义Bean,TypeHandlerRegistry不存在对应的TypeHandler。 ​

对于第一种情况,直接将parameterObject赋值给value,MyBatis已经具备设置参数的能力了。 ​

对于第二种情况,自定义的复杂对象,我们在xml中使用的是其属性,此时MyBatis会创建MetaObject对象。MetaObject有些复杂,这里不展开讲,你只需要知道它代表的是对象的元数据,通过它可以给对象进行属性的读写,底层还是通过反射来实现的。 ​

如下示例:

List<User> select(User user);

<select id="select">
select * from user where id = {id} and age = #{age}
</select>

形参是User对象,xml中使用的却是#{id}/#{age}属性,MyBatis如何设置参数呢?其实就是通过反射调用User对象的getId()getAge()方法获得属性值,然后设置参数。 ​

知道参数需要设置哪个值了,接下来就是根据参数类型找到对应的TypeHandler。前面已经说过,你可以省略JavaType和JdbcType,MyBatis会自动根据对象类型进行推断,过程是这样子的。 ​

如果没有配置JavaType和JdbcType,此时得到的TypeHandler实现为UnknownTypeHandler,它代表未知的TypeHandler。它会根据参数的Java类型做推导:

// 根据parameter参数对象类型自动推导出TypeHandler
TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
// 设置参数

resolveTypeHandler()方法会根据Java类型推导出TypeHandler。

// TypeHandlerRegistry里有容器记录了JavaType对应的TypeHandler 
handler = typeHandlerRegistrySupplier.get().getTypeHandler(parameter.getClass(), jdbcType);

示例代码中,我们的参数是Long id,因此TypeHandler为LongTypeHandler,我们看看它设置参数的代码,很简单,就是调用了JDBC原生的方法。

// 给第i号参数设置Long类型的值
ps.setLong(i, parameter);

至此,参数的设置流程就全部结束,此时的PreparedStatement已经可以调用execute()方法执行了。

【友情提醒】 如果参数为NULL,MyBatis是无法得到参数类型的,也就无法推导出JdbcType和TypeHandler。此时只能将JdbcType设置为JdbcType.OTHER。在Oracle数据库下,这种情况传入NULL是会抛异常的,因此你必须在xml中指明参数的JdbcType。 ​

3. 总结

ParameterHandler的目的是给PreparedStatement设置参数,MyBatis在解析xml文件中SQL标签节点时,会将#{}参数解析为ParameterMapping对象,通过它就可以知道参数的名称、JavaType、JdbcType等信息。再根据参数名称去ParamMap中解析出参数值,根据参数的JavaType找到对应的TypeHandler,再通过TypeHandler去设置对应类型的参数。

默认情况下,我们不需要在xml中指定参数的的类型,MyBatis会根据对象类型进行反射推导,但前提是对象不允许为NULL,一旦对象为null,就无法获取其Class对象,也就不知道Java对象类型了,间接导致无法推导JDBC类型。