MyBatis 类型处理器

Contents

  1. 1. 问题描述
  2. 2. 定位问题
  3. 3. 解决问题

问题描述

最近测试将 MyBatis 从 3.1.1 升级到 3.2.3 时遇到一个问题。原来可以正常工作的枚举类型处理器,抛异常了。

1
Caused by: org.apache.ibatis.executor.result.ResultMapException: Error attempting to get column 'member_type' from result set.  Cause: java.lang.IllegalArgumentException: No enum code 'MERCHANT'. class com...ChangeSceneType

涉及的代码及配置信息如下:

mybatis-config.xml :

1
2
3
4
5
6
7
8
9
10
<typeAliases>
<typeAlias type="com...CodeEnumTypeHandler" alias="enumHandler" />
</typeAliases>

<typeHandlers>
<!-- 其他枚举配置... -->
<typeHandler handler="com...CodeEnumTypeHandler" javaType="com...MemberType" />
<!-- 其他枚举配置... -->
<typeHandler handler="com...CodeEnumTypeHandler" javaType="com...ChangeSceneType" />
</typeHandlers>

Mapper:

1
2
3
4
5
<resultMap id="XxxMap" type="com...XxxEntity">
<!-- 其他属性配置... -->
<result column="member_type" property="memberType" jdbcType="VARCHAR" typeHandler="enumHandler"/>
<!-- 其他属性配置... -->
</resultMap>

CodeEnumTypeHandler.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class CodeEnumTypeHandler<E extends Enum & CodeEnum> extends BaseTypeHandler<E> {

private Class<E> type;

private Map<String, E> enums;

public CodeEnumTypeHandler(Class<E> type) {
this.type = type;
E[] es = type.getEnumConstants();
if (es == null || es.length==0) {
throw new IllegalArgumentException(type.getSimpleName() + " does not represent an enum type.");
}
enums = new HashMap<String, E>();
for (E e: es) {
enums.put(e.getCode(), e);
}
}

private E valueOf(String code) {
if (code == null) {
return null;
}
E e = enums.get(code);
if (e == null) {
throw new IllegalArgumentException("No enum code '" + code + "'. " + type);
}
return e;
}

@Override
public void setNonNullParameter(PreparedStatement ps, int i, E e, JdbcType jdbcType)
throws SQLException {
ps.setString(i, e.getCode());
}

@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return valueOf(value);
}

@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return valueOf(value);
}

@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return valueOf(value);
}
}

定位问题

1
2
3
4
5
1. org.apache.ibatis.builder.xml.XMLMapperBuilder#resultMapElement(org.apache.ibatis.parsing.XNode, java.util.List<org.apache.ibatis.mapping.ResultMapping>)
2. org.apache.ibatis.builder.MapperBuilderAssistant#buildResultMapping(java.lang.Class<?>, java.lang.String, java.lang.String, java.lang.Class<?>, org.apache.ibatis.type.JdbcType, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.Class<? extends org.apache.ibatis.type.TypeHandler<?>>, java.util.List<org.apache.ibatis.mapping.ResultFlag>, java.lang.String, java.lang.String, boolean)
3. org.apache.ibatis.builder.MapperBuilderAssistant#buildResultMapping(java.lang.Class<?>, java.lang.String, java.lang.String, java.lang.Class<?>, org.apache.ibatis.type.JdbcType, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.Class<? extends org.apache.ibatis.type.TypeHandler<?>>, java.util.List<org.apache.ibatis.mapping.ResultFlag>, java.lang.String, java.lang.String, boolean)
4. org.apache.ibatis.builder.BaseBuilder#resolveTypeHandler(java.lang.Class<?>, java.lang.Class<? extends org.apache.ibatis.type.TypeHandler<?>>)
5. org.apache.ibatis.type.TypeHandlerRegistry#getMappingTypeHandler

DEBUG 跟踪代码发现是 BaseBuilder 和 TypeHandlerRegistry 两个类做了调整

通过 Github 上 TypeHandlerRegistry 类的变更记录,发现是 commit e92c2a2 这次提交引入了这些变更。注释说明了这次变更的原因在 issue #746 中有记录。为了解决 Spring 注入依赖的问题,有了这次的代码变更。

可以看到缓存 TypeHandler 的 Map 有了变化

1
2
-  private final Map<Class<?>, Map<Type, TypeHandler<?>>> REVERSE_TYPE_HANDLER_MAP = new HashMap<Class<?>, Map<Type, TypeHandler<?>>>();
+ private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<Class<?>, TypeHandler<?>>();

原来查找具体的 TypeHandler 是先根据 TypeHandler.class 类查找到 Map<Type, TypeHandler<?>>,最后通过属性的 javaType 查找到 TypeHandler。所以之前的代码和配置运行是没问题。但是之后改成了直接通过 TypeHandler.class 找 TypeHandler,所以 <typeHandler handler="com...CodeEnumTypeHandler" javaType="com...ChangeSceneType" /> 之前枚举的 TypeHandler 配置都会被覆盖掉,会抛出 No enum code 'MERCHANT'. class com...ChangeSceneType 异常也就很好理解了。

解决问题

既然是直接通过 TypeHandler.class 找 TypeHandler,那么就可以对每种枚举都实现一个对应的 TypeHandler 类。这样带来的问题也很明显,项目中有多少枚举就得写多少对应的 TypeHandler。

1
2
3
4
5
public class MemberTypeHandler extends CodeEnumTypeHandler<MemberType> {
public MemberTypeHandler(Class<MemberType> type) {
super(type);
}
}

通过在 Github Issue 里翻 enum 相关的信息,在 Issue #995 找到了一个完美的解决办法。就是去掉 <resultMap> 配置中的typeHandler 属性。也就是说指定 typeHandler 是没有必要的,为什么是这样呢?

在设置返回对象的属性值时有如下的调用顺序:

1
2
3
4
5
1. org.apache.ibatis.executor.resultset.DefaultResultSetHandler#applyPropertyMappings
2. org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getPropertyMappingValue
3. org.apache.ibatis.type.BaseTypeHandler#getResult(java.sql.ResultSet, java.lang.String)
4. com...CodeEnumTypeHandler#getNullableResult(java.sql.ResultSet, java.lang.String)
5. com...CodeEnumTypeHandler#valueOf

org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getPropertyMappingValue 中有几行代码:

1
2
3
final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
return typeHandler.getResult(rs, column);

通过 propertyMapping 找到了类型处理器,这个 propertyMapping 是通过遍历 org.apache.ibatis.mapping.ResultMappropertyResultMappings 属性获得的。ResultMap 类对应于 Mapper 文件中的 <resultMap> 元素,ResultMapping 类对应于 <resultMap> 元素的子元素 <result>

ResultMap 类中有四个 ResultMapping 列表,ResultMap 实例是由内部类 ResultMap.Builder 构造的,通过其build 方法可以看出 idResultMappingsconstructorResultMappingspropertyResultMappingsresultMappings 的一个子集。resultMappings 又是什么时候构造出来的呢。

1
2
3
4
private List<ResultMapping> resultMappings;
private List<ResultMapping> idResultMappings;
private List<ResultMapping> constructorResultMappings;
private List<ResultMapping> propertyResultMappings;

ResultMap 构建的过程。

1
2
3
4
1. org.apache.ibatis.builder.xml.XMLMapperBuilder#resultMapElement(org.apache.ibatis.parsing.XNode, java.util.List<org.apache.ibatis.mapping.ResultMapping>)
2. org.apache.ibatis.builder.ResultMapResolver#resolve
3. org.apache.ibatis.builder.MapperBuilderAssistant#addResultMap
4. org.apache.ibatis.mapping.ResultMap.Builder#build

ResultMapping 也是由他的内部类ResultMapping.Builder 改造。它的构建的过程如下。

1
2
3
4
1. org.apache.ibatis.builder.xml.XMLMapperBuilder#resultMapElement(org.apache.ibatis.parsing.XNode, java.util.List<org.apache.ibatis.mapping.ResultMapping>)
2. org.apache.ibatis.builder.xml.XMLMapperBuilder#buildResultMappingFromContext
3. org.apache.ibatis.builder.MapperBuilderAssistant#buildResultMapping(java.lang.Class<?>, java.lang.String, java.lang.String, java.lang.Class<?>, org.apache.ibatis.type.JdbcType, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.Class<? extends org.apache.ibatis.type.TypeHandler<?>>, java.util.List<org.apache.ibatis.mapping.ResultFlag>, java.lang.String, java.lang.String, boolean)
4. org.apache.ibatis.mapping.ResultMapping.Builder#build

buildResultMapping 方法会调用前面提到的 BaseBuilder#resolveTypeHandler 方法,而因为我们没有配置 typeHandler 属性,所以此时调用 BaseBuilder#resolveTypeHandler 只会返回 null。但是每个类型都应该有它对应的类型处理器,这个类型处理器是什么时候构建出来的。ResultMapping.Builder#build 方法中就能找到答案了,这个方法会调用ResultMapping.Builder#resolveTypeHandler,如果它发现执行到这里 ResultMappingtypeHandler 属性还为 null,就会调用 typeHandlerRegistry.getTypeHandler(resultMapping.javaType, resultMapping.jdbcType),这个方法和前面提到的同名方法是有区别的。它是从 TYPE_HANDLER_MAP 中取的 typeHandler,而前面是 ALL_TYPE_HANDLERS_MAPTYPE_HANDLER_MAP 中存储的是属性的 javaType 和 jdbcType 与 typeHandler 映射的映射关系,可能有点拗口,也就是通过属性的 javaType 和 jdbcType 就可以找到它对应的类型处理器,而后者
ALL_TYPE_HANDLERS_MAP 存储的是 TypeHandler.class 和 TypeHandler 实例的映射关系。

总结一下,不必配置<result> 元素的 typeHandler 属性是因为这样避免了通过 typeHandler 属性值(在本例中就是之前的enumHandler)找对应的 TypeHandler 实例,而是通过 javaType 和 jdbcType (在本例终究是MemberType枚举和VARCHAR) 找到对应的 TypeHandler 实例。

最后一个问题。TypeHandlerRegistry 中存储的映射关系又是什么时候注册进来的呢。 从方法org.apache.ibatis.builder.xml.XMLConfigBuilder#typeHandlerElement里可以找到答案。