MyBatis使用EnumTypeHandler时出现的问题
应用环境
MyBatis版本为3.4.5,mybatis-generator版本为1.3.5,jdk1.8。非常简单的一张表,需要将Enum成员转为int进行处理,用于生成代码的mbg.xml中相关内容为:
1 | <table schema="" tableName="notify"> |
问题描述
在mybatis配置文件中没有显式指定任何TypeHandler,使用自动生成的mapper接口中的selecByExample方法进行查询,指定了枚举类型相关的条件,查询时报错:org.apache.ibatis.type.TypeException: Failed invoking constructor for handler class org.apache.ibatis.type.EnumOrdinalTypeHandler
。如果不指定枚举相关的条件,则没有任何问题。代码简单展示如下:
1 |
|
相关的mapper xml如下所示:
1 | <select id="selectByExample" parameterType="cn.tony.entity.NotifyExample" resultMap="BaseResultMap"> |
原因解析
由于普通条件查询没有任何问题,因此可以确定问题一定出在Enum的这个TypeHandler上,比较诡异的是普通查询正常返回说明TypeHandler是起了作用的。
debug进行分析排查,最后出错的位置为org.apache.ibatis.type.TypeHandlerRegistry#getInstance
,也就是创建TypeHandler时发生错误。该方法有两个入参,都是Class类型,一个是需要映射的类型的Class,另一个则是TypeHandler的Class。可以看到如果映射类型的Class为空则调用TypeHandler的无参构造函数创建TypeHandler,如果不为空则调用入参为单个Class类型的构造函数,这也解释了大部分TypeHandler构造函数都是无参,而Enum相关的两个默认的TypeHandler的构造函数确是可以带参数的。
1 | public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) { |
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 | select id, comment_id, state, master_read from notify |
可以发现入参就是#{…}中的表达式,mybatis需要知道这个表达式对应的Java类型。在ParameterMappingTokenHandler这个类中可以发现已经包含一个类型为MetaObject的名为metaParameters的成员,这是mybatis对于入参的元数据封装,查看里面的内容可以发现包含了example类的信息,拿debug内容来说,里面包含了一个类型map:
- “_parameter” -> xxxExample
- “__frch_criteria_0” -> xxxExample$Criteria
- “__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 | // org.apache.ibatis.builder.BaseBuilder |
此外还能够看到一个mybatis目前还没有关闭的issue:在注册中心获取TypeHandler时没有考虑JavaTypeClass,这将导致为不同类型注册的同一种TypeHandler在获取时会出现问题,getMappingTypeHandler
这个方法本质上是从HashMap里面把对应的Handler取出来,这将导致不能对同一个TypeHandler注册多次,最后一次配置将覆盖之前的配置,产生错误。只能手写继承EnumOrdinalTypeHandler进行逐个配置,每个enum类型对应一种子类handler。
1 | <configuration> |