Mybatis的TypeHandler注册流程

最近在项目中用到了自定义的枚举类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);
}

 

posted on 2021-05-10 14:57  maoshiling  阅读(1145)  评论(0编辑  收藏  举报

导航