今天开发 APP 后台程序的时候,升级了一下数据类型,把很多原本 String 类型的变量改成了集合类型。这里涉及到 MyBatis 中的自定义类型处理,记录在这里以作备忘。本文的讨论基于 MyBatis 3.4.0 版本。
类型处理器 TypeHandler
MyBatis 中的 TypeHandler 类型处理器用于 JavaType 与 JdbcType 之间的转换,用于 PreparedStatement 设置参数值和从 ResultSet 或 CallableStatement 中取出一个值。MyBatis 内置了大部分基本类型的类型处理器,所以对于基本类型可以直接处理,当我们需要处理其他类型的时候就需要自定义类型处理器。
MyBatis 内置的 TypeHandler
在 MyBatis 的 TypeHandlerRegistry 类型中,可以看到内置的类型处理器。内置处理器比较多,这里整理常见的一些。
BooleanTypeHandler:用于 java 类型 boolean,jdbc 类型 bit、boolean
ByteTypeHandler:用于 java 类型 byte,jdbc 类型 TINYINT
ShortTypeHandler:用于 java 类型 short,jdbc 类型 SMALLINT
IntegerTypeHandler:用于 INTEGER 类型
LongTypeHandler:用于 long 类型
FloatTypeHandler:用于 FLOAT 类型
DoubleTypeHandler:用于 double 类型
StringTypeHandler:用于 java 类型 string,jdbc 类型 CHAR、VARCHAR
ArrayTypeHandler:用于 jdbc 类型 ARRAY
BigDecimalTypeHandler:用于 java 类型 BigDecimal,jdbc 类型 REAL、DECIMAL、NUMERIC
DateTypeHandler:用于 java 类型 Date,jdbc 类型 TIMESTAMP
DateOnlyTypeHandler:用于 java 类型 Date,jdbc 类型 DATE
TimeOnlyTypeHandler:用于 java 类型 Date,jdbc 类型 TIME
对于常见的 Enum 类型,内置了 EnumTypeHandler 进行 Enum 名称的转换和 EnumOrdinalTypeHandler 进行 Enum 序数的转换。这两个类型处理器没有在 TypeHandlerRegistry 中注册,如果需要使用必须手动配置。
自定义 TypeHandler
自定义类型处理器是通过实现 org.apache.ibatis.type.TypeHandler 接口实现的。这个接口定义了类型处理器的基本功能,接口定义如下所示。
public interface TypeHandler{ 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;
}
其中 setParameter 方法用于把 java 对象设置到 PreparedStatement 的参数中,getResult 方法用于从 ResultSet 或 CallableStatement 中取出数据转换为 java 对象。
实际开发中,我们可以继承 org.apache.ibatis.type.BaseTypeHandler 类型来实现自定义类型处理器。这个类型是抽象类型,实现了 TypeHandler 的方法进行通用流程的封装,做了异常处理,并定义了几个类似的抽象方法,如下所示。继承 BaseTypeHandler 类型可以极大地降低开发难度。
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 类型和 jdbc 类型:
@MappedTypes:注解配置 java 类型
@MappedJdbcTypes:注解配置 jdbc 类型
源码分析
MyBatis 启动后首先把 typeHandler 注册进去。首先尝试读取 MappedTypes 注解,如果有这个注解定义了 java 类型,则把这个类型处理器注册到相应的 java 类型的处理器中。如果没有使用注解,但是继承了 TypeReference 类型,比如前面提到的 BaseTypeHandler,则通过 TypeReference 的接口获取原始类型注册到相应的 java 类型的处理器中。如果实在是获取不到 java 类型,则按照无类型处理。
publicvoid register(TypeHandler typeHandler) { boolean mappedTypeFound = false; MappedTypes mappedTypes = typeHandler.getClass()
.getAnnotation(MappedTypes.class); if (mappedTypes != null) { for (Class> handledType : mappedTypes.value()) { register(handledType, typeHandler); mappedTypeFound = true; } } // @since 3.1.0 - try to auto-discover the mapped type if (!mappedTypeFound && typeHandler instanceof TypeReference) { try { TypeReferencetypeReference = (TypeReference ) typeHandler; register(typeReference.getRawType(), typeHandler); mappedTypeFound = true; } catch (Throwable t) { // maybe users define the TypeReference with a different type and
are not assignable, so just ignore it } } if (!mappedTypeFound) { register((Class) null, typeHandler); } }
MyBatis 在预处理语句设置参数时调用 TypeHandler 进行 java 对象到 jdbc 的 PreparedStatement 参数值的转换。以下为其中一个调用片段。
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) { jdbcType = configuration.getJdbcTypeForNull();
}
try { typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) { throw new TypeException("Could not set parameters for mapping: "
+ parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) { throw new TypeException("Could not set parameters for mapping: "
+ parameterMapping + ". Cause: " + e, e);
}
MyBatis 查询数据库完成后,调用 TypeHandler 的方法读取数据转换成 java 对象。以下为其中一个调用片段。
private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject,
ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { if (propertyMapping.getNestedQueryId() != null) { return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping,
lazyLoader, columnPrefix); } else if (propertyMapping.getResultSet() != null) { addPendingChildRelation(rs, metaResultObject, propertyMapping);
// TODO is that OK? return DEFERED; } else { final TypeHandler> typeHandler = propertyMapping.getTypeHandler(); final String column = prependPrefix(propertyMapping.getColumn(),
columnPrefix); return typeHandler.getResult(rs, column); }
}
关于 TypeHandler,您有什么理解和不解呢,欢迎留言共同讨论。
分享学习笔记和技术总结,内容涉及 Java 技术、软件架构、前沿技术、开源框架、数据结构与算法、编程感悟等多个领域,欢迎关注。本文首发于微信公众号“后端开发那点事儿” 。