ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MyBatis TypeHandler 实战:从List转换到JSON字段类型映射完全指南

MyBatis TypeHandler 实战:从List转换到JSON字段类型映射完全指南 说实话我第一次在项目里遇到这个问题时是真被折腾得不轻数据库表里存的是java,mybatis,spring这种逗号分隔的字符串可实体类里对应字段偏偏是ListString。写数据时得手动join读数据时又得手动split还得分隔符、空值、去空格地处理一遍Service 层被这些脏代码糊得面目全非。后来同事提醒了一句“你查查 MyBatis 的 TypeHandler 吧。”我这才发现MyBatis 早就给我们留好了标准解法——TypeHandler 类型转换器。TypeHandler直译过来就是“类型转换器”。它要解决的核心问题其实就一个Java 类型与 JDBC 类型对不上。数据库字段是VARCHARJava 属性却想用ListString数据库字段是INTJava 属性却想直接映射成某个枚举数据库字段是JSON字符串Java 属性却想直接得到一个对象。这些场景统统可以通过 TypeHandler 优雅解决。这篇文章我会把 TypeHandler 的原理、内置实现、手写自定义处理器、MyBatis-Plus 里的特殊玩法以及我实际踩过的那些坑一次性讲透。适合被“字段类型不匹配”折磨过的人、想在 MyBatis/MyBatis-Plus 里扩展类型映射的人以及每一个想把实体类写得干净、舒服的 Java 开发。1. TypeHandler 到底在干什么从 JDBC 的“两座桥”说起1.1 JDBC 那套固定方法撑不起 Java 的类型系统很多人用了很久 MyBatis却从没想过一个底层问题MyBatis 最终还是要走 JDBC 来访问数据库的而 JDBC 和数据库之间的交互方式是固定的。写入时用PreparedStatement.setString(index, value)、setInt(index, value)读取时用ResultSet.getString(column)、getInt(column)。你看JDBC 给的这组方法数量有限、语义固定但 Java 业务对象的类型几乎是无上限的——今天出一个ListString明天出一个MapString, Object后天再来一个自定义枚举。如果没有中间层你就只能在 DAO/Service 层手动搬砖把对象拆成标量塞进 JDBC再从 JDBC 拿回标量拼装成对象。MyBatis 之所以让人觉得“好用”正是因为它把“Java 对象转成 JDBC 可写参数”“JDBC 查询结果转回 Java 对象”这两个过程封装成了框架能力。而 TypeHandler就是这两个过程的最终执行者。你可以把它理解成数据库字段和 Java 属性之间的“翻译官”。写入方向翻译官把 Java 属性翻译成数据库能存的值读取方向翻译官把数据库返回的值翻译回 Java 对象。MyBatis 能自动完成String到VARCHAR、Integer到INT这类常规映射靠的就是它内置的一系列翻译官遇到非常规映射我们就得自己写一个翻译官——也就是自定义 TypeHandler。1.2 从“写入”到“读取”TypeHandler 在 MyBatis 执行链路中的位置要真正理解 TypeHandler不能只停留在概念上。我建议你顺着 MyBatis 执行一条 SQL 的路径看一遍。先看写入insert/update阶段。MyBatis 会把你的实体对象交给 ParameterHandlerParameterHandler 遍历 SQL 里的每个参数占位符找到参数对应的 TypeHandler然后调用setParameter。setParameter里有个很关键的分支逻辑如果参数值是null直接调PreparedStatement.setNull(index, jdbcType)如果值不为null才会走setNonNullParameter把 Java 值转换成 JDBC 能接受的值比如setString、setInt、setObject。再看读取select阶段。MyBatis 执行完ResultSetHandler后会逐列读取结果根据列在 ResultMap 中配置的 TypeHandler没有显式配置就用内建默认的调用getNullableResult把数据库值翻译回 Java 对象再通过反射或构造器赋值给实体属性。所以你会发现一个规律TypeHandler 是分“方向”的。同一个 TypeHandler 类里同时包含写入和读取两个方向的方法。这个设计意味着我们的自定义转换逻辑必须同时考虑好“从 Java 到数据库”和“从数据库到 Java”两个方向。很多人写 TypeHandler 时只实现了一半结果就是插入正常、查询异常或者反过来这种“半截子”实现是最容易翻车的。1.3 为什么说 TypeHandler 是“优雅地搬砖”我见过不少团队处理“数据库字段与 Java 类型不匹配”时选择在 Service 层硬编码。比如上面说到的标签字段Service 层每次都要这么写// 写库 article.setTags(String.join(,, tagList)); // 读库 ListString tags Arrays.stream(article.getTags().split(,)) .map(String::trim) .filter(s - !s.isEmpty()) .collect(Collectors.toList());一两处还好一旦项目里多了三五个类似字段到处都是这种拆拆拼拼的代码。更麻烦的是如果字段在一个很深的联表查询结果集里你得在 DTO 层再处理一遍漏了一处就出 bug。TypeHandler 的价值就在于把这种“搬砖逻辑”收敛到一个地方并且把“转换时机”放在框架层。实体类里该是ListString就写ListString该是OrderStatus就写OrderStatusSQL 层和数据库层完全感知不到转换过程。上层的业务代码干净了底层的数据存取也稳定了。这就是“优雅地搬砖”——不是不搬而是把砖搬到了框架的轮子上。2. 内置 TypeHandler 大盘点哪些开箱即用哪些容易踩坑2.1 内置处理器家族与它们的“默认岗位”MyBatis 之所以开箱即用是因为它在启动时会往 TypeHandlerRegistry 里注册一批内置 TypeHandler。每个 TypeHandler 对应一组 Java 类型和 JDBC 类型组合框架遇到参数或结果集时会先找注册表里有没有匹配的处理器有就直接用没有才轮到自定义逻辑兜底。我整理了下大家在项目里接触最多的这几类内置 TypeHandler支持的 Java 类型通常对应的 JDBC 类型StringTypeHandlerStringVARCHAR / CHARIntegerTypeHandlerInteger / intINTEGERLongTypeHandlerLong / longBIGINTDoubleTypeHandlerDouble / doubleDOUBLEBigDecimalTypeHandlerBigDecimalDECIMAL / NUMERICDateTypeHandlerjava.util.DateTIMESTAMP / DATELocalDateTimeTypeHandlerjava.time.LocalDateTimeTIMESTAMPLocalDateTypeHandlerjava.time.LocalDateDATEBooleanTypeHandlerBoolean / booleanBOOLEAN / BITByteArrayTypeHandlerbyte[]VARBINARY / BLOBEnumTypeHandler任意枚举VARCHAREnumOrdinalTypeHandler任意枚举INTEGER这里有两个地方我特别提醒一下。第一EnumTypeHandler保存枚举时用的是name()也就是枚举常量名比如OrderStatus里的CREATED数据库里存的就是字符串CREATED不是1也不是待支付。很多新手第一次枚举接数据库时都会震惊“怎么存成名字了”。第二EnumOrdinalTypeHandler存的是ordinal()也就是枚举声明顺序的序号0, 1, 2...。这个方案我特别不建议默认使用因为一旦后续有人调整枚举顺序或者插入新枚举老数据的序号语义就全乱了。2.2 内置枚举处理器为什么容易“翻车”有次我们项目里一个订单状态字段要接入新枚举一位同事直接把新枚举值插到枚举类中间位置结果数据库里所有 “已支付” 订单查询出来后全变成了 “已关闭”。这个事故的根因就是他默认启用了EnumOrdinalTypeHandler数据库存的是一串序号而枚举的ordinal是跟着声明顺序走的只要顺序一变序号对应的业务含义就全变了。即使你用的是默认的EnumTypeHandler也有风险它存的是枚举名如果你后续把某个枚举常量改个名字比如PAID改成PAD_SUCCESS那数据库里旧数据存的PAID就永远也匹配不上新枚举了。所以我的建议是对于有业务含义的枚举不要依赖name()和ordinal()在数据库里专门给枚举设计一个稳定不变的 code 字段然后通过 TypeHandler 或者 MyBatis-Plus 的EnumValue来处理转换。这里面的具体写法后面我会单独讲。还有个很隐蔽的小坑如果你在实体类里用了枚举字段但 MyBatis 没找到专门为该枚举注册的 TypeHandler它会默认走EnumTypeHandler。这是“能用”但未必是“你想要”的效果。如果希望某个枚举统一用整数 code 存库只靠默认配置是不够的必须显式指定处理器。2.3 默认类型映射不够用的常见场景内置处理器覆盖了大部分标量类型但有几个场景是内置处理器完全搞不定的我把它们列出来基本就是你需要写自定义 TypeHandler 的信号数据库是VARCHARJava 是ListString/SetString数据库是VARCHAR或JSON类型Java 是自定义业务对象数据库是VARCHARJava 是MapString, Object数据库是INT或VARCHARJava 枚举需要做业务 code 映射数据库是BLOB/CLOBJava 是自定义序列化对象数据库是VARCHARJava 是某个需要加密或脱敏处理的特殊类型。遇到这些情况别硬啃内置处理器也别在 Service 层手写转换——写一个自定义 TypeHandler 才是正道。接下来我带大家走一遍完整的自定义 TypeHandler 实操。3. 自定义 TypeHandler 的完整实操从零写一个 ListString 转换器3.1 实现方式选择继承 BaseTypeHandler 还是直接实现 TypeHandler 接口我先说结论绝大多数情况下继承BaseTypeHandlerT就够了。MyBatis 官方也推荐这么做。BaseTypeHandlerT已经把参数为 null 时的处理、日志记录、异常包装这些框架级逻辑封装好了你只需要实现四个核心方法。直接实现TypeHandler接口当然也能用但你要自己处理空值分支代码量多还容易在 null 处理上写错。BaseTypeHandlerT里的T就是你希望转换的 Java 类型。比如我要处理ListString那么T就填ListString。这里有两个需要注意的细节如果想让自定义 TypeHandler 在 XML 中通过javaType匹配到最好在类上加MappedTypes(List.class)注解如果希望同时限定 JDBC 类型方向可以加MappedJdbcTypes(JdbcType.VARCHAR)。加了之后只有遇到VARCHAR类型的列/参数时才会匹配到这个处理器更精准也能避免误伤。3.2 手把手实现四个方法的职责逐一看我拿一个最经典的场景演示数据库字段存java,mybatis,springJava 字段是ListString用逗号分隔逗号两侧可能有空格需要清理。完整实现如下package com.example.handler; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import org.apache.ibatis.type.MappedTypes; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; MappedTypes(List.class) MappedJdbcTypes(JdbcType.VARCHAR) public class StringListTypeHandler extends BaseTypeHandlerListString { private static final String SPLIT_CHAR ,; Override public void setNonNullParameter(PreparedStatement ps, int i, ListString parameter, JdbcType jdbcType) throws SQLException { // 数据库设计约定存逗号分隔字符串这里做反向拼接 ps.setString(i, String.join(SPLIT_CHAR, parameter)); } Override public ListString getNullableResult(ResultSet rs, String columnName) throws SQLException { return convert(rs.getString(columnName)); } Override public ListString getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return convert(rs.getString(columnIndex)); } Override public ListString getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return convert(cs.getString(columnIndex)); } private ListString convert(String value) { if (value null || value.isEmpty()) { return null; } return Arrays.stream(value.split(SPLIT_CHAR)) .map(String::trim) .collect(Collectors.toList()); } }逐个方法说下职责。setNonNullParameter只在参数值不为 null 时被调用。把 Java 的ListString转成字符串写进 PreparedStatement。这里我直接用了String.join简洁。getNullableResult(ResultSet, String columnName)和getNullableResult(ResultSet, int columnIndex)这两个是查询时按列名和按列序号读取的两种方式。MyBatis 内部根据正好 ResultMap 配置走对应重载。正常开发中两个都要实现因为你不知道哪条 SQL 或哪个数据源会用哪种方式调用。getNullableResult(CallableStatement cs, int columnIndex)处理存储过程出参如果不写调用存储过程返回这个类型时会报抽象方法未实现。convert方法是我自己抽出来的私有方法负责把数据库字符串解析成ListString。这里我做了两个防御null 和空字符串都返回 null避免把空串 split 成一个空数组。虽然只看代码就够用了但我要特意强调一个容易踩的坑不要在setNonNullParameter里再次判空。因为 BaseTypeHandler 的父类逻辑已经处理了 null参数为 null 时会调用ps.setNull(i, jdbcType.TYPE_CODE)根本不会进到这里。你在子类里判空反而会造成死代码而且容易让人误以为“不判空就会 NPE”。3.3 注册 TypeHandler 的四种姿势与生效范围写好了类只是第一步你得让 MyBatis“认”它。实际项目里我见过四种注册方式按场景灵活选择。第一种XML 全局注册。在mybatis-config.xml的typeHandlers节点下typeHandlers typeHandler handlercom.example.handler.StringListTypeHandler javaTypejava.util.List jdbcTypeVARCHAR/ /typeHandlers这种方式在 XML 配置的 MyBatis 项目里最常见注册后全局生效。第二种包扫描自动注册。Spring Boot 集成 MyBatis 后在application.yml里配置mybatis: type-handlers-package: com.example.handler框架会扫描包下所有 TypeHandler并结合类上的MappedTypes/MappedJdbcTypes自动完成注册。这种方式最省事但如果某个类没加注解包扫描会找不到明确的匹配规则可能导致映射时不生效。第三种字段级显式指定。在 XML 的 ResultMap 中指定result columntags propertytags typeHandlercom.example.handler.StringListTypeHandler/这种方式只对当前 ResultMap 生效适合个别字段特殊处理、其他字段默认的场景。还有一种等价的写法是给实体类字段加注解类似TableFieldMyBatis-Plus 项目更常用。第四种MyBatis-Plus 实体注解只适用于 MP 项目TableName(value article, autoResultMap true) public class Article { TableField(typeHandler StringListTypeHandler.class) private ListString tags; }这里有个细节我必须强调如果只写了TableField(typeHandler ...)插入和更新时 TypeHandler 会生效但查询时不一定生效。MyBatis-Plus 只有在TableName上显式加了autoResultMap true才会自动为带 TypeHandler 的字段生成 ResultMap 并用于查询映射。很多人在插入时正常一查出来 tags 字段就变成 null十有八九就是缺了autoResultMap true。4. 再进一步JSON 对象、枚举映射与 MyBatis-Plus 的高阶玩法4.1 写一个通用 JSON 转换器一个模板搞定所有自定义对象如果说ListString是 TypeHandler 的入门题那JSON 字符串和自定义对象的互转就是项目里最常见的进阶需求。比如用户表里有一个extra_info字段数据库存{level:5,vip:true,score:90}Java 字段是个MapString, Object或者一个UserExtraInfo对象。我写过不少 JSON TypeHandler现在给你一个比较稳的模板——用 Jackson 处理MapString, Objectpackage com.example.handler; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import java.io.IOException; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Map; public class MapJsonTypeHandler extends BaseTypeHandlerMapString, Object { // 全局共用一个 ObjectMapper避免频繁创建带来的开销 private static final ObjectMapper MAPPER new ObjectMapper(); Override public void setNonNullParameter(PreparedStatement ps, int i, MapString, Object parameter, JdbcType jdbcType) throws SQLException { try { ps.setString(i, MAPPER.writeValueAsString(parameter)); } catch (JsonProcessingException e) { throw new SQLException(对象转 JSON 字符串失败, e); } } Override public MapString, Object getNullableResult(ResultSet rs, String columnName) throws SQLException { return parse(rs.getString(columnName)); } Override public MapString, Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return parse(rs.getString(columnIndex)); } Override public MapString, Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return parse(cs.getString(columnIndex)); } private MapString, Object parse(String json) throws SQLException { if (json null || json.isEmpty()) { return null; } try { return MAPPER.readValue(json, new TypeReferenceMapString, Object() {}); } catch (IOException e) { throw new SQLException(JSON 字符串转 Map 失败, e); } } }这里有个很重要的细节我特意用了TypeReferenceMapString, Object()而不是Map.class。原因是 Java 泛型擦除TypeHandlerMapString, Object里的泛型参数运行时拿不到直接MAPPER.readValue(json, Map.class)虽然能反序列化但得到的类型是一坨裸 Map内层类型全靠 Jackson 猜很容易出现LinkedHashMap嵌套、强转失败的情况。用TypeReference可以明确告诉 Jackson 目标类型解析结果更可控。如果你需要针对具体业务对象UserExtraInfo原理完全一样把TypeReference换成UserExtraInfo.classT也改成对应类型即可。如果你的项目用的是 Fastjson2 / Gson思路一致替换序列化 API 就行。4.2 枚举转换器的正确姿势别再用 name() 和 ordinal() 了前面提到默认枚举处理器不可控这里给一个比较稳的枚举 TypeHandler 写法。假设订单状态枚举public enum OrderStatus { CREATED(1, 已创建), PAID(2, 已支付), CLOSED(3, 已关闭); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } public static OrderStatus of(int code) { for (OrderStatus status : values()) { if (status.code code) { return status; } } throw new IllegalArgumentException(未知订单状态码 code); } }对应 TypeHandlerpackage com.example.handler; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import org.apache.ibatis.type.MappedTypes; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; MappedTypes(OrderStatus.class) MappedJdbcTypes(JdbcType.INTEGER) public class OrderStatusTypeHandler extends BaseTypeHandlerOrderStatus { Override public void setNonNullParameter(PreparedStatement ps, int i, OrderStatus parameter, JdbcType jdbcType) throws SQLException { // 存业务 code不存 name也不存 ordinal ps.setInt(i, parameter.getCode()); } Override public OrderStatus getNullableResult(ResultSet rs, String columnName) throws SQLException { return OrderStatus.of(rs.getInt(columnName)); } Override public OrderStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return OrderStatus.of(rs.getInt(columnIndex)); } Override public OrderStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return OrderStatus.of(cs.getInt(columnIndex)); } }关键点就一个数据库永远只存业务 code不依赖枚举声明顺序和枚举名称。这样一来后续你随便调整枚举顺序、给枚举改名只要 code 不变老数据就永远不坏。不过我也得说句实话如果你用的是 MyBatis-Plus这种自定义枚举 TypeHandler 大多数时候不用写。MP 提供了更轻量的EnumValue注解直接在期望存库的字段上加注解即可public enum OrderStatus { CREATED(1, 已创建), PAID(2, 已支付), CLOSED(3, 已关闭); EnumValue private final int code; // 省略其他 }加上EnumValue后MyBatis-Plus 会自动把code作为枚举与数据库的转换值插入、查询都无需额外 TypeHandler。这个方案我更喜欢代码量少、语义清晰。但如果你是纯 MyBatis 项目那自定义 TypeHandler 仍然是标准答案。4.3 MyBatis-Plus 中使用 TypeHandler 的三个关键点现在很多项目已经全面转向 MyBatis-Plus我再单独讲讲 MP 场景下的 TypeHandler 使用要点这些是我自己踩过坑之后总结出来的。第一个点实体字段配TableField(typeHandler ...)时一定要看查询是否走 ResultMap。前面提过TableName(autoResultMap true)这里我再强调一次因为它涉及一个非常隐蔽的现象insert/updateById时 TypeHandler 正常但selectById/selectList查出来的字段是 null。排查步骤很简单先看实体类上有没有autoResultMap true没有就加上。第二个点条件查询QueryWrapper传参时TypeHandler 不一定会自动生效。比如QueryWrapperArticle wrapper new QueryWrapper(); wrapper.eq(tags, tagList); // 期望把 List 转成逗号串再比较这条查询大概率不会按你的预期走因为eq里的参数是直接拼进 SQL 的不会经过tags字段的 TypeHandler 转换。真正要做这类查询要么把参数手动转成字符串再传要么用apply自定义 SQL 片段要么换个存储方案比如单独建关联表。很多人在 MP 里“TypeHandler 对查询条件无效”就是这么来的——不是 Bug是框架设计如此。第三个点MyBatis-Plus 内置了一个JacksonTypeHandlercom.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler可以直接用来处理 JSON 字段。比如实体里有一个对象属性TableField(typeHandler JacksonTypeHandler.class) private UserExtraInfo extraInfo;同样记得配合TableName(autoResultMap true)。它的优点是省得自己写 JSON 转换器缺点是它内部用的是 Jackson如果你的数据里有些日期格式、自定义序列化策略和全局 ObjectMapper 不一致还是自己写 TypeHandler 更可控。我个人的建议是简单场景用内置JacksonTypeHandler复杂场景嵌套泛型、自定义序列化、加密字段就自己写。5. 常见问题与排查思路实录5.1 写进去了读不出来TypeHandler 不生效的自查清单我接手过好几个同事“插入正常、查询为 null”的问题每次排查链路都差不多。我把它整理成一个速查清单你遇到类似问题时按顺序过一遍就行现象可能原因处理方式插入正常查询字段为 nullMyBatis-Plus 缺少autoResultMap true在TableName上补上插入正常查询字段为 nullResultMap 中没有为该字段指定typeHandler在result上显式声明插入正常查询报类型转换异常自定义 TypeHandler 的getNullableResult解析没考虑空值/格式变化检查解析逻辑增加异常兜底字段在其他项目里能被查到本项目查不到包扫描没覆盖到 TypeHandler 类检查type-handlers-package配置自定义 TypeHandler 完全没被调用没有注册或MappedTypes/MappedJdbcTypes不匹配显式在 XML / 配置文件中注册并核对类型组合这里我多说一句When 排查时最有效的工具是日志。MyBatis 的底层 SQL 日志里能看到实际绑定到 PreparedStatement 的参数值类型如果 SQL 日志显示绑定的是String那说明 TypeHandler 生效了如果显示的是对象那就说明 MyBatis 用了默认的 ObjectTypeHandler你的自定义 TypeHandler 根本没被找到。5.2 泛型擦除导致的反序列化失败T 拿不到别指望反射写BaseTypeHandlerT的时候很多人以为能在子类里拿到T的具体类型然后做类似MAPPER.readValue(json, T.class)的操作。我可以很明确地告诉你大多数情况下拿不到。Java 泛型在运行时会被擦除TypeHandler 实例化的时候T就是一棵裸的Object或Object.class。这也是我为什么在前面示例里反复用TypeReference和具体Class的原因。通用型 TypeHandler 如果用反射去猜T很容易在反序列化嵌套泛型时翻车。实际项目里我更推荐“一个业务类型写一个专用 TypeHandler”或者“在注册时显式绑定具体类型”宁可多写几个类也别追求一个万能泛型处理器。稳定比优雅更重要。还有一个类似的问题TypeHandler 的实例没有无参构造器。MyBatis 创建 TypeHandler 时默认走无参构造器如果你在类里定义了一个带参构造器又没有显式声明无参构造器MyBatis 会实例化失败。尤其是继承BaseTypeHandlerT时子类构造器不能被框架感知所以要么保证有无参构造器要么用MappedTypes让框架走默认实例化流程。5.3 性能问题ObjectMapper 别每次 new转换逻辑别写太肥TypeHandler 是在 SQL 执行链路里被高频调用的。一条批量插入几百条记录、每条三五个 TypeHandler 字段一次请求可能就要触发上千次转换。如果你的 TypeHandler 里每次都new ObjectMapper()或者创建其他重量级对象性能损耗会非常明显。建议把不依赖上下文的重量级工具设为static final字段比如前面示例里的ObjectMapper MAPPER。String 拼接和 split 这种轻操作可以放心写在方法里但如果转换涉及加解密、自定义序列化、远程字典翻译最好加一层缓存或设计成可控开销。说句实在的TypeHandler 适合做“纯转换”不适合做“业务加工”。你要是想在字段映射时顺带调接口、查缓存那应该在其他层完成后再写入实体而不是塞在 TypeHandler 里。另外convert方法里如果出现解析失败我强烈建议把原始值和异常信息一起放进SQLException抛出而不是返回 null。这样可以避免“数据库里有脏数据查出来却是 null业务层静默出错”的情况。排查问题时异常信息里的原始值往往能帮你一眼定位是分隔符问题还是 JSON 格式问题。最后再分享一个小技巧。如果你不想给每张表、每个字段都单独写 TypeHandler可以试试在接口设计的源头就做好约定所有 JSON 字段统一用JSON类型存储所有枚举都设计成带 code 的枚举并约定好 code 的稳定性。把存储契约定清楚了TypeHandler 就是从“救火工具”变成“顺手工具”。我自己在实际项目里的习惯是优先用内置处理器 MyBatis-Plus 的EnumValue解决不了再写专用 TypeHandler尽量少造通用轮子。TypeHandler 这东西边界划清楚了用起来很舒服划不清就是给自己埋雷。希望这篇文章能帮你把雷拆掉。
返回列表