资讯动态

MyBatis typeHandler源码解析:自定义类型转换器的原理与实战

发布时间:2026/10/17 10:42:12 来源:尧图企业网站定制
在 MyBatis 的使用过程中ORM 框架自动帮我们完成了 Java 类型和 JDBC 类型之间的相互转换但如果哪天你需要把一个对象存成 JSON 字符串或者把一个枚举存成数据库里的整型你会发现自己被卡住了。这就是我今天想聊的主题typeHandler以及它在 MyBatis 源码里到底是怎么运转的。我最早用 MyBatis 的时候一直觉得 typeHandler 是个“知道但不熟”的存在。直到有一次在一个业务项目里需要自定义一个枚举转换器不加思索照着网上的配置写完结果跑起来完全没生效。那一次我被迫把TypeHandlerRegistry的源码翻了个底朝天才真正理解了这套机制。这篇文章不打算铺开讲 MyBatis 的全部内容而是聚焦 typeHandler它负责什么、源码里如何注册和匹配、自定义扩展时最容易踩的坑以及我能给你的几条实战经验。如果你正准备手写自定义 typeHandler或者好奇为什么默认的枚举处理器总是和你预期不一致或者想知道MappedTypes和MappedJdbcTypes到底谁说了算这篇文章应该能给你一个比较完整的答案。我会从使用者的视角切入源码尽量说人话把关键路径一条一条捋清楚。1. 问题起点为什么需要 typeHandler先说个最简单的场景。MyBatis 执行一条 insert 语句时PreparedStatement需要知道 Java 对象的某个属性怎么设置到 SQL 参数上。执行一条 select 语句时ResultSet里的字段值又怎么转换回 Java 属性。这两个方向上的类型转换就是 typeHandler 要解决的问题。你可能会说Java 类型和 JDBC 类型不都是确定的类型吗直接setString、setInt不就行了问题是Java 类型和 JDBC 类型并不是一一对应的。比如java.util.Date既可以映射成数据库的DATE、TIME也可以映射成TIMESTAMP。再比如枚举数据库里存的是VARCHAR还是INTEGER完全取决于你的表结构。这些映射关系带有很强的业务属性框架没法替你决定所以必须留出扩展点。typeHandler 就是 MyBatis 给出的扩展点方案。它像一个翻译官夹在 Java 层和 JDBC 层之间。当 MyBatis 需要给PreparedStatement设置参数时会调用 typeHandler 的setParameter方法当 MyBatis 需要从ResultSet或CallableStatement中读取数据时会调用getResult方法。理解这两组方法就理解了 typeHandler 的全部职责。在 MyBatis 里默认注册了非常多的处理器覆盖常见的基本类型、日期类型、byte[]、BigDecimal 等。但不管内置处理器多全总有些类型是框架无法预知的比如某个业务系统里自定义的枚举、某个领域模型值对象、某个需要序列化成 JSON 的 List。这就是自定义 typeHandler 存在的意义。2. 源码里的核心角色与匹配逻辑2.1 接口与抽象类各司其职先看接口定义public interface TypeHandlerT { void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; T getResult(ResultSet rs, String columnName) throws SQLException; T getResult(ResultSet rs, int columnIndex) throws SQLException; T getResult(CallableStatement cs, int columnIndex) throws SQLException; }这就是所有 typeHandler 必须实现的方法。setParameter是写方向getResult是读方向。注意写方向多了一个jdbcType参数它在某些场景下会决定底层调用setNull还是其他方法。大多数人不会直接实现接口而是继承BaseTypeHandlerT。这个抽象类做了几件通用的事情处理parameter为 null 的情况如果jdbcType为 null会抛异常否则调用ps.setNull(i, jdbcType.TYPE_CODE)。把getResult的多个重载统一收拢到getNullableResult。对java.sql.SQLException做统一包装抛成TypeException。这也是为什么你继承BaseTypeHandler时只需要关心三件事怎么设置非空参数、怎么从 ResultSet 按列名取非空值、怎么从 ResultSet 按下标取非空值。其余通用逻辑框架已经处理完了。public abstract class BaseTypeHandlerT extends TypeReferenceT implements TypeHandlerT { 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 jdbcTypeForNull configuration property. Cause: e, e); } } } // 还有 getResult 的包装就不展开了 }这里有一个值得注意的细节如果你写一个自定义 typeHandler却没有给参数指定jdbcType并且参数值为 nullMyBatis 会直接抛TypeException。这个异常信息经常出现别以为是无缘无故的它就是这里抛出来的。2.2 TypeHandlerRegistry注册的中心枢纽TypeHandlerRegistry是 typeHandler 的“户口本”。所有内置处理器和自定义处理器都注册在它里面。它的内部结构并不复杂核心是几个 MapjdbcTypeHandlerMap按JdbcType映射处理器。typeHandlerMap按 Java 类型映射处理器。allTypeHandlersMap全部处理器的并集。注册处理器的方式不止一种。可以按 Java 类型注册可以按 JdbcType 注册也可以两者同时指定。源码里最常用的注册方法是public T void register(ClassT javaType, TypeHandler? extends T typeHandler) { register((TypeHandlerT) typeHandler, javaType, null); }除了显式注册之外MyBatis 启动时还会做一件很重要的事扫描所有标注了MappedTypes的处理器。所以如果你在自定义处理器上写了MappedTypes(MyEnum.class)那么它就有机会被自动注册。但注意前提是你的 MyBatis 配置开启了处理器扫描或者你通过typeHandlersPackage指定了扫描包名。我见过不少项目自定义 typeHandler 类上加了MappedTypes也配置了typeHandlersPackage结果发现枚举转 varchar 的规则没生效查来查去发现是类路径扫描到了但注册器的 Java 类型 key 和实际参数类型不匹配。这个后面细说。2.3 默认处理器和枚举的特殊处理MyBatis 内置了两个枚举处理器EnumTypeHandler和EnumOrdinalTypeHandler。EnumTypeHandler使用枚举的name()作为存储值存成 varcharEnumOrdinalTypeHandler使用枚举的ordinal()作为存储值存成整数。默认情况下MyBatis 注册的是EnumTypeHandler。如果你不想用这两个默认处理器有两个办法在mybatis-config.xml里配置defaultEnumTypeHandler指向你自己的枚举处理器。为特定枚举字段指定typeHandler。从源码角度解析器初始化时把EnumTypeHandler.class注册为枚举的默认处理器对应代码位置在TypeHandlerRegistry的构造方法里register(Enum.class, getInstance(null, EnumTypeHandler.class));但是这里有个容易忽略的地方Enum.class和具体的枚举子类在typeHandlerMap里的 key 并不是父子关系。MyBatis 在做类型匹配时会尝试先从javaType的 Class 作为 key 找处理器找不到再去typeHandlerMap里遍历找到能“兼容”的处理器。具体怎么兼容就是getTypeHandler里的逻辑。我看过几次这段源码后总结了一条规律如果你自定义处理器里写了MappedTypes(MyEnum.class)实际参数类型是MyEnum那匹配没问题但如果你写了MappedTypes(Enum.class)想通过“父类匹配”一网打尽十有八九会匹配不上。因为 MyBatis 的注册 map 默认是精确匹配不是父子兼容匹配除了极少数在getTypeHandler中做了特殊处理的类型。3. 自定义 typeHandler 的新手友好实现3.1 最短可用示例把枚举存成 varchar我直接给一个常用例子。假设有一个订单状态枚举public enum OrderStatus { CREATED(created), PAID(paid), SHIPPED(shipped), COMPLETED(completed); private final String code; OrderStatus(String code) { this.code code; } public String getCode() { return code; } public static OrderStatus fromCode(String code) { for (OrderStatus status : values()) { if (status.code.equals(code)) { return status; } } throw new IllegalArgumentException(Unknown order status: code); } }我们希望数据库里存字符串读取的时候再转成枚举。自定义处理器继承BaseTypeHandlerOrderStatusMappedTypes(OrderStatus.class) public class OrderStatusTypeHandler extends BaseTypeHandlerOrderStatus { Override public void setNonNullParameter(PreparedStatement ps, int i, OrderStatus parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter.getCode()); } Override public OrderStatus getNullableResult(ResultSet rs, String columnName) throws SQLException { String code rs.getString(columnName); return code null ? null : OrderStatus.fromCode(code); } Override public OrderStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException { String code rs.getString(columnIndex); return code null ? null : OrderStatus.fromCode(code); } Override public OrderStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { String code cs.getString(columnIndex); return code null ? null : OrderStatus.fromCode(code); } }注意setNonNullParameter里的逻辑因为BaseTypeHandler.setParameter已经处理过 null所以这里只需要关注非空情况。另外实现CallableStatement的getNullableResult是必须的接口里有这个方法你不实现编译都过不去。3.2 注册方式三种配置逐个说第一种是全局包扫描注册在 MyBatis 配置文件里configuration typeHandlers package namecom.example.handler/ /typeHandlers /configuration或者如果你用的是 MyBatis 单独配置typeHandlersPackagecom.example.handler这种方式的原理是启动时扫描包下的TypeHandler实现类读取类上的MappedTypes注解然后注册到TypeHandlerRegistry。第二种是显式注册某个类typeHandlers typeHandler handlercom.example.handler.OrderStatusTypeHandler/ /typeHandlers如果类上没有MappedTypes这种方式注册的处理器对应的 javaType 是 null只有当它也被声明为某个属性的 typeHandler 时才会被使用。第三种是在 XML 映射文件或注解里为某个字段指定resultMap idorderMap typecom.example.Order id propertyid columnid/ result propertystatus columnstatus typeHandlercom.example.handler.OrderStatusTypeHandler/ /resultMap或者在 insert/update 语句中的参数位置指定insert idinsertOrder insert into orders(status) values(#{status, typeHandlercom.example.handler.OrderStatusTypeHandler}) /insert这三种方式解决的场景完全不同。全局扫描适合“这个类型全局只有一种转换规则”的情况字段级指定适合“同一个类型在不同字段上有不同转换规则”的情况。比如同一个枚举字段一个映射到 varchar一个映射到 int你就不能靠全局扫描一锅端。3.3 泛型和 TypeReference 的关联BaseTypeHandlerT的父接口是TypeReferenceT。TypeReference利用泛型擦除的特性在子类初始化时把泛型类型记录下来public abstract class TypeReferenceT { private final Type rawType; protected TypeReference() { rawType getSuperclassTypeParameter(getClass()); } }这意味着如果你定义一个类class MyHandler extends BaseTypeHandlerOrderStatus那么OrderStatus.class可以从 TypeReference 里拿到。TypeHandlerRegistry在注册时也会借助这个信息自动识别 javaType。所以如果你的自定义处理器继承BaseTypeHandlerOrderStatus并且通过typeHandler handler.../显式注册MyBatis 是有机会推断出 javaType 的。这种推断只在你没有写MappedTypes时才有意义。如果写了MappedTypes它的优先级更高。这里也是源码解析时比较绕的地方我建议不要依赖“推断”显式写MappedTypes是更稳妥的做法。4. 深入参数解析与结果集映射的调用链4.1 写操作参数处理器如何找到 typeHandlerMyBatis 在执行一条 insert/update 时会把 SQL 里的每个#{}解析成一个ParameterMapping。ParameterMapping里保存了属性名、jdbcType、typeHandler 等信息。构建ParameterMapping时会调用ParameterMapping.Builder里面有一段关键逻辑private TypeHandler? resolveTypeHandler(Configuration configuration, String javaType, TypeHandler? typeHandler) { if (javaType ! null) { Class? clazz resolveClass(javaType); if (clazz ! null) { return configuration.getTypeHandlerRegistry().getTypeHandler(clazz, jdbcType); } } return configuration.getTypeHandlerRegistry().getTypeHandler(typeHandler); }也就是说如果你在 SQL 参数里显式写了typeHandler那直接用你指定的那个如果没有写MyBatis 会根据属性对应的 Java 类型去TypeHandlerRegistry里找。这里就有第一个容易踩坑的点#{}里的javaType不一定总是能解析出来因为 MyBatis 可能无法从 Bean 的属性反射中准确拿到泛型类型。当找不到时就会往Object.class的处理器方向兜底。另一个很容易被忽略的点如果你未指定 jdbcType 且参数值为 nullBaseTypeHandler.setParameter要求 jdbcType 不能为 null。所以你会遇到“Parameter null with JdbcType null”的异常。解决方式之一是全局配置jdbcTypeForNullsettings setting namejdbcTypeForNull valueNULL/ /settings或者调用参数映射时显式指定 jdbcType。这个配置项是 MyBatis 官方建议在生产环境设置的因为默认的jdbcTypeForNull是 OTHER某些数据库驱动在执行setNull(i, Types.OTHER)时会报错。4.2 读操作ResultSet 是怎么选中处理器的读方向的核心在DefaultResultSetHandler的applyAutomaticMappings和applyPropertyMappings。自动映射时MyBatis 会根据 JDBC 列的元数据得到JdbcType再根据 Java 属性类型得到javaType然后调用typeHandler typeHandlerRegistry.getTypeHandler(javaType, jdbcType);这个查询过程要说清楚。在TypeHandlerRegistry#getTypeHandler(Class? type, JdbcType jdbcType)的实现里会先查typeHandlerMap.get(type)如果为空再尝试从所有已注册处理器中找到能处理该类型的那一个。这个“找到能处理”的逻辑比较复杂它不只是看 javaType还看MappedJdbcTypes里的 jdbcType 是否匹配。所以你会发现一种很有趣的情况两个 typeHandler 都标了MappedTypes同一个 Java 类型但一个限定MappedJdbcTypes(JdbcType.VARCHAR)另一个限定MappedJdbcTypes(JdbcType.INTEGER)在读取不同 jdbcType 列时MyBatis 会分别选中对应的处理器。这个机制对应很多实用场景例如同一个枚举字段在 A 表里存 varchar、B 表里存 int。但如果两个处理器的MappedJdbcTypes是同一个值且都注册到了同一个 Java 类型那就可能出现覆盖关系。MyBatis 一般不会强校验注册顺序后到的大概率会覆盖先到的这会导致线上行为无法预测。所以在团队协作里自定义 typeHandler 必须有明确的包规范不能各写各的。4.3 自动映射为什么有时失效还有一个高频问题明明数据库有一列叫remarkJava 属性也有remark自动映射就是不生效。这种问题一般和 typeHandler 没关系但我也遇到过确实是因为类型不匹配导致 MyBatis 无法找到合适的处理器。比如数据库列是CLOBJava 属性是StringMyBatis 看到 jdbcType 为 CLOB去找能处理 CLOB 的处理器默认是有的。但如果你自己自定义了 String 的处理链干扰了注册就可能出问题。最稳妥的做法是不要全局注册一个覆盖String.class的自定义处理器。String 类型的使用面太广全局覆盖会让所有字符串字段的读写都走你的处理器风险极高。真要处理某个特殊类型的字符串内容建议做字段级指定。5. 高频踩坑实录与排查技巧5.1 问题一自定义枚举处理器没有生效表现SQL 执行成功但数据库里存的值是枚举的name()而不是你期望的code字段。排查思路确认自定义处理器类上是否标注了MappedTypes(OrderStatus.class)。确认typeHandlersPackage是否覆盖到了该处理器所在包。确认是否有另一个相同类型的处理器被注册存在覆盖关系。如果该字段在 resultMap 或#{}里显式指定了 typeHandler那么全局注册不会参与显式指定的优先级最高。我个人遇到的案例项目里旧代码直接用了MappedTypes(Enum.class)做全局枚举处理新代码又注册了OrderStatusTypeHandler。结果是新处理器有时生效、有时不生效因为注册顺序和匹配逻辑不确定。最后把所有枚举处理器统一成按具体枚举类注册世界清净了。5.2 问题二查询返回时枚举变 null 但不报错表现insert 正常select 返回的枚举对象为 null控制台没有任何异常。这种问题的关键通常不在 typeHandler 注册而在ResultSet里拿到的值和你的fromCode逻辑对不上。比如数据库里存的是1你的转换逻辑只认online、offline拿到1时抛了异常。但 MyBatis 的BaseTypeHandler.getNullableResult默认包装了异常为TypeException如果外层代码又把这个异常吞了就会看到 null。排查时可以先把 SQL 拿出来手动跑一遍看看原始存储值到底是什么。这一步往往能直接定位问题。5.3 问题三typeHandler 在插件拦截之后不生效如果你写过 MyBatis 插件对ParameterHandler或ResultSetHandler做了拦截那么 typeHandler 的生效位置就会被影响。举个例子插件拦截了ResultSetHandler.handleResultSets之后直接改了返回的 List那 MyBatis 里的 typeHandler 转换可能根本没走到。这种问题不好排查因为代码层面看起来一切正常。我的建议是插件里尽量不要绕开原生逻辑去改写结果除非你对 MyBatis 的调用链有绝对把握。typeHandler 是框架默认的处理机制任何绕过它的操作都可能引入隐蔽问题。6. 实战进阶JSON 字段与元注解的最佳实践6.1 用 typeHandler 存 JSON现在很多业务表里都会有扩展字段存放结构化的 JSON 字符串。常见的做法是定义一个JSONTypeHandlerMappedTypes(Object.class) public class JsonTypeHandler extends BaseTypeHandlerObject { // 写入时 object - json string // 读取时 json string - object }但这类处理器有一个很难绕过的坑Java 类型是Object.class时读取返回的反序列化目标类型不一定是你想要的。比如ListOrderItem和MapString, Object都是 Object 的运行时类型但反序列化方式完全不同。如果要支持泛型你需要借助 Jackson 的TypeReference把目标 Java 类型传递进来。在 MyBatis 的 ResultMap 映射中你可以通过typeHandler指定但 typeHandler 本身拿不到字段的泛型信息。MyBatis 解析 resultMap 时会把property对应的属性类型设置为javaType。如果你的属性是ListOrderItem实际 javaType 是List.class不是ListOrderItem的完整泛型。所以泛型信息会丢失。经常有人为了解决这个问题去写一个复合 typeHandler内部根据parameterObject的类型做判断。我建议不要这么做太脆弱。更好的方案是JSON 字段不要用泛型集合作为属性类型干脆用 JavaBean 包装一层。比如class ExtInfo { private ListOrderItem items; }这样属性类型是明确的JSON 处理器可以直接反序列化成ExtInfo。6.2 组合注解让映射更简洁在注解开发模式下你可以在方法上这么写Insert(insert into orders(status) values(#{status})) int insert(Param(status) MappedTypes(OrderStatusTypeHandler.class) OrderStatus status);实际上Param里并没有typeHandler属性你需要通过TypeHandler注解指定。MyBatis 从 3.5.x 开始提供了TypeHandler注解可以直接放在参数或属性上int insert(Param(status) TypeHandler(OrderStatusTypeHandler.class) OrderStatus status);这种写法比每次在 XML 里写完整类名简洁不少也更容易让后来人看懂。注解扫描不会注册处理器只是指定当前参数使用哪个处理器。它的优先级高于注册表里根据 javaType 自动选中的处理器。6.3 JSR-310 时间类型为什么需要额外配置MyBatis 3.4.5 之后默认支持 JSR-310 时间类型的处理器但如果你用的版本比较老或者某些方言数据库里日期时间类型处理有差异仍然可能需要自定义。常见的自定义场景是数据库存TIMESTAMPJava 用LocalDateTime但希望统一转成 UTC 字符串存储。这时你就要写一个继承BaseTypeHandlerLocalDateTime的处理器内部用DateTimeFormatter格式化。要注意的是LocalDateTime在 JDBC 4.2 的驱动里其实有原生支持PreparedStatement.setObject就能直接传。MyBatis 的默认处理器也是基于这个能力。自定义时不要做太多字符串转换保持简单否则容易引入时区问题。6.4 一个朴素但重要的建议自定义 typeHandler 的单元测试不能只测转换逻辑还要在完整 MyBatis 上下文中测试。你可以在测试里用内存数据库跑真实的 mapper 方法验证 insert 和 select 后值是否一致。很多处理器写方向没问题、读方向有问题就是因为只测了setNonNullParameter没测getNullableResult的下标取值分支。7. TypeHandlerRegistry 里的一些剩余细节回到源码TypeHandlerRegistry在初始化时还会注册很多内置处理器的别名。你可以通过Configuration.getTypeHandlerRegistry()随时查看当前环境里有哪些处理器Configuration config sqlSessionFactory.getConfiguration(); TypeHandlerRegistry registry config.getTypeHandlerRegistry(); TypeHandler? handler registry.getTypeHandler(OrderStatus.class);这个getTypeHandler在排错时很好用。我曾经排查过一个诡异问题同一条 SQL两个环境跑出来的枚举值不同。后来发现是一个环境里 classpath 多了某个旧版本的 MyBatis 扩展包里面自带枚举处理器注册顺序靠后把我们的自定义处理器覆盖了。通过打印allTypeHandlersMap一下子就看到了两个处理器同时存在问题立刻定位。另外如果你想在 Spring Boot 的MybatisAutoConfiguration环境里注册 typeHandler除了 XML 之外还可以在配置类里用ConfigurationCustomizer手动注册Bean public ConfigurationCustomizer mybatisConfigurationCustomizer() { return configuration - { TypeHandlerRegistry registry configuration.getTypeHandlerRegistry(); registry.register(OrderStatus.class, new OrderStatusTypeHandler()); }; }这种方式适合不想走包扫描、希望显式掌控注册顺序的场景。但要注意ConfigurationCustomizer的执行时机要早于 mapper 的解析否则在某些情况下 mapper 里的 resultMap 可能已经构建了没法再重新匹配。这一点在 Spring Boot 集成时尤其明显。8. 从源码机制反推设计原则读了 typeHandler 相关源码之后我对它的设计有几点体会可能对你在项目里做技术选也有帮助。第一typeHandler的匹配机制是“先 javaType再 jdbcType最后显式指定”。理解了这条优先级就不会在配置多重指定时自乱阵脚。显式指定的 typeHandler 永远最优先其次是根据 javaType 选择处理器再次才是根据 jdbcType 匹配默认处理器。第二MappedTypes和MappedJdbcTypes是元数据不是注册动作。只加注解不扫描不注册不会自动生效。很多人误以为加了MappedTypes就算注册了其实这只是给了扫描机制一个信息。要生效必须有typeHandlersPackage配置或者手动register。第三TypeHandlerRegistry的 key 设计并不是一个完整的二维矩阵。它内部有不同的 map 组合按 javaType 查、按 jdbcType 查都有各自的查找路径。你要是真的想彻底搞懂最好的办法是打开源码把getTypeHandler(Class? type, JdbcType jdbcType)这个方法的每一种分支走一遍比看十篇解析文章都管用。第四也是最重要的一条typeHandler 的粒度是“类型级”的不是“字段级”的。虽然你可以在字段上显式指定但那只是给某个调用点开了口子。全局注册仍然会作用在所有同类型字段上。因此如果你的转换规则只适用于少数几个字段尽量用字段级指定不要全局注册。我在实际项目里见过太多因为全局注册了一个枚举处理器导致所有枚举字段都按同一套规则转换的案例。某个枚举新加了枚举值数据库里存的值和 Java 枚举的 code 对应不上于是线上开始出现数据读取异常。全局规则必然会放大局部设计的失误。9. 结尾前的一点个人心得最开始我总认为typeHandler 只是个“写几行代码加个配置”的小工具后来才发现它关系到 MyBatis 参数绑定和结果映射两大核心链路。你把它的源码读明白了很多看似玄学的报错其实都有明确出处。比如TypeException: Error setting non null for parameter异常信息里就明确提示你检查 jdbcType 配置。如果不去看BaseTypeHandler的源码你可能永远不知道为什么一个 null 值会触雷。如果你想验证自己的理解我推荐一个做法打开TypeHandlerRegistry源码自己列一张表格把当前项目里涉及到的 Java 类型、对应的 JdbcType、期望的 typeHandler 列出来然后逐个确认。我用这个表格清理过好几个项目的自定义处理器冲突效果好得出奇。有些时候问题不是你的 processor 写得不对而是注册了太多版本系统里同时存在几个“看似对”的处理器。从实际开发和维护的角度我的最终建议很简单不要图省事把 typeHandler 当作万能膏药。能用默认处理器就用默认必须自定义时优先考虑字段级指定少做全局注册永远显式标注MappedTypes。把这些原则记牢MyBatis 的 typeHandler 不仅不会给你添乱反而能成为你手里非常利落的一套扩展工具。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑