Mybatis 的 TypeHandler 是用来将 JavaBean 的属性与数据库中的字段值互相转换的,如果我们的 JavaBean 的属性是简单的属性如 String, Integer, Enum 等,我们一般不用关心 Mybatis 的 TypeHandler,但是如果我们需要将 JavaBean 的复杂对象作为一个字段值存储在数据表中,则需要自定义 TypeHandler 来处理值的映射,比较常见的处理是将复杂对象转换成一个 Json 字符串存储在数据库中,因此需要自定义 JsonTypeHandler,并且我们希望这个 JsonTypeHandler 能够处理泛型。
但是最近写一个代码的时候发现 Mybatis 对于 JavaBean 的一个 Map 属性去获取对应的 TypeHandler 时拿错了泛型对应的类型,促使我看了看 Mybatis 到底是如何加载 TypeHandler,如果获取 TypeHandler,如果获取的
1 起因 在一个 springboot 项目里,在 application.properteis 文件中配置了 typehandler 的 package
1 mybatis.type-handlers-package=me.aki.demo.mybatisdemo.typehandler
并定义了如下 JsonTypeHandler
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 @MappedJdbcTypes(JdbcType.VARCHAR) @MappedTypes({Map.class, List.class, ChartType.class, GraphType.class, DatasourceMeta.class}) public class JsonTypeHandler <T> extends BaseTypeHandler <T> { private final Class<T> type; public JsonTypeHandler (final Class<T> type) { this .type = type; } @Override public void setNonNullParameter (PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, serializeAsJsonString(parameter)); } @Override public T getNullableResult (ResultSet rs, String columnName) throws SQLException { return deserializeFromJson(rs.getString(columnName), type); } @Override public T getNullableResult (ResultSet rs, int columnIndex) throws SQLException { return deserializeFromJson(rs.getString(columnIndex), type); } @Override public T getNullableResult (CallableStatement cs, int columnIndex) throws SQLException { return deserializeFromJson(cs.getString(columnIndex), type); } }
定义了一个 Mapper 类以及 getById 的方法
1 2 3 4 5 6 7 8 9 10 11 @Select("select * from chart where id = #{chartId}") @Results({ @Result(column = "type", property = "type", typeHandler = ChartTypeHandler.class), @Result(column = "graph_styles", property = "graphStyles", typeHandler = GraphStylesTypeHandler.class), @Result(column = "series_display_name", property = "seriesDisplayName", typeHandler = JsonTypeHandler.class), @Result(column = "stats", property = "stats", typeHandler = StatsTypeHandler.class), @Result(column = "headers", property = "headers", typeHandler = HeadersTypeHandler.class), @Result(column = "datasource_meta", property = "datasourceMeta", typeHandler = DataSourceMetaTypeHandler.class), @Result(column = "top_cells", property = "topCells", typeHandler = TopCellsTypeHandler.class), }) Chart getById (@Param("chartId") Integer chartId) ;
然后调用 getById 的方法时报了如下的错误 再 debug 一下发现是 mybatis 在设置 seriesDisplayName 这个属性时虽然使用了 JsonTypeHandler 但是泛型却错了
2 Mybatis TypeHandler 的注册 TypeHandler 的注册都是通过 TypeHandlerRegistry 这个类完成的,mybatis 已经预先定义了一些常用的 typehandler,需要特别注意的是 TYPE_HANDLER_MAP
和 ALL_TYPE_HANDLERS_MAP
这两个属性
1 2 3 4 private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap <JdbcType, TypeHandler<?>>(JdbcType.class);private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new ConcurrentHashMap <Type, Map<JdbcType, TypeHandler<?>>>();private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler (this );private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap <Class<?>, TypeHandler<?>>();
mybatis 中 javaType 与 jdbcType 是 多对多 的关系,对于 String
来说,可以映射的 jdbcType 有 CHAR
VARCHAR
等等, 特别的,jdbcType 也可以指定为 null,用于为 javaType 对应的 jdbcType 未被用户定义的情况,如下是 String
对应的注册关系
1 2 3 4 5 6 7 register(String.class, JdbcType.CHAR, new StringTypeHandler ()); register(String.class, JdbcType.CLOB, new ClobTypeHandler ()); register(String.class, JdbcType.VARCHAR, new StringTypeHandler ()); register(String.class, JdbcType.LONGVARCHAR, new ClobTypeHandler ()); register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler ()); register(String.class, JdbcType.NCHAR, new NStringTypeHandler ()); register(String.class, JdbcType.NCLOB, new NClobTypeHandler ());
TYPE_HANDLER_MAP
维护了这样的多对多的关系,而 ALL_TYPE_HANDLERS_MAP
则是简单粗暴的将 handler 的 class 与 handler 本身对应起来,所以对于 ALL_TYPE_HANDLERS_MAP
来说,接收泛型的 typehandler 在注册的时候存在覆盖的情况。
当使用 springboot 的自动注入时,spring 在构建 SqlSession 时通过 SqlSessionFactoryBean 处理 mybatis 的属性并注册 typehandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 SqlSessionFactoryBean#buildSqlSessionFactory ... if (hasLength(this .typeHandlersPackage)) { String[] typeHandlersPackageArray = tokenizeToStringArray(this .typeHandlersPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); for (String packageToScan : typeHandlersPackageArray) { configuration.getTypeHandlerRegistry().register(packageToScan); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers" ); } } } ...
调用了 TypeHandlerRegistry 的 register 如下
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 public void register (Class<?> typeHandlerClass) { boolean mappedTypeFound = false ; MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class); if (mappedTypes != null ) { for (Class<?> javaTypeClass : mappedTypes.value()) { register(javaTypeClass, typeHandlerClass); mappedTypeFound = true ; } } if (!mappedTypeFound) { register(getInstance(null , typeHandlerClass)); } } 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) { } 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); } } public <T> void register (TypeHandler<T> typeHandler) { boolean mappedTypeFound = false ; MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class); if (mappedTypes != null ) { for (Class<?> handledType : mappedTypes.value()) { register(handledType, typeHandler); mappedTypeFound = true ; } } if (!mappedTypeFound && typeHandler instanceof TypeReference) { try { TypeReference<T> typeReference = (TypeReference<T>) typeHandler; register(typeReference.getRawType(), typeHandler); mappedTypeFound = true ; } catch (Throwable t) { } } if (!mappedTypeFound) { register((Class<T>) null , typeHandler); } } private <T> void register (Type javaType, TypeHandler<? extends T> typeHandler) { MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class); if (mappedJdbcTypes != null ) { for (JdbcType handledJdbcType : mappedJdbcTypes.value()) { register(javaType, handledJdbcType, typeHandler); } if (mappedJdbcTypes.includeNullJdbcType()) { register(javaType, null , typeHandler); } } else { register(javaType, null , typeHandler); } } private void register (Type javaType, JdbcType jdbcType, TypeHandler<?> handler) { if (javaType != null ) { Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType); if (map == null ) { map = new HashMap <JdbcType, TypeHandler<?>>(); TYPE_HANDLER_MAP.put(javaType, map); } map.put(jdbcType, handler); } ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler); }
register 方法跳转很多,总的来说要处理几件事情
没有指定 MappedTypes 的情况下根据 handler 的 TypeReference 拿到其原始类型作为 javaType,指定了 MappedTypes 的情况下则使用 MappedTypes 作为其 javaType,并构造一个该 MappedType 对应的 handler
没有指定 jdbcType 的情况下,使用 null 作为 jdbcType,否则使用指定的 jdbcType
需要注意的点是
假设我们注册了多个 xx entends BaseTypeHandler<List<T>>
,那么他们 typeRerence 的 rawType 都是 List.class,这个 rawType 也就是 javaType 会作为 TYPE_HANDLER_MAP
中的 key,所以如果这多个 handler 的 jdbcType 也是一样的,那么这多个 handler 会在 TYPE_HANDLER_MAP
中被最后一个注册的覆盖
ALL_TYPE_HANDLERS_MAP
对于泛型的 handler,存在相互覆盖,如上文定义的 JsonTypeHandler, 最后只会留下 {JsonTypeHandler, JsonTypeHandler<DatasourceMeta>}
这个键值对
基本的注册流程就是这样子,下面我们来看下 Mybatis 是和做数据库值到 JavaBean 的转换的
3 DB Value 转换成 JavaBean Mybatis 在启动时就会解析我们定义的 Mapper 类,以上文提到的 mapper 为例
1 2 3 4 5 6 7 8 9 10 11 @Select("select * from chart where id = #{chartId}") @Results({ @Result(column = "type", property = "type", typeHandler = ChartTypeHandler.class), @Result(column = "graph_styles", property = "graphStyles", typeHandler = GraphStylesTypeHandler.class), @Result(column = "series_display_name", property = "seriesDisplayName", typeHandler = JsonTypeHandler.class), @Result(column = "stats", property = "stats", typeHandler = StatsTypeHandler.class), @Result(column = "headers", property = "headers", typeHandler = HeadersTypeHandler.class), @Result(column = "datasource_meta", property = "datasourceMeta", typeHandler = DataSourceMetaTypeHandler.class), @Result(column = "top_cells", property = "topCells", typeHandler = TopCellsTypeHandler.class), }) Chart getById (@Param("chartId") Integer chartId) ;
我们用注解的方式定义了数据库中的 column 与 javaBean 的 property 如何映射,以及使用什么 handler。Mybatis 会通过 MapperAnnotaionBuilder 类完成对该方法的解析,并设置 column 与 property 的映射关系
1 2 3 4 5 6 7 8 9 private void applyResultMap (String resultMapId, Class<?> returnType, Arg[] args, Result[] results, TypeDiscriminator discriminator) { List<ResultMapping> resultMappings = new ArrayList <>(); applyConstructorArgs(args, returnType, resultMappings); applyResults(results, returnType, resultMappings); Discriminator disc = applyDiscriminator(resultMapId, returnType, discriminator); assistant.addResultMap(resultMapId, returnType, null , disc, resultMappings, null ); createDiscriminatorResultMaps(resultMapId, returnType, discriminator); }
我们一路跟踪 applyResults(results, returnType, resultMappings) 这个调用链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 1. MapperAnnotaionBuilder#applyResults2. MapperBuilderAssistant#buildResultMapping3. BaseBuilder#resolveTypeHandler protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, Class<? extends TypeHandler <?>> typeHandlerType) { if (typeHandlerType == null ) { return null ; } TypeHandler<?> handler = typeHandlerRegistry.getMappingTypeHandler(typeHandlerType); if (handler == null ) { handler = typeHandlerRegistry.getInstance(javaType, typeHandlerType); } return handler; }
而 TypeHandlerRegistry#getMappingTypeHandler 如下
1 2 3 public TypeHandler<?> getMappingTypeHandler(Class<? extends TypeHandler <?>> handlerType) { return allTypeHandlersMap.get(handlerType); }
可以看到在我们定义了 result 里的 typehandler 时,是直接根据 typehandler 的 class 去 allTypeHandlersMap 拿 handler 的,而我们前文说过,allTypeHandlersMap 中的值是会被覆盖的,这就解释了为什么最后那个 mapper 方法会因为泛型错误而无法进行类型转换了。
如果我们删除 @Result(column = “series_display_name”, property = “seriesDisplayName”, typeHandler = JsonTypeHandler.class) 中的 typeHandler 则程序能正常运行,正如上文中的 resolveTypeHandler,当我们删除 typeHandler 时,typeHandler 为 null,然后走了 handler = typeHandlerRegistry.getInstance(javaType, typeHandlerType)
这个逻辑,也就是会新建一个正确合适的 handler。
这是在程序初始化时完成的事情,对于标注了 @Result 的 property 会在 MapperAnnotaionBuilder 中完成 mapping 的映射,其他的属性则是在运行过程中完成映射的,我们跟踪一下代码的执行链路
执行 query
DefaultResultSetHandler#handleReusltSets
DefaultResultSetHandler#handleResultSet
DefaultResultSetHandler#handleRowValues
DefaultResultSetHandler#getRowValue
DefaultResultSetHandler#applyAutomaticMappings
DefaultResultSetHandler#createAutomaticMappings
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 DefaultResultSetHandler#createAutomaticMappings if (property != null && metaObject.hasSetter(property) ) { if (resultMap.getMappedProperties().contains(property)) { continue ; } final Class<?> propertyType = metaObject.getSetterType(property); if (typeHandlerRegistry.hasTypeHandler(propertyType, rsw.getJdbcType(columnName))) { final TypeHandler<?> typeHandler = rsw.getTypeHandler(propertyType, columnName); autoMapping.add(new UnMappedColumnAutoMapping (columnName, property, typeHandler, propertyType.isPrimitive())); } else { configuration.getAutoMappingUnknownColumnBehavior() .doAction(mappedStatement, columnName, property, propertyType); } } else { configuration.getAutoMappingUnknownColumnBehavior() .doAction(mappedStatement, columnName, (property != null ) ? property : propertyName, null ); }
ResultSetWrapper 获取 TypeHandler 的关键代码如下
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 ResultSetWrapper#getTypeHandler ... JdbcType jdbcType = getJdbcType(columnName); handler = typeHandlerRegistry.getTypeHandler(propertyType, jdbcType); ... TypeHandlerRegistry#getTypeHandler private <T> TypeHandler<T> getTypeHandler (Type type, JdbcType jdbcType) { if (ParamMap.class.equals(type)) { return null ; } Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type); TypeHandler<?> handler = null ; if (jdbcHandlerMap != null ) { handler = jdbcHandlerMap.get(jdbcType); if (handler == null ) { handler = jdbcHandlerMap.get(null ); } if (handler == null ) { handler = pickSoleHandler(jdbcHandlerMap); } } return (TypeHandler<T>) handler; }
也就是根据 propertyType 从 TYPE_HANDLER_MAP
中拿 jdbcHandlerMap,如果能不能根据 jdbcType 拿到 handler 则用 null 去拿 handler,至此,所有的 handler 的获取逻辑就理完了,总结一下获取 typehandler 分两步:
如果定义 @Result,则在 Mybatis 初始化的时候就会配置好相应的 typehandler,且是从 ALL_TYPE_HANDLERS_MAP
取 handler,如果取到为空则新建一个 handler
对于未在 @Result 中定义的 property 是在运行时获取 handler 的,且是从 TYPE_HANDLER_MAP
中获取,取不到的话,这个值最终会被忽略