MyBatis 源码–类型转换包

gomkiri 发布于 22 天前 32 次阅读


AI 摘要

数据库与Java类型如何映射?MyBatis通过TypeHandler巧妙解决。BaseTypeHandler运用模板方法模式,统一处理空值,而具体类型转换逻辑则由子类实现。以LongTypeHandler为例,揭秘JDBC与Java对象间的精准转换。

*声明,本文章的参考
Mybatis 3.5.19 源码
技术文章摘抄-深入剖析 MyBatis 核心原理-完

通过反射包提供的工具,我们可以为 JDBC 对象和 Java 对象完成转换和赋值操作,但是还有一个问题,数据库中的数据类型和 Java 数据类型不是一一对应的,比如 VARCHAR 和 CHAR 都是对应了 String 类型,那么 MyBatis 在进行数据转换时,该如何转换为相对应的类型呢。

TypeHandler

MyBatis 巧妙的使用了一个模版方法模式,先看一看接口和抽象方法:

public interface TypeHandler<T> {

  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}




public abstract class BaseTypeHandler<T> implements TypeHandler<T> {


  @Deprecated
  protected Configuration configuration;

  @Deprecated
  public void setConfiguration(Configuration c) {
    this.configuration = c;
  }

  @Override
  public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
    if (parameter == null) {
      if (jdbcType == null) {
        throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
      }
      try {
        ps.setNull(i, jdbcType.TYPE_CODE);
      } catch (SQLException e) {
        throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
            + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
            + "Cause: " + e, e);
      }
    } else {
      try {
        setNonNullParameter(ps, i, parameter, jdbcType);
      } catch (Exception e) {
        throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
            + "Try setting a different JdbcType for this parameter or a different configuration property. " + "Cause: "
            + e, e);
      }
    }
  }

  @Override
  public T getResult(ResultSet rs, String columnName) throws SQLException {
    try {
      return getNullableResult(rs, columnName);
    } catch (Exception e) {
      throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set.  Cause: " + e,
          e);
    }
  }

  @Override
  public T getResult(ResultSet rs, int columnIndex) throws SQLException {
    try {
      return getNullableResult(rs, columnIndex);
    } catch (Exception e) {
      throw new ResultMapException("Error attempting to get column #" + columnIndex + " from result set.  Cause: " + e,
          e);
    }
  }

  @Override
  public T getResult(CallableStatement cs, int columnIndex) throws SQLException {
    try {
      return getNullableResult(cs, columnIndex);
    } catch (Exception e) {
      throw new ResultMapException(
          "Error attempting to get column #" + columnIndex + " from callable statement.  Cause: " + e, e);
    }
  }

  public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType)
      throws SQLException;

  public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;

  public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException;

  public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;

}
Java

可以看到 BashTypeHandler 的实现方法中调用了 setNonNullParameter 、getNullableResult 方法,但是并没有实现逻辑,只保留了抽象接口。然后具体的实现类只需要重写这三个接口即可。

BaseTypeHandler 的实现类有很多,上面没有全部展示,以其中的一个 LongTypeHandler 方法来看眼实现:

public class LongTypeHandler extends BaseTypeHandler<Long> {

  // 创建一个全局的单例
  public static final LongTypeHandler INSTANCE = new LongTypeHandler();

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Long parameter, JdbcType jdbcType) throws SQLException {
    ps.setLong(i, parameter);
  }

  @Override
  public Long getNullableResult(ResultSet rs, String columnName) throws SQLException {
    long result = rs.getLong(columnName);
    return result == 0 && rs.wasNull() ? null : result;
  }

  @Override
  public Long getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    long result = rs.getLong(columnIndex);
    return result == 0 && rs.wasNull() ? null : result;
  }

  @Override
  public Long getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    long result = cs.getLong(columnIndex);
    return result == 0 && cs.wasNull() ? null : result;
  }
}
Java

可以看到,实际做的就是帮我们将数据从 JDBC 的类型转换为 Java 的类型。这样就解决了转换问题,但是有一个新的问题,该如何找到我们需要的那个转换器。这时就需要一个仓库 TypehandlerRegistry。

TypehandlerRegistry

管理 Handler 的仓库,注册在 configuration 中。

其中有四个非常重要的集合:

  • jdbcTypeHandlerMap:该集合记录了 JdbcType 与 TypeHandler 之间的关联关系。JdbcType 是一个枚举类型,每个 JdbcType 枚举值对应一种 JDBC 类型,例如,JdbcType.VARCHAR 对应的就是 JDBC 中的 varchar 类型。在从 ResultSet 中读取数据的时候,就会从 JDBC_TYPE_HANDLER_MAP 集合中根据 JDBC 类型查找对应的 TypeHandler,将数据转换成 Java 类型。
  • typeHandlerMap:该集合第一层 Key 是需要转换的 Java 类型,第二层 Key 是转换的目标 JdbcType,最终的 Value 是完成此次转换时所需要使用的 TypeHandler 对象。那为什么要有两层 Map 的设计呢?这里我们举个例子:Java 类型中的 String 可能转换成数据库中的 varchar、char、text 等多种类型,存在一对多关系,所以就可能有不同的 TypeHandler 实现。
  • allTypeHandlersMap:该集合记录了全部 TypeHandler 的类型以及对应的 TypeHandler 实例对象。
  • NULL_TYPE_HANDLER_MAP:空 TypeHandler 集合的标识,默认值为 Collections.emptyMap()。

项目启动时,configuration 会触发他的构造器的,将所有的常见的组合注册进去:

  public TypeHandlerRegistry(Configuration configuration) {
    // If a handler is registered against null JDBC type, it is the default handler for the Java type. Users can
    // override the default handler (e.g. `register(boolean.class, null, new YNBooleanTypeHandler())` or register a
    // custom handler for a specific Java-JDBC type combination (e.g. `register(boolean.class, JdbcType.CHAR, new
    // YNBooleanTypeHandler())`).
    register(new Type[] { Boolean.class, boolean.class }, new JdbcType[] { null }, BooleanTypeHandler.INSTANCE);
    register(new Type[] { Byte.class, byte.class }, new JdbcType[] { null }, ByteTypeHandler.INSTANCE);
    register(new Type[] { Short.class, short.class }, new JdbcType[] { null }, ShortTypeHandler.INSTANCE);
    register(new Type[] { Integer.class, int.class }, new JdbcType[] { null }, IntegerTypeHandler.INSTANCE);
    register(new Type[] { Long.class, long.class }, new JdbcType[] { null }, LongTypeHandler.INSTANCE);
    register(new Type[] { Float.class, float.class }, new JdbcType[] { null }, FloatTypeHandler.INSTANCE);
    register(new Type[] { Double.class, double.class }, new JdbcType[] { null }, DoubleTypeHandler.INSTANCE);
    register(new Type[] { Character.class, char.class }, new JdbcType[] { null }, new CharacterTypeHandler());
    register(String.class, null, StringTypeHandler.INSTANCE);
    register(Reader.class, null, new ClobReaderTypeHandler());
    register(BigInteger.class, null, new BigIntegerTypeHandler());
    register(BigDecimal.class, null, BigDecimalTypeHandler.INSTANCE);
    register(InputStream.class, null, new BlobInputStreamTypeHandler());
    register(Byte[].class, null, new ByteObjectArrayTypeHandler());
    register(byte[].class, null, ByteArrayTypeHandler.INSTANCE);
    register(Date.class, null, DateTypeHandler.INSTANCE);
    register(java.sql.Date.class, null, new SqlDateTypeHandler());
        // .........
    jdbcTypeHandlerMap.put(JdbcType.NCHAR, NStringTypeHandler.INSTANCE);
    jdbcTypeHandlerMap.put(JdbcType.LONGNVARCHAR, NStringTypeHandler.INSTANCE);
    jdbcTypeHandlerMap.put(JdbcType.NCLOB, NClobTypeHandler.INSTANCE);
    jdbcTypeHandlerMap.put(JdbcType.ARRAY, new ArrayTypeHandler());
    jdbcTypeHandlerMap.put(JdbcType.BINARY, ByteArrayTypeHandler.INSTANCE);
    jdbcTypeHandlerMap.put(JdbcType.VARBINARY, ByteArrayTypeHandler.INSTANCE);
    jdbcTypeHandlerMap.put(JdbcType.LONGVARBINARY, ByteArrayTypeHandler.INSTANCE);
    jdbcTypeHandlerMap.put(JdbcType.BLOB, BlobTypeHandler.INSTANCE);
    jdbcTypeHandlerMap.put(JdbcType.TIMESTAMP, DateTypeHandler.INSTANCE);
    jdbcTypeHandlerMap.put(JdbcType.DATE, DateOnlyTypeHandler.INSTANCE);
    jdbcTypeHandlerMap.put(JdbcType.TIME, TimeOnlyTypeHandler.INSTANCE);
  }
Java

下面就以 register(new Type[] { Long.class, long.class }, new JdbcType[] { null }, LongTypeHandler.INSTANCE); 为例,过一遍 register 的核心逻辑:

  private void register(Type[] mappedJavaTypes, JdbcType[] mappedJdbcTypes, TypeHandler<?> handler) {
    for (Type javaType : mappedJavaTypes) {
      // 遍历需要注册的 java 类型,这里只要 Long 一个
      if (javaType == null) {
        continue;
      }

      // 如果已经创建了内层的 map ,则直接往里添加新的并修改;如果没有创建就创建并新增。
      typeHandlerMap.compute(javaType, (k, v) -> {
        Map<JdbcType, TypeHandler<?>> map = (v == null || v == NULL_TYPE_HANDLER_MAP ? new HashMap<>() : v);
        // 从一步传过来的 mappedJdbcTypes 是 new JdbcType[] { null },也就是说他里面只要一项,就是个 null。这里我们要记得,null 可以作为 map 的 key ,且是唯一的。
        for (JdbcType jdbcType : mappedJdbcTypes) {
          map.put(jdbcType, handler);
        }
        return map;
      });

      // 处理含参数类型的数据类型,这里不会走到这
      if (javaType instanceof ParameterizedType) {
        // MEMO: add annotation to skip this?
        Type rawType = ((ParameterizedType) javaType).getRawType();
        typeHandlerMap.compute(rawType, (k, v) -> {
          Map<JdbcType, TypeHandler<?>> map = (v == null || v == NULL_TYPE_HANDLER_MAP ? new HashMap<>() : v);
          for (JdbcType jdbcType : mappedJdbcTypes) {
            map.merge(jdbcType, handler, (handler1, handler2) -> handler1.equals(handler2) ? handler1
                : new ConflictedTypeHandler((Class<?>) rawType, jdbcType, handler1, handler2));
          }
          return map;
        });
      }
    }

    allTypeHandlersMap.put(handler.getClass(), handler);
  }
Java

这样就完成了所有常见类型的注册。同时,MyBatis 还提供了自定义式的 Handler 注册,当我们调用 register(TypeHandler<T> handler) 方法,会先分别通过 mappedJavaTypes 和 mappedJdbcTypes 两个方法分别读取 MappedTypes 和 MappedJdbcTypes 两个注解的内容,然后再走统一的 register方法:

  public <T> void register(TypeHandler<T> handler) {
    register(mappedJavaTypes(handler.getClass()), mappedJdbcTypes(handler.getClass()), handler);
  }
  
  private Type[] mappedJavaTypes(Class<?> clazz) {
    MappedTypes mappedTypesAnno = clazz.getAnnotation(MappedTypes.class);
    if (mappedTypesAnno != null) {
      return mappedTypesAnno.value();
    }
    return TypeParameterResolver.resolveClassTypeParams(TypeHandler.class, clazz);
  }
  
  
  private JdbcType[] mappedJdbcTypes(Class<?> clazz) {
    MappedJdbcTypes mappedJdbcTypesAnno = clazz.getAnnotation(MappedJdbcTypes.class);
    if (mappedJdbcTypesAnno != null) {
      JdbcType[] jdbcTypes = mappedJdbcTypesAnno.value();
      if (mappedJdbcTypesAnno.includeNullJdbcType()) {
        int newLength = jdbcTypes.length + 1;
        jdbcTypes = Arrays.copyOf(jdbcTypes, newLength);
        jdbcTypes[newLength - 1] = null;
      }
      return jdbcTypes;
    }
    return new JdbcType[] { null };
  }
  
Java

TypehandlerRegister 提供了一系列重载的 getTypeHandler 方法用于获取 Handler ,其中最主要的一个实现是TypeHandler<?> getTypeHandler(Type type, JdbcType jdbcType)

  public TypeHandler<?> getTypeHandler(Type type, JdbcType jdbcType) {
    if (ParamMap.class.equals(type)) {
      // 忽略 MyBatis 的 Param
      return null;
    } else if (type == null) {
      return getTypeHandler(jdbcType);
    }

    TypeHandler<?> handler = null;
    
    // 拿到 java 类型对应的所有的处理器
    Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
    
    // 有具体类型就有限匹配,没有就用 null 兜底,没有 null 就有其他可能得兜底

    if (Object.class.equals(type)) {
      if (jdbcHandlerMap != null) {
        handler = jdbcHandlerMap.get(jdbcType);
      }
      return handler;
    }

    if (jdbcHandlerMap != null) {
      handler = jdbcHandlerMap.get(jdbcType);
      if (handler == null) {
        handler = jdbcHandlerMap.get(null);
      }
      if (handler == null) {
        // #591
        handler = pickSoleHandler(jdbcHandlerMap);
      }
    }
    if (handler == null) {
      handler = getSmartHandler(type, jdbcType);
    }
    if (handler == null && type instanceof ParameterizedType) {
      handler = getTypeHandler((Class<?>) ((ParameterizedType) type).getRawType(), jdbcType);
    }
    return handler;
  }
  
  
  private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMap(Type type) {
    // 先从已注册的 jdbcHandlerMap 去拿,能拿到的话做完防御校验就可以 return
    // 没有注册的话就需要调用 getJdbcHandlerMapForSuperclass 方法
    Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = typeHandlerMap.get(type);
    if (jdbcHandlerMap != null) {
      return NULL_TYPE_HANDLER_MAP.equals(jdbcHandlerMap) ? null : jdbcHandlerMap;
    }
    if (type instanceof Class) {
      Class<?> clazz = (Class<?>) type;
      if (!Enum.class.isAssignableFrom(clazz)) {
        jdbcHandlerMap = getJdbcHandlerMapForSuperclass(clazz);
      }
    }
    typeHandlerMap.put(type, jdbcHandlerMap == null ? NULL_TYPE_HANDLER_MAP : jdbcHandlerMap);
    return jdbcHandlerMap;
  }


  private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMapForSuperclass(Class<?> clazz) {
    // 从 typeHandlerMap 中获取对应类的处理器,如何没有就去找他的父类
    Class<?> superclass = clazz.getSuperclass();
    if (superclass == null || Object.class.equals(superclass)) {
      return null;
    }
    Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = typeHandlerMap.get(superclass);
    if (jdbcHandlerMap != null) {
      return jdbcHandlerMap;
    }
    return getJdbcHandlerMapForSuperclass(superclass);
  }
Java

TypeAliasRegistry

仅使用反射的方式去构建类,就需要写出的类的全类名,还是很长的,因此 MyBatis 帮我们对常用的数据类型注册了别名,并放置在一个 Map<String,Class<?>> 类型的 typeAliases 属性中。然后将 TypeAliasRegitsrty 注册在 Configuration 中。

  private final Map<String, Class<?>> typeAliases = new HashMap<>();

  public TypeAliasRegistry() {
    registerAlias("string", String.class);

    registerAlias("byte", Byte.class);
    registerAlias("char", Character.class);
    // ......
    registerAlias("arraylist", ArrayList.class);
    registerAlias("collection", Collection.class);
    registerAlias("iterator", Iterator.class);

    registerAlias("ResultSet", ResultSet.class);
  }
  
  public void registerAlias(String alias, Class<?> value) {
    if (alias == null) {
      throw new TypeException("The parameter alias cannot be null");
    }
    // issue #748
    String key = alias.toLowerCase(Locale.ENGLISH);
    if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
      throw new TypeException(
          "The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
    }
    typeAliases.put(key, value);
  }
Java

然后提供一个 resolveAlias 方法来从中获取别名对应的 Class 类:

  public <T> Class<T> resolveAlias(String string) {
    try {
      if (string == null) {
        return null;
      }
      // issue #748
      String key = string.toLowerCase(Locale.ENGLISH);
      Class<T> value;
      if (typeAliases.containsKey(key)) {
        value = (Class<T>) typeAliases.get(key);
      } else {
        value = (Class<T>) Resources.classForName(string);
      }
      return value;
    } catch (ClassNotFoundException e) {
      throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
    }
  }
Java