MyBatis类型处理模块详解

深入理解MyBatis类型转换机制,掌握Java与JDBC之间的桥梁

一、MyBatis整体架构与类型处理模块

在深入类型处理模块之前,我们先了解MyBatis的整体架构,以及类型处理模块在其中的重要地位。

从架构图可以看出,MyBatis采用了分层架构设计,而类型处理模块(TypeHandler)位于基础支撑层,是连接Java类型与JDBC类型的桥梁。它负责在SQL执行过程中,将Java对象参数转换为JDBC能够识别的参数类型,以及将ResultSet中的数据转换为Java对象。

型处理模块的核心职责

类型处理模块主要承担以下核心职责:

sql 复制代码
✅ Java类型与JDBC类型相互转换 - 实现双向类型转换
✅ 参数设置时的类型转换 - 将Java参数转换为JDBC参数
✅ 结果集获取时的类型转换 - 将JDBC结果转换为Java对象
✅ Null值处理 - 处理Java空值与JDBC NULL的转换
✅ 类型注册与管理 - 维护Java类型与TypeHandler的映射关系

为什么需要TypeHandler?

在Java应用程序和数据库之间,存在着类型系统的差异:

Java类型 JDBC类型 说明
String VARCHAR, CHAR 字符串类型
Integer INTEGER 整数类型
Long BIGINT 长整数类型
Date TIMESTAMP 日期时间类型
BigDecimal NUMERIC 精确数值类型

TypeHandler的核心作用就是解决这些类型之间的映射和转换问题。

二、TypeHandler体系架构

TypeHandler采用了泛型接口设计,支持各种类型的扩展。

TypeHandler接口

TypeHandler是类型转换的核心接口,定义了类型转换的基本方法:

java 复制代码
public interface TypeHandler<T> {
    // 设置PreparedStatement参数(Java → JDBC)
    void setParameter(PreparedStatement ps, int i, 
                     T parameter, JdbcType jdbcType) 
                     throws SQLException;

    // 获取ResultSet结果(JDBC → Java,按列名)
    T getResult(ResultSet rs, String columnName) 
               throws SQLException;

    // 获取ResultSet结果(JDBC → Java,按列索引)
    T getResult(ResultSet rs, int columnIndex) 
               throws SQLException;

    // 获取CallableStatement结果(存储过程)
    T getResult(CallableStatement cs, int columnIndex) 
               throws SQLException;
}

BaseTypeHandler抽象类

为了简化TypeHandler的实现,MyBatis提供了BaseTypeHandler抽象类,它帮我们处理了Null值检查等通用逻辑:

java 复制代码
public abstract class BaseTypeHandler<T> 
    implements TypeHandler<T> {

    @Override
    public void setParameter(PreparedStatement ps, int i, 
                            T parameter, JdbcType jdbcType) 
                            throws SQLException {
        if (parameter == null) {
            // 处理Null值
            ps.setNull(i, jdbcType.TYPE_CODE);
        } else {
            // 设置非空参数,由子类实现
            setNonNullParameter(ps, i, parameter, jdbcType);
        }
    }

    @Override
    public T getResult(ResultSet rs, String columnName) 
                      throws SQLException {
        T result = getNullableResult(rs, columnName);
        // 检查是否为null
        return rs.wasNull() ? null : result;
    }

    // 抽象方法,由子类实现
    protected abstract void setNonNullParameter(
        PreparedStatement ps, int i, 
        T parameter, JdbcType jdbcType) 
        throws SQLException;

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

TypeHandler继承体系

三、类型转换双向流程

TypeHandler实现了Java类型与JDBC类型之间的双向转换。

Java → JDBC 转换流程

当执行SQL语句时,需要将Java参数转换为JDBC参数:

ini 复制代码
// 1. ParameterHandler获取参数值
Object value = getParameterValue(
    parameterObject, parameterMapping);

// 2. 获取对应的TypeHandler
TypeHandler<?> typeHandler = 
    parameterMapping.getTypeHandler();

// 3. 调用setParameter设置参数
typeHandler.setParameter(ps, i + 1, value, jdbcType);

JDBC → Java 转换流程

当从ResultSet获取结果时,需要将JDBC数据转换为Java对象:

scss 复制代码
// 1. ResultSetHandler处理ResultSet
while (resultSet.next()) {
    // 2. 根据ResultMapping获取TypeHandler
    TypeHandler<?> typeHandler = 
        resultMapping.getTypeHandler();

    // 3. 调用getResult获取结果
    Object value = typeHandler.getResult(
        resultSet, column);

    // 4. 设置到目标对象
    metaObject.setValue(property, value);
}

四、参数设置时的类型转换

参数设置是Java类型向JDBC类型转换的过程。

基本类型转换示例

java 复制代码
public class StringTypeHandler 
    extends BaseTypeHandler<String> {

    @Override
    public void setNonNullParameter(
        PreparedStatement ps, int i, 
        String parameter, JdbcType jdbcType) 
        throws SQLException {
        // String → VARCHAR
        ps.setString(i, parameter);
    }

    @Override
    public String getNullableResult(
        ResultSet rs, String columnName) 
        throws SQLException {
        // VARCHAR → String
        return rs.getString(columnName);
    }
}
public class LongTypeHandler 
    extends BaseTypeHandler<Long> {

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

    @Override
    public Long getNullableResult(
        ResultSet rs, String columnName) 
        throws SQLException {
        // BIGINT → Long
        long value = rs.getLong(columnName);
        // ⚠️ 关键:检查是否为NULL
        return value == 0 && rs.wasNull() ? null : value;
    }
}
public class DateTypeHandler 
    extends BaseTypeHandler<Date> {

    @Override
    public void setNonNullParameter(
        PreparedStatement ps, int i, 
        Date parameter, JdbcType jdbcType) 
        throws SQLException {
        // Date → Timestamp
        ps.setTimestamp(i, 
            new Timestamp(parameter.getTime()));
    }

    @Override
    public Date getNullableResult(
        ResultSet rs, String columnName) 
        throws SQLException {
        // Timestamp → Date
        Timestamp timestamp = rs.getTimestamp(columnName);
        return timestamp == null ? null : 
               new Date(timestamp.getTime());
    }
}

Null值处理

Null值处理是参数设置中的重要环节:

xml 复制代码
<!-- 场景1:明确的jdbcType -->
<insert id="insertUser">
    INSERT INTO t_user (name, email)
    VALUES (
        #{name, jdbcType=VARCHAR}, 
        #{email, jdbcType=VARCHAR}
    )
</insert>

<!-- 场景2:全局配置Null的jdbcType -->
<settings>
    <setting name="jdbcTypeForNull" value="NULL"/>
</settings>

重要提示:未指定jdbcType时,传入null参数会抛出异常!

五、结果集获取时的类型转换

结果集获取是JDBC类型向Java类型转换的过程。

ResultSet结果获取流程

scss 复制代码
// 遍历ResultSet的完整流程
while (resultSet.next()) {
    for (ResultMapping mapping : 
         resultMap.getPropertyResultMappings()) {

        // 获取列名
        String column = mapping.getColumn();

        // 获取TypeHandler
        TypeHandler<?> handler = 
            mapping.getTypeHandler();

        // 获取结果值
        Object value = handler.getResult(
            resultSet, column);

        // 设置到目标对象
        metaObject.setValue(
            mapping.getProperty(), value);
    }
}

三种getResult方式

TypeHandler提供了三种获取结果的方式:

arduino 复制代码
@Override
public String getNullableResult(
    ResultSet rs, String columnName) 
    throws SQLException {
    return rs.getString(columnName);
}

使用场景:

java 复制代码
<result column="user_name" property="userName"/>
@Override
public String getNullableResult(
    ResultSet rs, int columnIndex) 
    throws SQLException {
    return rs.getString(columnIndex);
}

使用场景:自动映射或需要按索引获取时

java 复制代码
@Override
public String getNullableResult(
    CallableStatement cs, int columnIndex) 
    throws SQLException {
    return cs.getString(columnIndex);
}

使用场景:

sql 复制代码
<select id="callProcedure" statementType="CALLABLE">
    {call get_user_info(
        #{userId, mode=IN, jdbcType=BIGINT},
        #{userName, mode=OUT, jdbcType=VARCHAR}
    )}
</select>

wasNull()的重要性

在类型转换中,wasNull()方法非常关键:

java 复制代码
@Override
public Long getNullableResult(
    ResultSet rs, String columnName) 
    throws SQLException {

    long value = rs.getLong(columnName);
    // ⚠️ 如果数据库是NULL,getLong()返回0

    // ✅ 必须检查wasNull()区分真实的0和NULL
    return value == 0 && rs.wasNull() ? null : value;
}

六、TypeHandler注册机制

TypeHandlerRegistry负责管理所有TypeHandler的注册和查找。

TypeHandlerRegistry结构

arduino 复制代码
public class TypeHandlerRegistry {
    // JDBC类型 → TypeHandler
    private final Map<JdbcType, TypeHandler<?>> 
        jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);

    // Java类型 → (JDBC类型 → TypeHandler)
    private final Map<Type, Map<JdbcType, TypeHandler<?>>> 
        typeHandlerMap = new HashMap<>();

    // Java类型 → 默认TypeHandler
    private final Map<Class<?>, TypeHandler<?>> 
        allTypeHandlersMap = new HashMap<>();

    public TypeHandlerRegistry(){
        // 注册所有内置TypeHandler
        register(Boolean.class, new BooleanTypeHandler());
        register(Integer.class, new IntegerTypeHandler());
        register(Long.class, new LongTypeHandler());
        register(String.class, new StringTypeHandler());
        register(Date.class, new DateTypeHandler());
        // ... 更多注册
    }
}

注册TypeHandler的四种方式

typescript 复制代码
public <T> void register(
    Class<T> javaType, 
    TypeHandler<? extends T> typeHandler) {
    register((Type) javaType, typeHandler);
}
public <T> void register(
    Class<T> type, 
    JdbcType jdbcType, 
    TypeHandler<? extends T> typeHandler) {
    register((Type) type, jdbcType, typeHandler);
}
public <T> void register(
    TypeReference<T> javaTypeReference, 
    TypeHandler<? extends T> typeHandler) {
    register(javaTypeReference.getRawType(), 
             typeHandler);
}
public void register(String packageName) {
    ResolverUtil<Class<?>> resolverUtil = 
        new ResolverUtil<>();
    resolverUtil.find(
        new ResolverUtil.IsA(TypeHandler.class), 
        packageName);

    Set<Class<? extends Class<?>>> handlerSet = 
        resolverUtil.getClasses();

    for (Class<?> type : handlerSet) {
        if (!type.isAnonymousClass() && 
            !type.isInterface() && 
            !Modifier.isAbstract(type.getModifiers())) {
            register(type);
        }
    }
}

查找TypeHandler

typescript 复制代码
// 根据Java类型和JDBC类型查找
public <T> TypeHandler<T> getTypeHandler(
    Class<T> type, JdbcType jdbcType) {

    // 1. 从Java类型映射中查找
    Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = 
        typeHandlerMap.get(type);

    if (jdbcHandlerMap != null) {
        TypeHandler<?> handler = 
            jdbcHandlerMap.get(jdbcType);
        if (handler != null) {
            return (TypeHandler<T>) handler;
        }
    }

    // 2. 查找默认TypeHandler
    return (TypeHandler<T>) getTypeHandler((Type) type);
}

七、自定义TypeHandler

当内置TypeHandler无法满足需求时,可以自定义TypeHandler。

实现TypeHandler接口

下面是一个JSON类型处理器的完整示例:

java 复制代码
@MappedTypes(List.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JsonListTypeHandler 
    extends BaseTypeHandler<List<String>> {

    private static final Gson GSON = new Gson();
    private static final Type LIST_TYPE = 
        new TypeToken<List<String>>(){}.getType();

    @Override
    public void setNonNullParameter(
        PreparedStatement ps, int i, 
        List<String> parameter, JdbcType jdbcType) 
        throws SQLException {
        // List<String> → JSON字符串
        String json = GSON.toJson(parameter);
        ps.setString(i, json);
    }

    @Override
    public List<String> getNullableResult(
        ResultSet rs, String columnName) 
        throws SQLException {
        // JSON字符串 → List<String>
        String json = rs.getString(columnName);
        return parseJson(json);
    }

    @Override
    public List<String> getNullableResult(
        ResultSet rs, int columnIndex) 
        throws SQLException {
        String json = rs.getString(columnIndex);
        return parseJson(json);
    }

    @Override
    public List<String> getNullableResult(
        CallableStatement cs, int columnIndex) 
        throws SQLException {
        String json = cs.getString(columnIndex);
        return parseJson(json);
    }

    private List<String> parseJson(String json) {
        if (json == null || json.trim().isEmpty()) {
            return null;
        }
        return GSON.fromJson(json, LIST_TYPE);
    }
}

枚举类型处理

MyBatis提供了两种枚举类型处理器:

scala 复制代码
public class EnumTypeHandler<E extends Enum<E>> 
    extends BaseTypeHandler<E> {

    private final Class<E> type;

    @Override
    public void setNonNullParameter(
        PreparedStatement ps, int i, 
        E parameter, JdbcType jdbcType) 
        throws SQLException {
        // Enum → name (VARCHAR)
        ps.setString(i, parameter.name());
    }

    @Override
    public E getNullableResult(
        ResultSet rs, String columnName) 
        throws SQLException {
        // name → Enum
        String name = rs.getString(columnName);
        return name == null ? null : 
               Enum.valueOf(type, name);
    }
}
public class EnumOrdinalTypeHandler<E extends Enum<E>> 
    extends BaseTypeHandler<E> {

    private final Class<E> type;

    @Override
    public void setNonNullParameter(
        PreparedStatement ps, int i, 
        E parameter, JdbcType jdbcType) 
        throws SQLException {
        // Enum → ordinal (INTEGER)
        ps.setInt(i, parameter.ordinal());
    }

    @Override
    public E getNullableResult(
        ResultSet rs, String columnName) 
        throws SQLException {
        // ordinal → Enum
        int ordinal = rs.getInt(columnName);
        if (ordinal == 0 && rs.wasNull()) {
            return null;
        }
        E[] enums = type.getEnumConstants();
        return enums[ordinal];
    }
}

注册自定义TypeHandler的三种方式

xml 复制代码
@MappedTypes(List.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JsonListTypeHandler 
    extends BaseTypeHandler<List<String>> {
    // 实现代码...
}
<typeHandlers>
    <!-- 指定类名 -->
    <typeHandler 
        handler="com.example.JsonListTypeHandler"/>

    <!-- 指定Java类型和JDBC类型 -->
    <typeHandler 
        javaType="java.util.List"
        jdbcType="VARCHAR"
        handler="com.example.JsonListTypeHandler"/>

    <!-- 包扫描 -->
    <package name="com.example.typehandler"/>
</typeHandlers>
@Configuration
public class MyBatisConfig {
    @Bean
    public ConfigurationCustomizer 
        configurationCustomizer() {
        return configuration -> {
            TypeHandlerRegistry registry = 
                configuration.getTypeHandlerRegistry();
            registry.register(
                List.class, 
                JdbcType.VARCHAR, 
                new JsonListTypeHandler()
            );
        };
    }
}

八、最佳实践

问题1:类型转换异常

sql 复制代码
// 问题:Cannot convert value '0000' from column 1 to TIMESTAMP
// 原因:日期格式不匹配
// 解决:使用正确的日期类型或自定义TypeHandler

问题2:Null值处理

csharp 复制代码
// 问题:JDBC requires that the JdbcType must be specified
// 解决1:指定jdbcType
#{name, jdbcType=VARCHAR}

// 解决2:全局配置
<setting name="jdbcTypeForNull" value="NULL"/>

问题3:枚举类型处理

ini 复制代码
// 配置默认枚举处理器
<settings>
    <setting name="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumTypeHandler"/>
</settings>

// 或在ResultMap中指定
<result column="status" property="status"
         typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>

九、总结

MyBatis的类型处理模块是整个框架的基础组件,通过精心设计的TypeHandler体系,实现了Java类型与JDBC类型的无缝转换。

相关推荐
金牌归来发现妻女流落街头2 小时前
【线程池 + Socket 服务器】
java·运维·服务器·多线程
wanghowie2 小时前
01.03 Spring核心|事务管理实战
java·后端·spring
Chen不旧2 小时前
Java模拟死锁
java·开发语言·synchronized·reentrantlock·死锁
千寻技术帮2 小时前
10356_基于Springboot的老年人管理系统
java·spring boot·后端·vue·老年人
最贪吃的虎2 小时前
Redis 除了缓存,还能干什么?
java·数据库·redis·后端·缓存
崎岖Qiu2 小时前
【设计模式笔记24】:JDK源码分析-Comparator中的「策略模式」
java·笔记·设计模式·jdk·策略模式
萧曵 丶2 小时前
Java 安全的单例模式详解
java·开发语言·单例模式
Qiuner2 小时前
Spring Boot 全局异常处理策略设计(一):异常不只是 try-catch
java·spring boot·后端
强子感冒了2 小时前
Java List学习笔记:ArrayList与LinkedList的实现源码分析
java·笔记·学习