Mybatis源码之美:2.14.解析Mybatis的typeHandlers元素,配置Mybatis的类型转换器

解析Mybatis的typeHandlers元素,配置Mybatis的类型转换器

在学习本章内容之前,可以通过类型转换器(typeHandlers)来了解关于TypeHandler的用法。

示例

为了更好的理解mybatisTypeHandler对象,我们在测试包org.apache.learning下,新建一个typehandler包,该包下的所有数据,均用于演示TypeHandler对象的用法。

首先我们在包下新建一个CreateDB.sql的脚本文件:

drop table users if exists;
create table users
(
    id     varchar(36),
    name   varchar(20),
    gender varchar(6)

);

insert into users (id, name,gender) values ('38400000-8cf0-11bd-b23e-10b96e4ef00d', 'Panda', '男');

该脚本用于创建一个users表,并插入一条数据以供使用。

之后我们新建一个mybatis-config.xml文件,该文件用于为mybatis提供一个基本的数据源环境配置:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC">
                <property name="" value=""/>
            </transactionManager>
            <!-- 使用 hsql 内存数据库 -->
            <dataSource type="UNPOOLED">
                <property name="driver" value="org.hsqldb.jdbcDriver"/>
                <property name="url" value="jdbc:hsqldb:mem:type_handler_test"/>
                <property name="username" value="sa"/>
            </dataSource>
        </environment>
    </environments>

</configuration>

然后创建一个名为EUserGender的枚举对象:

/**
 * 用户性别
 *
 * @author HanQi [Jpanda@aliyun.com]
 * @version 1.0
 * @since 2020/3/14 10:47
 */
@Getter
@AllArgsConstructor
public enum EUserGender {
    MALE("男"), FEMALE("女");
    private String name;
}

并为EUserGender创建一个专属的TypeHandler——EUserGenderTypeHandler:

public class EUserGenderTypeHandler extends BaseTypeHandler<EUserGender> {

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

    @Override
    public EUserGender getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String columnValue = rs.getString(columnName);

        return findEUserGender(columnValue);
    }

    @Override
    public EUserGender getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String columnValue = rs.getString(columnIndex);
        return findEUserGender(columnValue);
    }

    @Override
    public EUserGender getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String columnValue = cs.getString(columnIndex);
        return findEUserGender(columnValue);
    }

    private EUserGender findEUserGender(String dbName) {
        return Stream.of(EUserGender.values())
                .filter(g -> g.getName().equals(dbName))
                .findFirst()
                .orElse(null);
    }
}

再新建一个名为User的实体对象,对象中的属性定义和创建的users表中的列保持一致:

@Getter
@Setter
@NoArgsConstructor
public class User {
    private String id;
    private String name;
    private EUserGender gender;
}

紧接着新建一个Mapper接口,该接口定义了操作User对象的两个方法:

public interface Mapper {
    List<User> selectByGender(EUserGender gender);

    List<User> selectByGenderWithoutSpecify(EUserGender gender);
}

以及Mapper对应的xml文件——Mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.apache.learning.typehandler.Mapper">

    <resultMap type="org.apache.learning.typehandler.User"
               id="userResult">
        <result property="id" column="id"/>
        <result property="name" column="name"/>
        <!-- 指定属性对应的TypeHandler-->
        <result property="gender" column="gender"
                typeHandler="org.apache.learning.typehandler.EUserGenderTypeHandler"/>
    </resultMap>

    <!-- 指定属性对应的TypeHandler-->
    <select id="selectByGender" resultMap="userResult">
        select *
        from users
        where gender = #{gender, typeHandler=org.apache.learning.typehandler.EUserGenderTypeHandler}
    </select>

    <!-- 未指定属性对应的TypeHandler-->
    <select id="selectByGenderWithoutSpecify" resultType="org.apache.learning.typehandler.User">
        select *
        from users
        where gender = #{gender}
    </select>

</mapper>

其中selectByGender()的方法针对gender属性明确指定了EUserGenderTypeHandler作为类型转换器,selectByGenderWithoutSpecify()方法只是一个普通的方法定义。

完成上述的基础数据的准备之后,新建一个名为typeHanlderTest的单元测试类,测试常见的三种场景:

/**
 * 类型转换器测试类
 *
 * @author HanQi [Jpanda@aliyun.com]
 * @version 1.0
 * @since 2020/3/14 10:45
 */
public class TypeHandlerTest {
    private static SqlSessionFactory sqlSessionFactory;

    @BeforeEach
    @SneakyThrows
    public void setup() {
        @Cleanup
        Reader reader = Resources.getResourceAsReader("org/apache/learning/typehandler/mybatis-config.xml");
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        sqlSessionFactory.getConfiguration().addMapper(Mapper.class);
        BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), "org/apache/learning/typehandler/CreateDB.sql");
    }

    /**
     * 声明语句时,指定类型转换器
     */
    @Test
    public void testForCustomTypeHandler() {
        // 执行准备工作
        @Cleanup
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 执行
        Mapper mapper = sqlSession.getMapper(Mapper.class);
        List<User> users = mapper.selectByGender(EUserGender.MALE);
        Assertions.assertEquals("Panda", users.get(0).getName());


    }

    /**
     * 普通方法且无类型转换器
     */
    @Test
    public void testForCustomTypeHandlerWithoutSpecify() {
        // 执行准备工作
        @Cleanup
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 执行
        Mapper mapper = sqlSession.getMapper(Mapper.class);
        List<User> users = mapper.selectByGenderWithoutSpecify(EUserGender.MALE);
        Assertions.assertTrue(users.isEmpty());
    }

    /**
     * 测试普通方法触发全局类型转换器
     */
    @Test
    public void testForCustomTypeHandlerWithoutSpecifyNeedSuccess() {
        // 注册全局的类型转换器
        sqlSessionFactory.getConfiguration().getTypeHandlerRegistry().register(EUserGenderTypeHandler.class);

        // 执行准备工作
        @Cleanup
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 执行
        Mapper mapper = sqlSession.getMapper(Mapper.class);
        List<User> users = mapper.selectByGenderWithoutSpecify(EUserGender.MALE);
        Assertions.assertEquals("Panda", users.get(0).getName());
    }
}

毫无疑问,上面的单元测试都能正常运行,这也就意味着我们自定义的TypeHandler生效了,那么TypeHandler到底是一个怎样的东西呢?

类型转换器——TypeHandler

TypeHandlermyabtis中定义的负责jdbc类型和java类型互相转换的策略接口,我称之为类型转换器

TypeHandler接口有一个泛型参数定义,这个泛型参数就是要处理的java类型,该定义还约束了标准的类型转换器需要提供下列四种方法:

  • 根据参数索引和jdbc类型设置PreparedStatement语句中的值,即将?替换为实际值的setParameter(PreparedStatement,int,T,JdbcType).
  • 获取指定ResultSet中指定名称的列对应的值,并转换为相应的JAVA类型的getResult(ResultSet,String)
  • 获取指定ResultSet中指定索引下面的列对应的值,并转换为相应的JAVA类型的getResult(ResultSet,int)
  • 获取指定存储过程(CallableStatement)中指定索引下标对应的值,并转换为相应的JAVA类型的getResult(CallableStatement,int
/**
 * 类型转换处理器
 *
 * @author Clinton Begin
 */
public interface TypeHandler<T> {

    /**
     *  根据参数索引和jdbc类型设置PreparedStatement语句中的值,即将? 替换为实际值。
     *
     * @param ps        PreparedStatement 语句
     * @param i         参数索引
     * @param parameter 参数
     * @param jdbcType  jdbc类型
     * @throws SQLException SQL异常
     */
    void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

    /**
     * 获取指定ResultSet中指定名称的列对应的值,并转换为相应的JAVA类型
     *
     * @param rs         ResultSet
     * @param columnName 列名称
     * @return 值
     * @throws SQLException SQL异常
     */
    T getResult(ResultSet rs, String columnName) throws SQLException;

    /**
     * 获取指定ResultSet中指定索引下面的列对应的值,并转换为相应的JAVA类型
     *
     * @param rs          ResultSet
     * @param columnIndex 列索引
     * @return 值
     * @throws SQLException SQL异常
     */
    T getResult(ResultSet rs, int columnIndex) throws SQLException;

    /**
     * 获取指定存储过程(CallableStatement)中指定索引下标对应的值,并转换为相应的JAVA类型
     *
     * @param cs          存储过程
     * @param columnIndex 列索引
     * @return 值
     * @throws SQLException SQL异常
     */
    T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}

为了简化我们的操作,Mybatis定义了一个名为BaseTypeHandler的抽象类,实现了TypeHandler接口,他是Mybatis提供的一个默认的类型转换器的基类。

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

    /**
     * Mybatis配置
     *
     * @deprecated Since 3.5.0 - See https://github.com/mybatis/mybatis-3/issues/1203. This field will remove future.
     */
    @Deprecated
    protected Configuration configuration;

    /**
     * @deprecated Since 3.5.0 - See https://github.com/mybatis/mybatis-3/issues/1203. This property will remove future.
     */
    @Deprecated
    public void setConfiguration(Configuration c) {
        this.configuration = c;
    }

    /**
     * @param ps        PreparedStatement
     * @param i         参数索引
     * @param parameter 参数值
     * @param jdbcType  jdbc类型
     */
    @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;
}

BaseTypeHandler实现了TypeHandler接口定义的默认方法,添加了基本的数据校验,并暴露出了新的抽象方法供子类实现。

虽然他还添加了对Mybatis配置类Configuration的引用,并提供了对应的设值方法,但是这一功能已被弃用,所以这里就不在讨论。

获取泛型定义原始类型——TypeReference

除此之外,BaseTypeHandler还继承了TypeReference抽象类,提供了获取泛型定义原始类型的能力。

public abstract class TypeReference<T> {

    /**
     * TypeReference<T> 中T的泛型类型
     */
    private final Type rawType;

    protected TypeReference() {
        // 获取当前类的泛型类型
        rawType = getSuperclassTypeParameter(getClass());
    }
    // ... 省略 getSuperclassTypeParameter() 方法
}

TypeReference抽象类中提供了一个Type类型的rawType属性,并对外了暴露了该属性的getter方法,该属性的作用是保存类上泛型定义的类型,他的初始化工作是在TypeReference的构造方法中完成的。

有关于泛型解析相关的知识,我们在介绍TypeParameterResolver时已经做了很完整的分析,这里就不在赘述。

我们自己定义的EUserGenderTypeHandler就继承了BaseTypeHandler基类,并通过泛型参数指定了要处理的java类型是EUserGender

存储类型转换器的注册表——TypeHandlerRegistry

在简单的了解了TypeHandler之后,我们回过头来继续看TypeHandlerRegistry

Tips:
我想换一种方式来描述对象的功能和实现,之前描述一个类的源码时,都是站在程序员的角度去想,去写,按照方法调用栈来解析源代码。现在我想站在产品的角度来解析代码,
先给出实例存在的目的和宗旨,并围绕着这个目的和宗旨,逐步的扩展出代码。

TypeHandlerRegistry的作用

TypeHandlerRegistry作为存储类型转换器的注册表,他存在的目的就是为了保存所有类型转换器,并提供相应的注册获取类型转换器的功能。

编程是一件开放性很高的事情,一千个人眼中就有一千个哈姆雷特,一千个程序员对于同一个功能可能有不止一万种实现方案。

所以在接下来我们会根据需求去梳理TypeHandlerRegistry的实现,而不是去重新实现一个新的TypeHandlerRegistry

TypeHandlerRegistry的属性定义

首先我们看一下TypeHandlerRegistry中的属性定义:

/**
 * jdbc类型和处理器的映射关系
 */
private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<>(JdbcType.class);
/**
 * java类型和映射处理器的关系
 */
private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new ConcurrentHashMap<>();
/**
 * 未知类型的对应的默认处理器
 */
private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);
/**
 * 处理器类型和处理器实例的映射关系
 */
private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<>();

/**
 * 空的类型转换器映射关系,仅仅是一个标志性对象
 */
private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();

/**
 * 默认的枚举类型转换器
 */
private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;

仔细看前两个属性的定义,JDBC_TYPE_HANDLER_MAP用于存放JdbcTypeTypeHandler的对应关系。

TYPE_HANDLER_MAPkey值是一个java类型,valueMap<JdbcType, TypeHandler<?>>集合。

这两个属性的定义很容易让人误认为TYPE_HANDLER_MAPvalue存放是的指定java类型对应的JDBC_TYPE_HANDLER_MAP集合。

但是,事实上,在梳理源码后,我们会发现二者毫无关系

JDBC_TYPE_HANDLER_MAP的作用是:维护一个JdbcType和其默认TypeHandler之间的对应关系。

TYPE_HANDLER_MAP的作用是:维护java类型和其所有可转换的JdbcType的关系,并保留负责进行转换操作的TypeHandler实例。

换句话,TYPE_HANDLER_MAP集合中的value虽然类型也是Map<JdbcType, TypeHandler<?>>,但是他存储的JdbcTypeTypeHandler的对应关系可能会因为所属的java类型的不同而不同。

比如:

var TYPE_HANDLER_MAP={
 'Integer':{
 "JdbcType.INTEGER":"IntegerTypeHandler"
 },
 'Double':{
 "JdbcType.INTEGER":"DoubleTypeHandler"
 }
}

在上面这个通过JS描述的伪对象TYPE_HANDLER_MAP中,我们可以看到随着java类型由Integer变成Double,JdbcType.INTEGER对应的TypeHandler实例也由IntegerTypeHandler变成了DoubleTypeHandler

除了这个容易让人混淆的问题之外,还有一个问题困扰了我很久,按照前面的理解,一个jdbc类型应该是可以对应多个TypeHandler对象的,毕竟,一个jdbc类型可能会转换成不同的JAVA对象,可是按照JDBC_TYPE_HANDLER_MAP的定义,一个jdbc类型只能对应一个TypeHandler,这是为什么呢?

这是因为JDBC_TYPE_HANDLER_MAP负责存储的是JdbcType和其默认TypeHandler之间的对应关系,JDBC_TYPE_HANDLER_MAPTYPE_HANDLER_MAP的一个补充,当TYPE_HANDLER_MAP中无法获取TypeHandler时,就会考虑根据jdbc类型从JDBC_TYPE_HANDLER_MAP中获取一个默认的TypeHandler来完成类型转换的处理操作。

TypeHandlerRegistry还有其他几个属性,被final修饰的UNKNOWN_TYPE_HANDLER属性是一个UnknownTypeHandler类型的类型转换器。

UnknownTypeHandler虽然听起来像是用于处理未知类型,但是实际上在他的实现中,我们会发现他更偏向于一个代理对象,他会在运行期间,动态的去选择一个有效的TypeHandler实例来完成类型转换的工作。

类型为Map<JdbcType, TypeHandler<?>>NULL_TYPE_HANDLER_MAP属性和JDBC_TYPE_HANDLER_MAP也没有任何关系,他是一个标志性的对象,用来表示一个空的类型转换器映射关系。

类型为Map<Class<?>, TypeHandler<?>>ALL_TYPE_HANDLERS_MAP属性,他的keyTypeHandler实例的具体类型,他的value则是TypeHandler实例本身,他的作用是维护所有有效的TypeHandler

最后一个类型为Class<? extends TypeHandler>defaultEnumTypeHandler属性,则定义了默认的枚举类型转换器。

枚举类型转换器实际上是一个比较特殊的处理器,他有别于普通的处理器只用于处理特定的类,枚举类型转换器会处理任意继承了Enum的类。

注册类型转换器功能的实现

TypeHandlerRegistry的几个集合类型的属性已经为我们提供了保存类型转换器的功能,现在我们看一下TypeHandlerRegistry如何实现注册类型转换器的功能。

现在已经知道一个类型转换器核心的数据有:被处理的Java类型,被处理的JDBC类型以及类型转换器TypeHandler本身这三个核心要素,同时Java类型JDBC类型之间是一个多对多的关系。

那么就注册功能的实现来讲,最好的场景就是我们能够直接拿到Java类型JDBC类型以及相应的类型转换器TypeHandler实例这三个参数。

如果不能,那就退而求其次,获取类型转换器TypeHandler加上Java类型JDBC类型中的一个。

如果还不行,只有类型转换器TypeHandler也可以。

对于那些缺失的参数,我们的实现通常是想办法推导出它们,然后转为完美场景进行处理。

TypeHandlerRegistry在注册类型转换器的功能实现方面,提供了较多的register方法重载方法。

其中有一个方法签名为private void register(Type javaType, JdbcType jdbcType,TypeHandler<?> handler)的重载方法,用于处理三要素齐全的场景,完成了事实上的类型转换器的注册工作:

/**
 * 注册类型转换器
 *
 * @param javaType java类型
 * @param jdbcType jdbc类型
 * @param handler  类型转换器
 */
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
        // 获取该java类型对应的jdbc类型转换器的集合
        Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);

        if (map == null || map == NULL_TYPE_HANDLER_MAP) {
            map = new HashMap<>();
            // 注册java类型和【jdbc和处理器的映射关系】的映射关系
            TYPE_HANDLER_MAP.put(javaType, map);
        }
        
        map.put(jdbcType, handler);
    }
    // 注册处理器类型和实例的关系
    ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}

这是register重载方法中最核心的方法,多数register重载方法的调用,最终会落在该方法上来完成最终的处理操作。

他的实现比较简单,唯一值得注意的就是NULL_TYPE_HANDLER_MAP作为标识符的作用就体现在这里。

还有一些register重载方法的入参会缺失Java类型JDBC类型中的一个,甚至是二者全部缺失。

我们前面说过,当我们缺少某一参数时,会尽可能的去推导出缺少属性的参数类型,然后转为完美场景进行处理。

推导Java类型参数

在缺失java类型参数的场景下,TypeHandlerRegistry给出的解决方案是提供MappedTypes注解和TypeReference抽象父类。

MappedTypes注解可以直接提供TypeHandler对象可转换的java类型集合:

/**
 * 该注解用于给类型处理器指定可处理的JAVA类型
 *
 * @author Eduardo Macarron
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MappedTypes {
    /**
     * 可转换的JAVA类型集合
     */
    Class<?>[] value();
}

TypeReference抽象父类则可以获取其实现类的泛型参数定义。

具体的解析方法可以参考register(TypeHandler<T> typeHandler)方法:

public <T> void register(TypeHandler<T> typeHandler) {
    boolean mappedTypeFound = false;
    // 获取MappedTypes注解
    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 {
            TypeReference<T> typeReference = (TypeReference<T>) 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
        }
    }
    // 特殊的null类型
    if (!mappedTypeFound) {
        register((Class<T>) null, typeHandler);
    }
}

优先解析MappedTypes注解中指定的类型,其次是根据TypeReference获取泛型类型,如果还没能获取java类型,那就使用特殊的类型null


推导Jdbc类型参数

推导jdbc类型参数,TypeHandlerRegistry给出的解决方案是提供MappedJdbcTypes注解,和MappedTypes相似。

MappedJdbcTypes注解的作用是标注出指定TypeHandler可转换的jdbc类型集合:

/**
 * 给TypeHandler指定处理的JdbcType
 * @author Eduardo Macarron
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MappedJdbcTypes {
    // 可处理的jdbc类型集合
    JdbcType[] value();
    // 是否额外注册JDBC类型为null的处理器
    boolean includeNullJdbcType() default false;
}

MappedJdbcTypes除了可以指定jdbc类型集合之外,可有一个includeNullJdbcType()方法,用于指定是否额外注册Jdbc类型为null的处理器。

负责推导jdbc类型参数的方法是private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler):

/**
 * 注册java类型和类型转换出处理器的关系
 *
 * @param javaType    java类型
 * @param typeHandler 类型转换处理器实例
 * @param <T>         java类型
 */
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
    // 寻找mappedJdbcTypes注解,mappedJdbcTypes用于给TypeHandler指定处理的JdbcType
    MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
    if (mappedJdbcTypes != null) {
        for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
            // 注册java类型处理和jdbc类型以及类型转换处理器的关系
            register(javaType, handledJdbcType, typeHandler);
        }
        if (mappedJdbcTypes.includeNullJdbcType()) {
            // 注册JDBC类型为null的处理器
            register(javaType, null, typeHandler);
        }
    } else {
        // 注册JDBC类型为null的处理器
        register(javaType, null, typeHandler);
    }
}

上面的两个方法的实现上,分别

上面提到的三个register()方法,是TypeHandlerRegistry中比较核心的三个注册方法,这三个方法相互配合使用基本可以完成绝大多数注册场景。

还有一些其他的register()重载方法,这些重载方法中有一部分用于处理java类型的变种表现:

比如,将String类型描述的Java类型转换为实际的Java类型:

/**
 * 注册指定java类型的指定类型转换器
 *
 * @param javaTypeClassName    java类型名称
 * @param typeHandlerClassName 类型转换处理器名称
 */
public void register(String javaTypeClassName, String typeHandlerClassName) throws ClassNotFoundException {
    register(Resources.classForName(javaTypeClassName), Resources.classForName(typeHandlerClassName));
}

比如,获取TypeReference对应的泛型类型,然后进行注册:

/**
 * 根据泛型注册类型转换处理器
 *
 * @param javaTypeReference java泛型
 * @param handler           处理器
 * @param <T>               泛型
 */
public <T> void register(TypeReference<T> javaTypeReference, TypeHandler<? extends T> handler) {
    register(javaTypeReference.getRawType(), handler);
}

还有一部分register()方法用于处理特殊的场景,比如:

/**
 * 注册JDBC类型转换器
 *
 * @param jdbcType jdbc类型
 * @param handler  处理器
 */
public void register(JdbcType jdbcType, TypeHandler<?> handler) {
    // 注册jdbc类型转换器
    JDBC_TYPE_HANDLER_MAP.put(jdbcType, handler);
}

这个方法注册的是指定的Jdbc类型与其对应的默认转换器,和前面提到那些register()方法在实际意义上完全不同。

还有一个register()方法用于批量注册类型转换器:

// 注册指定包下所有的java类
public void register(String packageName) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    // 返回当前已经找到的所有TypeHandler的子类
    resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
    Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
    for (Class<?> type : handlerSet) {
        //Ignore inner classes and interfaces (including package-info.java) and abstract classes
        if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
            // 忽略匿名类、接口以及抽象类
            // 注册类型转换处理器
            register(type);
        }
    }
}

在这个方法中,他通过ResolverUtilsfind()方法来获取初步符合预期的类集合,之后排除掉匿名类、成员类和接口,对剩下的类执行别名注册的操作。

有关于更多ResolverUtil的信息,在解析TypeAliases元素时,已经给出。


获取类型转换器功能的实现

在实现上TypeHandlerRegistry获取类型转换器的方法大致可以分为两类,其中一类是用于判断指定的类型转换器是否存在的hasTypeHandler()方法,一类是实际获取类型转换器对象的getTypeHandler()方法。

这两类方法的实现逻辑大致是一致的,hasTypeHandler()方法仅仅是对getTypeHandler()方法的返回结果做了一层非空判断,并包装成Boolean值而已。

getTypeHandler()方法的核心重载实现是:

/**
 * 获取指定类型的处理器
 *
 * @param type     java类型
 * @param jdbcType jdbc类型
 * @param <T>      处理器类型
 */
@SuppressWarnings("unchecked")
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
    if (ParamMap.class.equals(type)) {
        return null;
    }
    /*
     * 获取所有指定类型的java处理器
     */
    Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
    TypeHandler<?> handler = null;
    if (jdbcHandlerMap != null) {
        // 获取对应的jdbc处理器
        handler = jdbcHandlerMap.get(jdbcType);
        if (handler == null) {
            handler = jdbcHandlerMap.get(null);
        }
        if (handler == null) {
            // 选择一个适合的惟一的处理器
            // #591
            handler = pickSoleHandler(jdbcHandlerMap);
        }
    }
    // 返回处理器
    // type drives generics here
    return (TypeHandler<T>) handler;
}

可以看到,方法实现比较简单,先获取指定java类型对应的已注册的jdbc类型及其转换处理器,之后根据jdbc类型参数获取转换器,
如果没有指定jdbc类型,则调用pickSoleHandler(jdbcHandlerMap)方法,尝试获取一个有效转换器:

/**
 * 从一组类型转换处理器中选择唯一的一个转换器,如果超过一个类型转换处理器,返回null。
 *
 * @param jdbcHandlerMap jdbc类型对应的类型转换处理器集合
 */
private TypeHandler<?> pickSoleHandler(Map<JdbcType, TypeHandler<?>> jdbcHandlerMap) {
    TypeHandler<?> soleHandler = null;
    for (TypeHandler<?> handler : jdbcHandlerMap.values()) {
        if (soleHandler == null) {
            soleHandler = handler;
        } else if (!handler.getClass().equals(soleHandler.getClass())) {
            // More than one type handlers registered.
            return null;
        }
    }
    return soleHandler;
}

pickSoleHandler(jdbcHandlerMap)方法对于有效转换器的定义比较简单,那就是返回java类型对应的唯一的类型转换器,这个唯一的类型转换器可以当做是默认的类型转换器来使用。

负责获取指定java类型对应的已注册jdbc类型及其转换处理器的方法getJdbcHandlerMap()在实现上兼容了枚举和子类的处理操作。

// 获取指定java类型对应所有类型转换处理器
private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMap(Type type) {
    // 获取当前java类型对应的类型转换器集合
    Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(type);

    if (NULL_TYPE_HANDLER_MAP.equals(jdbcHandlerMap)) {
        return null;
    }

    if (jdbcHandlerMap == null && type instanceof Class) {
        Class<?> clazz = (Class<?>) type;
        if (clazz.isEnum()) {
            // 根据枚举查找类型转换处理器
            jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(clazz, clazz);
            if (jdbcHandlerMap == null) {
                // 注册类型转换处理器
                register(clazz, getInstance(clazz, defaultEnumTypeHandler));
                // 返回类型转换处理器
                return TYPE_HANDLER_MAP.get(clazz);
            }
        } else {
            // 根据父类查找类型转换处理器
            jdbcHandlerMap = getJdbcHandlerMapForSuperclass(clazz);
        }
    }
    // 注册类型转换处理器
    TYPE_HANDLER_MAP.put(type, jdbcHandlerMap == null ? NULL_TYPE_HANDLER_MAP : jdbcHandlerMap);
    return jdbcHandlerMap;
}

枚举兼容

在处理枚举类型时,如果该枚举没有定义相关的类型转换器,将会递归调用getJdbcHandlerMapForEnumInterfaces()方法,尝试从实现的接口定义中获取类型转换器。

如果其接口也没有对应的类型转换器,那么将会使用TypeHandlerRegistrydefaultEnumTypeHandler属性对应的类型转换器作为当前枚举的类型处理器。

/**
 * 根据枚举接口获取类型转换器
 *
 * @param clazz     java类型
 * @param enumClazz 枚举类型
 * @return 类型转换处理器
 */
private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMapForEnumInterfaces(Class<?> clazz, Class<?> enumClazz) {
    for (Class<?> iface : clazz.getInterfaces()) {
        // 从指定java类的接口上找类型转换处理器
        Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(iface);

        if (jdbcHandlerMap == null) {
            // 没有对应的类型转换器,递归往上找
            jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(iface, enumClazz);
        }
        if (jdbcHandlerMap != null) {
            // 如果找到了类型转换器
            // Found a type handler regsiterd to a super interface
            HashMap<JdbcType, TypeHandler<?>> newMap = new HashMap<>();
            for (Entry<JdbcType, TypeHandler<?>> entry : jdbcHandlerMap.entrySet()) {
                // Create a type handler instance with enum type as a constructor arg
                // 创建一个对应该类的类型转换器
                newMap.put(entry.getKey(), getInstance(enumClazz, entry.getValue().getClass()));
            }
            return newMap;
        }
    }
    return null;
}

子类兼容

在处理普通类型时,如果该类型没有定义相关的类型转换器,将会递归调用getJdbcHandlerMapForSuperclass()方法,尝试从继承的父类定义中获取类型转换器。

private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMapForSuperclass(Class<?> clazz) {
    Class<?> superclass = clazz.getSuperclass();
    if (superclass == null || Object.class.equals(superclass)) {
        return null;
    }
    Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(superclass);
    if (jdbcHandlerMap != null) {
        return jdbcHandlerMap;
    } else {
        return getJdbcHandlerMapForSuperclass(superclass);
    }
}

无论是针对普通类还是枚举,在查找到新的类型转换器关系之后,都会重新注册到TypeHandlerRegistry中。

我思考了很久,为什么在做子类兼容时,只递归查找父类对应的类型转换器集合,而不是连父接口对应的类型转换器一并查找?

先说结论:因为没必要。

首先,我们要明确一点:接口,是抽象方法的集合,方法定义的是一个对象的行为,所以接口定义的是一类对象的行为,而不是特性。

被类型转换器处理的java类型通常需要具有一定程度上的一致性和可预见性,比如,给定Jdbc数值A,可以获取java对象a,那么理论上可预见的是:通过java对象a可以获得Jdbc数值A

这种特性意味着,被类型转换器处理的java类型通常具有简单性,不会是一个复杂的业务对象,这时候强调的往往是该对象的某一特性,而不是行为,这时候,这种对象通常会被直接定义为类而不是接口。

那么枚举对象为什么适配的是接口而不是类呢?

枚举对象也具有简单性,但是枚举默认继承了Enum父类,所以只能实现接口,没有办法直接定义属性,所以退而求其次,通过方法来间接的限制属性。

getTypeHandler()方法还有一些其他重载实现,逻辑大致与register()方法重载实现一致,这里就不做讨论了。

到这里,TypeHandlerRegistry就完成了保存所有类型转换器,以及相应的注册获取类型转换器的功能的实现。

其他

TypeHandlerRegistry中还有一些其他方法定义,比如defaultEnumTypeHandler属性的getter/setter方法和UNKNOWN_TYPE_HANDLER属性的getter方法,以及通过反射获取TypeHandler对象实例的getInstance方法。

这些方法的实现比较简单,这里就不在赘述了。


解析typeHandlers元素

现在我们回到typeHandlers元素的解析工作中。

在Mybatis中关于typeHandlersDTD是如此定义的:

<!ELEMENT typeHandlers (typeHandler*,package*)>

typeHandlers下面可以出现零个或多个typeHandler或者package标签。

typeHandler元素用于注册单个TypeHandler实例,它有三个属性javaType,jdbcType以及handler,其中javaTypejdbcType是可选的,他们分别表示java类型和jdbc类型,handler属性是必填的,他表示类型转换器的类型,这三个属性都可以使用类型别名。

<!ELEMENT typeHandler EMPTY>
<!ATTLIST typeHandler
javaType CDATA #IMPLIED
jdbcType CDATA #IMPLIED
handler CDATA #REQUIRED
>

package元素用于批量注册TypeHandler实例,它只有一个必填的name属性,他表示用户需要注册TypeHandler的基础包名,Mybatis将会递归处理该基础包及其子包下所有可用的TypeHandler实例。

<!ELEMENT package EMPTY>
<!ATTLIST package
name CDATA #REQUIRED
>

调用解析的入口(XmlConfigBuilder):

private void parseConfiguration(XNode root) {
    // ...
    // 注册类型转换器
    typeHandlerElement(root.evalNode("typeHandlers"));
   // ...
}

解析并注册TypeHandler实例:

// 解析typeHandlers元素
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 {
                // 单个注册
                // 获取java类型的名称
                String javaTypeName = child.getStringAttribute("javaType");
                // 获取jdbc类型的名称
                String jdbcTypeName = child.getStringAttribute("jdbcType");
                // 获取类型转换处理器的名称
                String handlerTypeName = child.getStringAttribute("handler");
                // 解析出java类型
                Class<?> javaTypeClass = resolveClass(javaTypeName);
                // 解析jdbc类型
                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实例的解析注册也分为两种:

一类是解析typeHandler元素,尝试获取注册TypeHandler实例的三要素完成注册工作。

一类是解析package元素,获取用于批量注册的包名,调用TypeHandlerRegistryregister()方法完成整包批量注册。

关于具体注册方法的实现,前面已经有了详细的阐述。

至此,整个typeHandlers元素的解析也已经完成,Mybatis的基础准备工作也准备的差不多了。

接下来就是解析MybatisMapper配置文件。

关注我,一起学习更多知识

关注我
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容