应用环境

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

1
2
3
<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。如果不指定枚举相关的条件,则没有任何问题。代码简单展示如下:

1
2
3
4
5
6
7
8
9
10
11
@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如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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的构造函数确是可以带参数的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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,内容形如

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

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

1
2
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后问题不再发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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>
使用Entity Graph优化JPA查询
使用rsync部署Hexo博客