最近在項目中用到了自定義的枚舉類typeHandler,參考了網上的代碼,定義的枚舉類處理器如下:
@MappedTypes({ BaseCodeEnum.class, UserType.class, UserStatus.class, Gender.class }) public class CodeEnumTypeHandler<E extends Enum<?> & BaseCodeEnum> extends BaseTypeHandler<BaseCodeEnum> { private Class<E> type; public CodeEnumTypeHandler(Class<E> type) { if (type == null) { throw new IllegalArgumentException("Type argument cannot be null."); } this.type = type; } @Override public void setNonNullParameter(PreparedStatement ps, int i, BaseCodeEnum parameter, JdbcType jdbcType) throws SQLException { ps.setInt(i, parameter.getCode()); } @Override public BaseCodeEnum getNullableResult(ResultSet rs, String columnName) throws SQLException { int code = rs.getInt(columnName); return rs.wasNull() ? null : CodeEnumUtil.codeOf(type, code); } @Override public BaseCodeEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException { int code = rs.getInt(columnIndex); return rs.wasNull() ? null : CodeEnumUtil.codeOf(type, code); } @Override public BaseCodeEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { int code = cs.getInt(columnIndex); return cs.wasNull() ? null : CodeEnumUtil.codeOf(type, code); } }
最開始使用的mybatis-spring-boot-starter版本為2.1.0,發現在自定義的TypeHandler上使用@MappedTypes注解標注需要處理的Java枚舉類型,並在spring配合文件中如下配置並不生效:
mybatis:
mapper-locations: classpath:/mapper/*
type-aliases-package: com.xx.**.entity
type-handlers-package: com.xx.mybatis.handler
需要在mapper.xml文件中指定TypeHandler才可以進行正確的存儲與讀取
<resultMap type="com.xx.UserEntity" id="userResultMap"> <id property="id" column="id"/> <result property="username" column="username"/> <result property="status" column="status" typeHandler="com.xx.mybatis.handler.CodeEnumTypeHandler"/> </resultMap>
由於某些原因,需要將mybatis-spring-boot-starter版本升級為2.1.4,發現在使用該版本時,代碼不能正常的運行,追蹤代碼發現,在讀取數據時,拿到的TypeHandler的目標java類型不正確,為了解決這個問題,追蹤了mybatis的TypeHandler注冊流程,發現注冊的過程可以正常的進行,試着把mapper.xml文件中的TypeHandler去掉以后,發現代碼可以正常運行
<resultMap type="com.xx.UserEntity" id="userResultMap"> <id property="id" column="id"/> <result property="username" column="username"/> <result property="status" column="status"/> </resultMap>
為此,下載了mybatis-spring-boot-starter的源碼進行閱讀,相關的代碼入口在SqlSessionFactoryBean.java
@Override public void afterPropertiesSet() throws Exception { notNull(dataSource, "Property 'dataSource' is required"); notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required"); state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null), "Property 'configuration' and 'configLocation' can not specified with together"); // 具體的代碼邏輯在次方法內部 this.sqlSessionFactory = buildSqlSessionFactory(); }
buildSqlSessionFactory涉及到的內容較大,這里只對TypeHandler的處理部分做記錄:
protected SqlSessionFactory buildSqlSessionFactory() throws Exception { ...... // spring配置文件中配置的mybatis.type-handlers-package,掃描給定目錄下的TypeHandler實現類,進行注冊 if (hasLength(this.typeHandlersPackage)) { scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass()) .filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers())) .forEach(targetConfiguration.getTypeHandlerRegistry()::register); // 注冊處理流程 } if (!isEmpty(this.typeHandlers)) { Stream.of(this.typeHandlers).forEach(typeHandler -> { targetConfiguration.getTypeHandlerRegistry().register(typeHandler); LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'"); }); } targetConfiguration.setDefaultEnumTypeHandler(defaultEnumTypeHandler); if (this.mapperLocations != null) { if (this.mapperLocations.length == 0) { LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found."); } else { for (Resource mapperLocation : this.mapperLocations) { if (mapperLocation == null) { continue; } try { XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments()); // 對mapper.xml文件進行處理,解析resultMap、sql等,配置文件中的TypeHandler的處理流程在這個流程中 xmlMapperBuilder.parse(); } catch (Exception e) { throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e); } finally { ErrorContext.instance().reset(); } LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'"); } } } else { LOGGER.debug(() -> "Property 'mapperLocations' was not specified."); } ...... return this.sqlSessionFactoryBuilder.build(targetConfiguration); }
下面關注一下register方法的內部邏輯:
public void register(Class<?> typeHandlerClass) { boolean mappedTypeFound = false; // 獲取TypeHandler處理的java類型 MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class); if (mappedTypes != null) { for (Class<?> javaTypeClass : mappedTypes.value()) { // 對每個java類型注冊TypeHandler register(javaTypeClass, typeHandlerClass); mappedTypeFound = true; } } if (!mappedTypeFound) { register(getInstance(null, typeHandlerClass)); } } public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) { // 關鍵的代碼在getInstance內部 register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass)); } public <T> void register(Class<T> javaType, TypeHandler<? extends T> typeHandler) { register((Type) javaType, 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); } } // register最終調用的為該方法 private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) { if (javaType != null) { Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType); if (map == null || map == NULL_TYPE_HANDLER_MAP) { map = new HashMap<>(); } map.put(jdbcType, handler); typeHandlerMap.put(javaType, map); } // 注意:mapper.xml文件中獲取的TypeHandler是從該map中進行獲取的, // 可以發現,當TypeHandler的@MapperTypes有多個時,該map中的數據前面的會被后面的更新掉,所以mapper.xml文件中獲取得到的TypeHandler均為同一個 // 導致枚舉類型獲取實例會出現獲取不到、賦值(調用set方法)的時候會出現類型不匹配的問題 allTypeHandlersMap.put(handler.getClass(), handler); }
接下來看一下mapper.xml文件中關於TypeHandler的解析,具體邏輯在XMLMapperBuilder的parse方法中:
public void parse() { if (!configuration.isResourceLoaded(resource)) { // 解析邏輯 configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }
private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.isEmpty()) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); cacheRefElement(context.evalNode("cache-ref")); cacheElement(context.evalNode("cache")); // 可參考parameterMap的解析,其他元素中的TypeHandler處理類似 parameterMapElement(context.evalNodes("/mapper/parameterMap")); resultMapElements(context.evalNodes("/mapper/resultMap")); sqlElement(context.evalNodes("/mapper/sql")); buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); } } private void parameterMapElement(List<XNode> list) { for (XNode parameterMapNode : list) { String id = parameterMapNode.getStringAttribute("id"); String type = parameterMapNode.getStringAttribute("type"); Class<?> parameterClass = resolveClass(type); List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter"); List<ParameterMapping> parameterMappings = new ArrayList<>(); for (XNode parameterNode : parameterNodes) { String property = parameterNode.getStringAttribute("property"); String javaType = parameterNode.getStringAttribute("javaType"); String jdbcType = parameterNode.getStringAttribute("jdbcType"); String resultMap = parameterNode.getStringAttribute("resultMap"); String mode = parameterNode.getStringAttribute("mode"); String typeHandler = parameterNode.getStringAttribute("typeHandler"); Integer numericScale = parameterNode.getIntAttribute("numericScale"); ParameterMode modeEnum = resolveParameterMode(mode); Class<?> javaTypeClass = resolveClass(javaType); JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType); Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler); ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale); parameterMappings.add(parameterMapping); } builderAssistant.addParameterMap(id, parameterClass, parameterMappings); } } public ParameterMapping buildParameterMapping( Class<?> parameterType, String property, Class<?> javaType, JdbcType jdbcType, String resultMap, ParameterMode parameterMode, Class<? extends TypeHandler<?>> typeHandler, Integer numericScale) { resultMap = applyCurrentNamespace(resultMap, true); // Class parameterType = parameterMapBuilder.type(); Class<?> javaTypeClass = resolveParameterJavaType(parameterType, property, javaType, jdbcType); // 獲取TypeHandler TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler); return new ParameterMapping.Builder(configuration, property, javaTypeClass) .jdbcType(jdbcType) .resultMapId(resultMap) .mode(parameterMode) .numericScale(numericScale) .typeHandler(typeHandlerInstance) .build(); } 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 TypeHandler<?> handler = typeHandlerRegistry.getMappingTypeHandler(typeHandlerType); if (handler == null) { // not in registry, create a new one handler = typeHandlerRegistry.getInstance(javaType, typeHandlerType); } return handler; } public TypeHandler<?> getMappingTypeHandler(Class<? extends TypeHandler<?>> handlerType) { // 參見上面的說明,可以指定獲取的針對不同類型的Enum,獲取得到的handler是同一個,會出現類型不匹配的問題 return allTypeHandlersMap.get(handlerType); }