MyBatis使用EnumTypeHandler时出现的问题

应用环境

MyBatis版本为3.4.5,mybatis-generator版本为1.3.5,jdk1.8。非常简单的一张表,需要将Enum成员转为int进行处理,用于生成代码的mbg.xml中相关内容为

<table schema="" tableName="notify">
  <columnOverride column="state" javaType="cn.tony.entity.type.NotifyState" jdbcType="INTEGER" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
</table>

问题描述

在mybatis配置文件中没有显式指定任何TypeHandler,使用自动生成的mapper接口中的selecByExample方法进行查询,指定了枚举类型相关的条件,查询时报错: org.apache.ibatis.type.TypeException: Failed invoking constructor for handler class org.apache.ibatis.type.EnumOrdinalTypeHandler 。如果不指定枚举相关的条件,则没有任何问题。代码简单展示如下:

@Autowired
private NotifyMapper notifyMapper;

@Test
@Transactional
public void testNotifyQueryExample(){
   NotifyExample example = new NotifyExample();
   example.createCriteria().andStateEqualTo(NotifyState.UN_SEND);
   List<Notify> notifys = notifyMapper.selectByExample(example);
   System.out.println("result : "+notifys.size());
}

相关的mapper xml如下所示:

<select id="selectByExample" parameterType="cn.tony.entity.NotifyExample" resultMap="BaseResultMap">
  select
  <if test="distinct">
    distinct
  </if>
  <include refid="Base_Column_List" />
  from notify
  <if test="_parameter != null">
    <include refid="Example_Where_Clause" />
  </if>
  <if test="orderByClause != null">
    order by ${orderByClause}
  </if>
</select>

原因解析

由于普通条件查询没有任何问题,因此可以确定问题一定出在Enum的这个TypeHandler上,比较诡异的是普通查询正常返回说明TypeHandler是起了作用的。

debug进行分析排查,最后出错的位置为 org.apache.ibatis.type.TypeHandlerRegistry#getInstance ,也就是创建TypeHandler时发生错误。该方法有两个入参,都是Class类型,一个是需要映射的类型的Class,另一个则是TypeHandler的Class。可以看到如果映射类型的Class为空则调用TypeHandler的无参构造函数创建TypeHandler,如果不为空则调用入参为单个Class类型的构造函数,这也解释了大部分TypeHandler构造函数都是无参,而Enum相关的两个默认的TypeHandler的构造函数确是可以带参数的。

public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
  if (javaTypeClass != null) {
    try {
      Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
      return (TypeHandler<T>) c.newInstance(javaTypeClass);
    } catch (NoSuchMethodException ignored) {
      // ignored
    } catch (Exception e) {
      throw new TypeException("Failed invoking constructor for handler " + typeHandlerClass, e);
    }
  }
  try {
    Constructor<?> c = typeHandlerClass.getConstructor();
    return (TypeHandler<T>) c.newInstance();
  } catch (Exception e) {
    throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e);
  }
}

debug显示传入的javaTypeClass为Object.class,查看EnumOrdinalTypeHandler的构造函数就可以很明显地发现如果传入类型不是Enum的子类型就会报错。

接下来顺着调用堆栈查看为什么最后解析出来的是Object.class而不是预想的类型。发现获取这个Class的地方位于 org.apache.ibatis.builder.SqlSourceBuilder 类的静态子类ParameterMappingTokenHandler的方法buildParameterMapping中。入参为String,内容形如

"__frch_criterion_1.value,typeHandler=org.apache.ibatis.type.EnumOrdinalTypeHandler"

再往前进行查阅可以发现这个内容源于mapper的xml解析。过程简单描述为:解析mapper对应的xml内容,解析替换掉其中的辅助标签,获得一个用于过渡的sql语句。

select id, comment_id, state, master_read from notify
WHERE (  state = #{__frch_criterion_1.value,typeHandler=org.apache.ibatis.type.EnumOrdinalTypeHandler} )

可以发现入参就是#{…}中的表达式,mybatis需要知道这个表达式对应的Java类型。在ParameterMappingTokenHandler这个类中可以发现已经包含一个类型为MetaObject的名为metaParameters的成员,这是mybatis对于入参的元数据封装,查看里面的内容可以发现包含了example类的信息,拿debug内容来说,里面包含了一个类型map:

  1. “_parameter” → xxxExample
  2. “__frch_criteria_0” → xxxExample$Criteria
  3. “__frch_criterion_1” → xxxExample$Criterion

就是example和其中的子类。为了获取上述表达式对应的类型,metaParameter执行getGetterType方法(实际上是执行objectMapper成员的getGetterType),这个方法将循环封装调用,简单来说会根据 ‘.’ 分割上述表达式,挨个解析,解析的方法是使用 反射 查看类中指定名称成员的类型。

那么表达式 __frch_criterion_1.value 对应的就是example下Criterion子类中的value字段,可以发现就是Object类型,终于真相大白。不过这个value字段为了通用性的要求还就必须是Object类型。

结论与解决

在debug过程中可以发现由于没有显式注册TypeHandler,每次调用都会新建一个TypeHandler,从而进入上述发生错误的代码,因此显式注册TypeHandler后问题不再发生。

// org.apache.ibatis.builder.BaseBuilder
protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, Class<? extends TypeHandler<?>> typeHandlerType) {
  if (typeHandlerType == null) {
    return null;
  }
  // javaType ignored for injected handlers see issue #746 for full detail
  TypeHandler<?> handler = typeHandlerRegistry.getMappingTypeHandler(typeHandlerType);
  if (handler == null) {
    // not in registry, create a new one
    handler = typeHandlerRegistry.getInstance(javaType, typeHandlerType);
  }
  return handler;
}

此外还能够看到一个mybatis目前还没有关闭的issue:在注册中心获取TypeHandler时没有考虑JavaTypeClass,这将导致为不同类型注册的同一种TypeHandler在获取时会出现问题, getMappingTypeHandler 这个方法本质上是从HashMap里面把对应的Handler取出来,这将导致 不能对同一个TypeHandler注册多次 ,最后一次配置将覆盖之前的配置,产生错误。只能手写继承EnumOrdinalTypeHandler进行逐个配置,每个enum类型对应一种子类handler。

<configuration>
 <!-- other config --> 
 <!-- ... -->
         
 <typeHandlers>        
     <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="cn.tony.entity.type.NotifyState" />  
     <!-- 不可以像这样重复配置!!! -->        
     <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="cn.tony.entity.type.TestState" />    
 </typeHandlers>
 
 <!-- other config --> 
 <!-- ... --> 
 
</configuration>

原文:MyBatis使用EnumTypeHandler时出现的问题 | TONY'S TOY BLOG