← 返回题目列表

MyBatis 的 TypeHandler 是什么?如何自定义类型转换?

中等 第 17 / 24 题 更新于 2026/07/26
TypeHandler类型转换枚举自定义

简化版

TypeHandler(类型处理器)是 MyBatis 负责 Java 类型和 JDBC 类型互相转换的组件。数据库里存的是 JDBC 类型(VARCHARINTTIMESTAMP),Java 里是 StringIntegerLocalDateTime——两者之间的转换全靠 TypeHandler:设置参数时#{})把 Java 值转成 JDBC 类型(setString/setInt),读取结果时把 JDBC 值转成 Java 类型(getString/getInt)。MyBatis 内置了一堆常用类型的 TypeHandler。当你有特殊转换需求(如枚举存成 code、把 JSON 字符串转成对象、加解密字段)时,就自定义 TypeHandler——实现 TypeHandler 接口(或继承 BaseTypeHandler)注册进去。

详细版

TypeHandler 的职责(双向转换):

Java 参数 → 数据库(写):#{name} 时,TypeHandler 把 Java 值 set 进 PreparedStatement
数据库 → Java 对象(读):查询结果,TypeHandler 从 ResultSet 把列值 get 成 Java 类型

内置 TypeHandler 举例:
  StringTypeHandler:String ↔ VARCHAR
  IntegerTypeHandler:Integer ↔ INT
  LocalDateTimeTypeHandler:LocalDateTime ↔ TIMESTAMP
  EnumTypeHandler:枚举 ↔ 枚举名字符串(默认按 name())

自定义 TypeHandler(以「枚举存 code」为例):

// 需求:Status 枚举在数据库存 int code(1=启用, 0=禁用),而非枚举名
public enum Status {
    ENABLED(1), DISABLED(0);
    private final int code;
    Status(int code) { this.code = code; }
    public int getCode() { return code; }
}

// 自定义 TypeHandler,继承 BaseTypeHandler
@MappedTypes(Status.class)              // 处理的 Java 类型
@MappedJdbcTypes(JdbcType.INTEGER)      // 对应的 JDBC 类型
public class StatusTypeHandler extends BaseTypeHandler<Status> {
    // 写:Java 枚举 → 数据库 int
    public void setNonNullParameter(PreparedStatement ps, int i, Status s, JdbcType jt) throws SQLException {
        ps.setInt(i, s.getCode());       // 存 code
    }
    // 读:数据库 int → Java 枚举
    public Status getNullableResult(ResultSet rs, String col) throws SQLException {
        int code = rs.getInt(col);
        return codeToStatus(code);       // 按 code 反查枚举
    }
    // 还有两个 getNullableResult 重载(按列索引、CallableStatement)
}

注册方式

<!-- mybatis-config.xml -->
<typeHandlers>
    <typeHandler handler="com.example.StatusTypeHandler"/>
</typeHandlers>
# Spring Boot:application.yml 指定扫描包
mybatis:
  type-handlers-package: com.example.handler

⚠️ MyBatis 内置的 EnumTypeHandler 默认把枚举存成枚举名字符串ENABLED/DISABLED),而 EnumOrdinalTypeHandler序数 ordinal(0/1)。别用 EnumOrdinalTypeHandler——它按枚举声明顺序存序数,一旦调整枚举顺序,数据库里的老数据全错位(和「别用 ordinal 做持久化」是同一个坑)。要存自定义 code 就自定义 TypeHandler,别依赖 ordinal。

完整版教学

一、TypeHandler 解决什么:类型的「翻译官」

数据库世界和 Java 世界的类型是两套体系:数据库有 VARCHARINTDATEBLOB(JDBC 类型),Java 有 StringIntegerLocalDatebyte[]。它们不能直接互通——从数据库读出来的是 JDBC 类型,要转成 Java 类型才能用;往数据库写时反过来。TypeHandler 就是这个「翻译官」

写数据(Java → 数据库):
  userMapper.insert(user)  →  #{user.createTime}(LocalDateTime)
  → TypeHandler 调 ps.setTimestamp() 转成 JDBC TIMESTAMP 存进去

读数据(数据库 → Java):
  查出来的 create_time 列是 JDBC TIMESTAMP
  → TypeHandler 调 rs.getTimestamp() 转成 Java LocalDateTime 填进对象

关键认知:每次 #{} 参数设置、每次结果列映射,背后都有一个 TypeHandler 在做类型转换。你平时感觉不到它,是因为 MyBatis 内置了几十个常用类型的 TypeHandler,自动处理了 String、数字、日期、布尔等常见转换。只有当默认转换不满足需求时,你才需要自定义。理解「TypeHandler 是类型转换的底层机制」,就理解了 MyBatis 怎么把「一行数据」变成「一个 Java 对象」。

二、TypeHandler 的四个方法:读写双向

TypeHandler 接口(通常继承 BaseTypeHandler)有四个核心方法,覆盖「写一次、读三种情况」:

public abstract class BaseTypeHandler<T> implements TypeHandler<T> {
    // 写:设置参数(Java → JDBC)
    void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType);

    // 读:从 ResultSet 按列名取(JDBC → Java)
    T getNullableResult(ResultSet rs, String columnName);
    // 读:从 ResultSet 按列索引取
    T getNullableResult(ResultSet rs, int columnIndex);
    // 读:从 CallableStatement 取(存储过程)
    T getNullableResult(CallableStatement cs, int columnIndex);
}

设计逻辑:写只有一种(往 PreparedStatement 设参数),读有三种来源(ResultSet 按列名、ResultSet 按列索引、CallableStatement 存储过程)。BaseTypeHandler 已经处理了 null 判断(setParameter 里判 null、getResult 返回 null),你只需实现「非 null 情况下怎么转」。所以自定义时重点是:写的时候 Java 值怎么 set 进 JDBC、读的时候 JDBC 值怎么 get 成 Java——这就是转换逻辑的核心。

三、内置 TypeHandler 与自动匹配

MyBatis 内置了大量 TypeHandler,覆盖常见类型,自动按「Java 类型 + JDBC 类型」匹配

你的实体字段          数据库列类型        自动选用的 TypeHandler
String name          VARCHAR            StringTypeHandler
Integer age          INT                IntegerTypeHandler
LocalDateTime time   TIMESTAMP          LocalDateTimeTypeHandler
Boolean active       BIT/TINYINT        BooleanTypeHandler
BigDecimal amount    DECIMAL            BigDecimalTypeHandler

MyBatis 维护一个 TypeHandlerRegistry(注册表),根据「Java 类型和 JDBC 类型」查找对应的 TypeHandler。所以大多数标准类型你什么都不用做——#{} 和结果映射会自动选对 TypeHandler。只有以下情况需要自定义:① 数据库存的格式和 Java 类型不直接对应(枚举存 code、状态存中文);② 复杂类型转换(JSON 字符串 ↔ 对象、逗号分隔字符串 ↔ List);③ 字段级加解密(存密文、读明文)。这些「非标准转换」就是 TypeHandler 的用武之地。

四、经典场景一:枚举的正确存储

枚举存储是 TypeHandler 最经典的应用,也最容易踩坑。MyBatis 有两个内置枚举 TypeHandler:

EnumTypeHandler(默认):存枚举的 name()
  Status.ENABLED → 数据库存 "ENABLED"(字符串)
  优点:直观、改枚举顺序不影响
  缺点:占空间、和数据库常见的 int code 约定不符

EnumOrdinalTypeHandler:存枚举的 ordinal()(序数 0,1,2...)
  Status.ENABLED → 数据库存 0(int)
  ★ 危险:ordinal 绑定声明顺序,中间插入一个枚举值,老数据全错位!

生产实践几乎都是「枚举存自定义 code」(如 1=启用, 0=禁用),既省空间又语义稳定。做法就是自定义 TypeHandler(如上面的 StatusTypeHandler)——写时存 getCode()、读时按 code 反查枚举。千万别用 EnumOrdinalTypeHandler——它按声明顺序存序数,和「别用 ordinal 做持久化」是同一个坑:调整枚举顺序,数据库里的历史数据就全部对应错枚举了。这是枚举存储的核心考点。

五、经典场景二、三:JSON 字段与加解密

另外两个高频自定义场景:

// 场景二:把一个 List/对象字段,以 JSON 字符串存进一个 VARCHAR 列
@MappedTypes(List.class)
public class JsonListTypeHandler extends BaseTypeHandler<List<String>> {
    public void setNonNullParameter(PreparedStatement ps, int i, List<String> list, JdbcType jt) {
        ps.setString(i, JSON.toJSONString(list));   // List → JSON 字符串存
    }
    public List<String> getNullableResult(ResultSet rs, String col) {
        return JSON.parseArray(rs.getString(col), String.class);  // JSON → List
    }
}

// 场景三:字段级加解密(数据库存密文,Java 里是明文)
public class EncryptTypeHandler extends BaseTypeHandler<String> {
    public void setNonNullParameter(PreparedStatement ps, int i, String plain, JdbcType jt) {
        ps.setString(i, encrypt(plain));   // 明文加密后存
    }
    public String getNullableResult(ResultSet rs, String col) {
        return decrypt(rs.getString(col)); // 密文解密后返回明文
    }
}

这两个场景体现 TypeHandler 的价值——把「转换逻辑」收敛到一处,业务代码无感。JSON 字段:实体里就是 List<String>,存进去自动变 JSON、读出来自动变回 List,业务不用手动序列化。字段加解密:实体里是明文,存进数据库自动加密、查出来自动解密,业务代码完全不知道底层加密的存在(如手机号、身份证号的隐私字段加密)。这种「透明转换」是 TypeHandler 最优雅的用法。

六、注册方式与作用范围

自定义 TypeHandler 后要注册,有全局和局部两种方式:

全局注册(对该类型的所有映射生效):
  XML:<typeHandlers><typeHandler handler="..."/></typeHandlers>
  Spring Boot:mybatis.type-handlers-package: com.example.handler(扫包)
  → 之后所有 Status 类型的 #{} 和结果映射都自动用你的 TypeHandler

局部指定(只对某个字段/参数生效):
  #{status, typeHandler=com.example.StatusTypeHandler}
  <result column="status" property="status" typeHandler="..."/>
  → 只有这一处用指定的 TypeHandler

一般用全局注册(一劳永逸,该类型统一转换)。局部指定用于「同一个 Java 类型在不同地方要不同转换」的特殊情况。注册时用 @MappedTypes(指定 Java 类型)和 @MappedJdbcTypes(指定 JDBC 类型)帮助 MyBatis 精确匹配。理解注册的作用范围,就能控制「哪些字段用自定义转换、哪些用默认」。

记忆钩子:「TypeHandler 是 Java 类型↔JDBC 类型的翻译官:写时 set 参数、读时 get 结果,四个方法(一写三读);内置覆盖标准类型自动匹配;自定义用于枚举存 code(别用 EnumOrdinalTypeHandler,ordinal 会错位)、JSON 字段、字段加解密;全局注册或 #{} 局部指定」

七、常见误区与追问

  • 误区:TypeHandler 只在读结果时工作。 双向——写参数时(#{})把 Java 转 JDBC、读结果时把 JDBC 转 Java,四个方法覆盖「一写三读」。
  • 误区:枚举存储用 EnumOrdinalTypeHandler 最省空间。 危险——它存 ordinal(声明序数),调整枚举顺序会让老数据全错位;要存 int 应自定义 TypeHandler 存稳定的 code。
  • 误区:所有类型转换都要自定义 TypeHandler。 标准类型(String/数字/日期/布尔)MyBatis 内置自动处理;只有枚举存 code、JSON 字段、加解密等非标准转换才需自定义。
  • 误区:TypeHandler 处理不了复杂对象。 能——把复杂对象序列化成 JSON 字符串存进一个列(读时反序列化),就是常见的 JSON TypeHandler 用法。
  • 追问:MyBatis 默认怎么存枚举? 默认 EnumTypeHandler 存枚举的 name()(字符串);EnumOrdinalTypeHandler 存 ordinal(不推荐);要存自定义 code 需自定义 TypeHandler。
  • 追问:字段加解密怎么做到业务无感? 自定义 TypeHandler——写时在 setNonNullParameter 里加密、读时在 getNullableResult 里解密,实体字段始终是明文,业务代码不知道底层加密。
  • 追问:TypeHandler 是在 MyBatis 执行流程的哪一步工作? 写:StatementHandler/ParameterHandler 设置参数时;读:ResultSetHandler 处理结果集、把列值映射到对象属性时——都通过 TypeHandler 做类型转换。

八、加强记忆

TypeHandler 是 MyBatis 里 Java 类型 ↔ JDBC 类型的「翻译官」——数据库存 JDBC 类型(VARCHAR/INT/TIMESTAMP)、Java 用 String/Integer/LocalDateTime,两者的转换全靠它:写数据时#{})把 Java 值 set 进 PreparedStatement(Java→JDBC),读数据时把 ResultSet 列值 get 成 Java 类型(JDBC→Java),接口有四个方法(一写三读,BaseTypeHandler 已处理 null)。MyBatis 内置了几十个 TypeHandler 覆盖标准类型、按「Java 类型+JDBC 类型」自动匹配,所以平时无感。需要自定义的三大场景:① 枚举存 code(最经典——默认 EnumTypeHandler 存 name 字符串、EnumOrdinalTypeHandler 存 ordinal 但绝不能用因调整顺序会错位,要自定义存稳定 code);② JSON 字段(List/对象 ↔ JSON 字符串,业务无感序列化);③ 字段加解密(存密文读明文,隐私字段透明加密)。注册用全局(type-handlers-package 扫包)或 #{status, typeHandler=...} 局部指定,配 @MappedTypes/@MappedJdbcTypes 精确匹配。一句话「TypeHandler 翻译 Java↔JDBC,写 set 读 get,自定义处理枚举 code/JSON/加解密,别用 EnumOrdinalTypeHandler」。