2-2 类型处理器源码分析
与类型别名注册表类似,类型处理器注册表也是由BaseBuilder管理的。同样一些其余的XXXBuilder需要使用类型处理器注册表,因此,BaseBuilder负责维护该属性。
接下来首先介绍一下类型处理器的功能。
2-2.1 类型处理器的功能
MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。
上面的解释摘自Mybatis文档,如果你对JDBC有一定使用经验的话,应该对这句话有比较深的理解,这里笔者使用JDBC构架另一个Demo,使用的库仍然是第一节创建的库,只不过本次不再使用Mybatis操作数据库,而是使用原生JDBC。
这次完成的功能仅仅是通过id对数据进行查询:
public class JdbcExample {
public static void main(String[] args) {
try {
// 驱动加载
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
System.exit(-1);
}
String url = "jdbc:mysql://127.0.0.1:3306/mybatis";
String username = "root";
String password = "123456";
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
// 创建连接
connection = DriverManager.getConnection(url, username, password);
// 准备PrepareStatement
preparedStatement = connection.prepareStatement("select id,title,content from blog where id = ?");
// 为PrepareStatement拼接参数
preparedStatement.setInt(1, 4);
// 执行SQL
resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
// 读取结果
System.out.println(resultSet.getString(1));
System.out.println(resultSet.getString(2));
System.out.println(resultSet.getString(3));
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
// 清理资源
...
}
}
}
为了减少代码行数,笔者省略了一些清除资源用的代码。通常情况下,使用JDBC进行数据库操作,我们需要执行如下几步:
- 驱动加载
- 创建连接
- 准备PrepareStatement
- 设置查询参数
- 读取数据
- 清理资源
这里我们主要关注第3-5步。如果使用PrepareStatement操作数据库时,为了防止出现SQL注入,我们通常都会将语句写成这种形式:
select id,title,content from blog where id = ?
其中参数通过PrepareStatement的各个set方法设置到对应的位置,例如本例中的:
preparedStatement.setInt(1, 4);
这意味着将preparedStatement中第一个位置的参数按照Int型的方式设置为4。
等到查询出数据后,JDBC会通过ResultSet接到数据,然后可以通过其提供的get方法获取数据,例如:
System.out.println(resultSet.getString(1));
上面代码的意思是,使用获取String的方式读取resultSet中第一列的数据。
在这之后我们考察一下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;
}
可以看到,这里提供了两类方法:
- 通过列索引(i)为PreparedStatement设置参数
- 通过列索引(columnIndex)读取ResultSet中的结果集
其实TypeHandler就是对PreparedStatement的简单封装,可以让JDBC_TYPE映射到JavaType更加规范化,而不用自己手写了罢了。
不过需要注意,TypeHandler仅仅是将每行的单项数据转化为Java类型,而不是将整行数据进行转换。
你可以通过按照如下步骤创建一个新的TypeHandler(这里引用Mybatis文档中的例子),自定义Java类型与JDBC类型的对应关系。
- 实现
TypeHandler接口 或者 继承BaseTypeHandler// ExampleTypeHandler.java @MappedJdbcTypes(JdbcType.VARCHAR) public class ExampleTypeHandler extends BaseTypeHandler<String> { @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter); } @Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { return rs.getString(columnName); } @Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return rs.getString(columnIndex); } @Override public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return cs.getString(columnIndex); } } - 将TypeHandler配置到Mybatis的配置文件中。
<!-- mybatis-config.xml --> <typeHandlers> <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/> </typeHandlers>
使用上述的类型处理器将会覆盖已有的处理 Java String 类型的属性以及 VARCHAR 类型的参数和结果的类型处理器。 要注意 MyBatis 不会通过检测数据库元信息来决定使用哪种类型,所以你必须在参数和结果映射中指明字段是 VARCHAR 类型, 以使其能够绑定到正确的类型处理器上。这是因为 MyBatis 直到语句被执行时才清楚数据类型。
通过类型处理器的泛型,MyBatis 可以得知该类型处理器处理的 Java 类型,不过这种行为可以通过两种方法改变:
- 在类型处理器的配置元素(typeHandler 元素)上增加一个 javaType 属性(比如:javaType=”String”);
- 在类型处理器的类上增加一个 @MappedTypes 注解指定与其关联的 Java 类型列表。 如果在 javaType 属性中也同时指定,则注解上的配置将被忽略。
可以通过两种方式来指定关联的 JDBC 类型:
- 在类型处理器的配置元素上增加一个 jdbcType 属性(比如:jdbcType=”VARCHAR”);
- 在类型处理器的类上增加一个 @MappedJdbcTypes 注解指定与其关联的 JDBC 类型列表。 如果在 jdbcType 属性中也同时指定,则注解上的配置将被忽略。
当在 ResultMap 中决定使用哪种类型处理器时,此时 Java 类型是已知的(从结果类型中获得),但是 JDBC 类型是未知的。 因此 Mybatis 使用 javaType=[Java 类型], jdbcType=null 的组合来选择一个类型处理器。 这意味着使用 @MappedJdbcTypes 注解可以限制类型处理器的作用范围,并且可以确保,除非显式地设置,否则类型处理器在 ResultMap 中将不会生效。 如果希望能在 ResultMap 中隐式地使用类型处理器,那么设置 @MappedJdbcTypes 注解的 includeNullJdbcType=true 即可。 然而从 Mybatis 3.4.0 开始,如果某个 Java 类型只有一个注册的类型处理器,即使没有设置 includeNullJdbcType=true,那么这个类型处理器也会是 ResultMap 使用 Java 类型时的默认处理器。
最后,可以让 MyBatis 帮你查找类型处理器:
<!-- mybatis-config.xml -->
<typeHandlers>
<package name="org.mybatis.example"/>
</typeHandlers>
注意在使用自动发现功能的时候,只能通过注解方式来指定 JDBC 的类型。
通过上面的描述可以知道,TypeHandler提供了一个三元组<JavaType,JdbcType,TypeHandler>其中,前两者中的任意一个可以为null。
2-2.2 类型处理器源码解析
对于类型解析器的配置解析在XmlConfigBuilder的typeHandlerElement(XNode parent)方法中,代码如下:
private void typeHandlerElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// 处理包扫描类型解析器逻辑
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {
// 处理通过typeHandler标签设置类型解析器的逻辑
String javaTypeName = child.getStringAttribute("javaType");
String jdbcTypeName = child.getStringAttribute("jdbcType");
String handlerTypeName = child.getStringAttribute("handler");
Class<?> javaTypeClass = resolveClass(javaTypeName);
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
Class<?> typeHandlerClass = resolveClass(handlerTypeName);
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
可以看到,typeHandler的解析逻辑与typeAlias相同,也分为两种,分别是扫描整包和解析单个TypeHandler。前文笔者已经提过,typeHandler的配置是一个三元组:<JavaType,JdbcType,TypeHandler>,所以,解析配置也就是将配置文件转化为这三元组的集合,然后存储到TypeHandlerRegistry中。
在TypeHandlerRegistry中,注册单个TypeHandler的方法有如下几种:
- register(Class<?> javaTypeClass, Class<?> typeHandlerClass)
- register(Class<?> typeHandlerClass)
- register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass)
负责扫描整个包的方法是register(String packageName)。
与分析TypeAliasRegistry一样,我们先对注册单个TypeHandler的方法进行分析。在分析具体的方法之前,我们首先对TypeHandlerRegistry的属性进行分析:
public final class TypeHandlerRegistry {
/**
* 存储JdbcType与TypeHandler的关系
*/
private final Map<JdbcType, TypeHandler<?>> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);
/**
* 存储Java类型与JDBC类型以及TypeHandler的关系
*/
private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
/**
* 未知类型处理器
*/
private final TypeHandler<Object> unknownTypeHandler;
/**
* 类型处理器汇总,通过类型处理器Class对象获取类型处理器
*/
private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();
/**
* 空表
*/
private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();
/**
* 枚举类型处理器
*/
private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
}
TypeHandlerRegistry主要提供了4种配置:
- 未知类型的类型处理器
- 枚举类型的类型处理器
- Jdbc类型对应的类型处理器
- Java类型与Jdbc类型之间的关系
首先查看register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass)方法,该方法提供了三元组中的全部信息,是所有方法的根方法:
public <T> void register(Class<T> type, JdbcType jdbcType, TypeHandler<? extends T> handler) {
register((Type) type, jdbcType, handler);
}
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
// 注册JdbcType与TypeHandler的关系
Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
}
map.put(jdbcType, handler);
// 注册Java类型与JDBCType的关系
typeHandlerMap.put(javaType, map);
}
// 统计所有类型处理器
allTypeHandlersMap.put(handler.getClass(), handler);
}
可以看到,注册过程大致如下:
- 构建JdbcType与TypeHandler的关系
- 构建Java类型与JdbcType的关系
- 构建TypeHandler的Class对象与TypeHandler的关系
如果是Java类型直接注册TypeHandler,处理方法是register(Class<?> javaTypeClass, Class<?> 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) {
// 获取对应的JdbcType
MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
if (mappedJdbcTypes != null) {
for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
// JdbcType不为空,调用上述接口注册
register(javaType, handledJdbcType, typeHandler);
}
if (mappedJdbcTypes.includeNullJdbcType()) {
// jdbcType是空,那么,直接将JdbcType设置为null,然后注册
register(javaType, null, typeHandler);
}
} else {
register(javaType, null, typeHandler);
}
}