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);
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM