5.2 TypeHandler:类型缝合线 本节摘要:TypeHandler 负责单个 Java 类型与单个 JDBC 类型的双向转换,贯穿参数设值与结果取值两条通道。本节先看默认处理器的清单与枚举映射的两种内置选择,再手写一个"字符串列表与 JSON 列互转"的自定义处理器,走完实现、注册、验证的全程。 2.2 节参数装配时提到过它:值从 Java 到 JDBC 的类型翻译。这一节把这条隐形缝合线抽出来看清楚。 它站在哪两条通道上 参数入时把 Java 值翻译成 JDBC 类型设进语句;结果出时把 JDBC 值翻译回 Java 类型装进属性。
本节摘要:TypeHandler 负责单个 Java 类型与单个 JDBC 类型的双向转换,贯穿参数设值与结果取值两条通道。本节先看默认处理器的清单与枚举映射的两种内置选择,再手写一个"字符串列表与 JSON 列互转"的自定义处理器,走完实现、注册、验证的全程。
2.2 节参数装配时提到过它:值从 Java 到 JDBC 的类型翻译。这一节把这条隐形缝合线抽出来看清楚。
参数入时把 Java 值翻译成 JDBC 类型设进语句;结果出时把 JDBC 值翻译回 Java 类型装进属性。同一个处理器服务两条通道:

常见类型的默认翻译框架已经内置,日常无感:
| Java 侧 | JDBC 侧 | 默认行为 |
|---|---|---|
| String | VARCHAR / LONGVARCHAR | 直通 |
| Integer、Long 等 | INTEGER / BIGINT | 直通(null 安全) |
| BigDecimal | DECIMAL | 精度保留 |
| Date、LocalDateTime | TIMESTAMP | 日期时间互转 |
| Boolean | BOOLEAN;或数值 0/1 | 按驱动方言 |
| byte[] | BLOB | 二进制直通 |
| Enum | VARCHAR 或 INTEGER | 两种内置处理器分野 |
枚举是第一个"需要做选择"的场景。框架内置两种处理器,行为截然不同:
public enum OrderStatus { WAITING, PAID, CLOSED }
<!-- 写法一:默认按名字存 VARCHAR,列值是 PAID 这样的字符串 --> <resultMap id="orderMap" type="Order"> <result property="status" column="status" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/> </resultMap> <!-- 写法二:按序数存 INTEGER,列值是 0、1、2 --> <resultMap id="orderMap2" type="Order"> <result property="status" column="status" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/> </resultMap>
工程判断很明确:按名字存。序数绑定声明顺序——中间插一个新枚举值,历史数据的 1 就从 PAID 变成 CLOSED,这类事故在半夜最难查。除非表结构已定死为整型列,否则一律选名字存储。
背景:商品表有个 tags 列存 JSON 字符串(如 '["秒杀","满减"]'),Java 实体想直接用 List tags。默认翻译器不认识这对组合,需要自定义处理器。
实现——继承 BaseTypeHandler,四个方法各管一条通道的一端:
// 自定义处理器:List<String> 与 JSON 文本互转 @MappedTypes(List.class) // 声明服务的 Java 类型 @MappedJdbcTypes(JdbcType.VARCHAR) // 声明服务的 JDBC 类型 public class StringListJsonHandler extends BaseTypeHandler<List<String>> { // 参数通道:Java 值 -> 设进语句 @Override public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, String.join("\",\"", parameter)); // 实际项目用 JSON 库序列化;此处手拼演示结构 } // 结果通道:按列名取值 -> Java 值 @Override public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException { return parse(rs.getString(columnName)); } // 结果通道:按列下标取值 @Override public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return parse(rs.getString(columnIndex)); } // 结果通道:存储过程出参取值 @Override public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return parse(cs.getString(columnIndex)); } private List<String> parse(String raw) { if (raw == null || raw.isBlank()) return new ArrayList<>(); // 去掉方括号与引号后按逗号拆分(示意实现,生产用 JSON 库) String body = raw.substring(1, raw.length() - 1); if (body.isEmpty()) return new ArrayList<>(); return Arrays.stream(body.split(",")) .map(s -> s.replace("\"", "").trim()) .collect(Collectors.toList()); } }
注册——三种方式按影响范围递增:
<!-- 方式一:全局注册,对整个应用的该类型组合生效 --> <typeHandlers> <typeHandler handler="com.example.handler.StringListJsonHandler"/> </typeHandlers>
<!-- 方式二:单个映射点声明,只在本地生效 --> <resultMap id="productMap" type="Product"> <result property="tags" column="tags" typeHandler="com.example.handler.StringListJsonHandler"/> </resultMap>
// 方式三:注解绑定在接口方法上 @Insert("INSERT INTO products(name, tags) VALUES(#{name}, #{tags, " + "typeHandler=com.example.handler.StringListJsonHandler})") int insert(Product product);
注意差异:参数侧的井号占位要显式指定 typeHandler(写在井号括号内),结果侧写在 resultMap 的 result 元素上——两边声明位置不同,漏了一边就是"存得进取不出"或反之。
验证:
Product p = new Product(); p.setName("保温杯"); p.setTags(List.of("秒杀", "满减")); mapper.insert(p); session.commit(); Product db = mapper.findByName("保温杯"); // 输出:[秒杀, 满减] System.out.println(db.getTags());
⚠️ 常见坑:全局注册 List.class 会把所有 List 类型参数都导向这个处理器——实体里 List< Integer> 之类的字段会被误伤。窄化声明(@MappedJdbcTypes 限定 VARCHAR,或在 resultMap 里局部指定)比全局注册更安全;泛型擦除让注册表无法区分 List 的元素类型,这是 Java 侧的先天限制。
💡 关键直觉:TypeHandler 是"每个字段一对类型"的微型转换器,不是全局翻译服务。默认清单覆盖不到的组合(JSON、加密列、自定义值对象)才需要它;先确认确实没有现成处理器,再动手写。
缝合线看完,下一节讲插队机制:插件如何在四大对象的执行链上插入自己的逻辑——分页、慢 SQL 审计都从这里生长。