如何自定义 MyBatis枚举处理器(EnumTypeHandler):分享从 “同包同名 shadow 覆盖” 到 “官方扩展点” 的重构经历
场景:
sby-component-mybatisplus组件中自定义 MyBatis 枚举处理器,从"同包同名 shadow 覆盖"重构为"setDefaultEnumTypeHandler官方扩展点注册"。本文记录两种方案的原理、对比与关于健壮性代码的思考。
一、背景:为什么需要自定义“mybatis枚举处理器”
我们的业务系统里,数据库枚举字段的存储形式存在如下不统一的情况:
- 有的存枚举名:
SIGN - 有的存小写:
sign - 有的存业务 code:
4
MyBatis 默认的 EnumTypeHandler 只支持按枚举名精确匹配,读到 sign 或 4 会直接抛 IllegalArgumentException,导致历史脏数据、跨系统数据无法正常读取。
一个健壮的系统需要兼容这种“异常”数据的情况。
二、初版方案:同包同名 Shadow 覆盖
彼时的2023年8月,我决定在我们项目底层 sby-component-mybatisplus 组件里实现一个兼容处理器,匹配策略:
- 优先按
Enum.name()精确匹配; - 失败后按忽略大小写匹配;
- 若枚举实现了
EnumAbility,再尝试按 code 匹配。
2.1 实现方式-shadow模式
为了让 MyBatis 的枚举映射自动走到自定义处理器,最初的思路非常直接:在组件 jar 里定义一个与 MyBatis 官方类完全同名同包的类:
// sby-component-mybatisplus/src/main/java/org/apache/ibatis/type/EnumTypeHandler.java(旧)
package org.apache.ibatis.type;
/**与 mybatis jar 中的 org.apache.ibatis.type.EnumTypeHandler 同名同包
* @author gz.zhang
* <p>见 mybatis-3.5.x.jar 中的同名class。 可以点击{@link org.apache.ibatis.type.EnumOrdinalTypeHandler}进行寻找</p>
*/
public class EnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
String colValue = rs.getString(columnName);
return colValue == null ? null : enumValueOf(type, colValue);
}
// 其他重载方法...
// 兼容策略:name -> 忽略大小写 -> EnumAbility.code
public static <E extends Enum<E>> E enumValueOf(Class<E> _enumType, String name) {
if (name == null || "".equals(name)) return null;
try {
return Enum.valueOf(_enumType, name.trim());
} catch (IllegalArgumentException e) {
// 1. 忽略大小写匹配
for (E enumConstant : _enumType.getEnumConstants()) {
if (enumConstant.name().equalsIgnoreCase(name)) {
return enumConstant;
}
}
// 2. 实现 EnumAbility 的枚举按 code 匹配
if (EnumAbility.class.isAssignableFrom(_enumType)) {
for (E enumConstant : _enumType.getEnumConstants()) {
EnumAbility<Object> enumAbility = (EnumAbility) enumConstant;
Object code = name;
if (enumAbility.getCode() instanceof Integer) {
code = Integer.parseInt(name);
}
if (enumAbility.codeEquals(code)) {
return enumConstant;
}
}
}
log.warn("从db读取数据,枚举转换失败--字段值={},期望枚举是:{}", name, _enumType.getName());
}
return null;
}
}
2.2 生效原理:靠 classpath 顺序"巧合"生效
JVM 加载类时按 classpath 从前到后查找第一个匹配的类。因此只要组件 jar 在 classpath 中排在 mybatis jar 之前,org.apache.ibatis.type.EnumTypeHandler 就会被加载成我们的版本。
而 classpath 顺序由 Maven 依赖解析顺序决定(依赖声明顺序 + 间接依赖解析规则),所以这个方案隐含地绑定在 pom.xml 的依赖声明顺序上:
<!-- 只有当 sby-component-mybatisplus 声明在 mybatis 相关依赖之前时,覆盖才生效 -->
<dependency>
<groupId>com.serviceshare</groupId>
<artifactId>sby-component-mybatisplus</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
2.3 为什么说这是"巧合编程"
| 风险 | 说明 |
|---|---|
| 依赖顺序敏感 | 任何人调整 pom 中依赖的先后顺序,覆盖立即静默失效,MyBatis 恢复默认行为 |
| 间接依赖不可控 | Maven 对传递依赖的排序规则并不直观,改一个无关模块都可能影响全局 classpath |
| 环境不一致 | IDE 运行、单测、打包产物的 classpath 可能不同,行为不一致且极难排查 |
| 拆分包(split package) | org.apache.ibatis.type 同时出现在两个 jar 中;JDK 9+ 模块系统(JPMS)直接禁止,jdeps/shade 等工具告警 |
| 升级脆断 | 升级 MyBatis 小版本,官方类一旦有变化,覆盖可能以不可预期的方式失效 |
| 可读性差 | 新人看到 EnumTypeHandler 以为就是官方类,实际被偷偷替换,认知成本极高 |
近期有同事在一个上层应用pom中增加了高版本的mybatis依赖,直接导致这个应用中的`EnumTypeHandler` shadow覆盖失效,导致部分业务功能不可用。因此,这种靠巧合的实现方式存在隐患。
三、重构方案:利用mybatis官方扩展点 setDefaultEnumTypeHandler
3.1 思路转变→ 我们不需要"顶替"MyBatis 的类,只需要告诉 MyBatis"用我的类作为默认枚举处理器"
经查资料,MyBatis 官方早已提供该扩展点:
// org.apache.ibatis.session.Configuration
public void setDefaultEnumTypeHandler(Class<? extends TypeHandler> typeHandler)
于是重构分两步:
- 挪包:把自定义类从
org.apache.ibatis.type迁移到自有包com.serviceshare.mybatisplus.typehandler。类名可以保持EnumTypeHandler(与官方类名相同没问题,因为包不同就是两个完全不同的类),关键是彻底脱离第三方包的命名空间。 - 显式注册:通过 MyBatis-Plus 的
ConfigurationCustomizer在应用启动时注册为默认枚举处理器。
这样旧的生产代码目录
org/apache/ibatis/type/已被删除,拆分包从根上消除。
3.2 代码
自定义处理器(已迁到自有包):
// com/serviceshare/mybatisplus/typehandler/EnumTypeHandler.java(新)
package com.serviceshare.mybatisplus.typehandler;
import org.apache.ibatis.type.BaseTypeHandler;
@Slf4j
public class EnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
// 兼容逻辑与原来完全一致:name -> 忽略大小写 -> EnumAbility.code
// 虽名为 EnumTypeHandler,但位于自有包,与官方的 org.apache.ibatis.type.EnumTypeHandler 是完全不同的两个类
}
注册点(MyBatisPlusComponentConfig):
@Bean
public ConfigurationCustomizer enumDefaultTypeHandlerCustomizer() {
// 用自定义枚举处理器替换 MyBatis 默认的 EnumTypeHandler(兼容 name / EnumAbility.code / 字符串)。
// 通过官方扩展点显式注册,不再依赖“同包同名 shadow”覆盖,避免拆分包与类加载顺序问题。
return configuration -> configuration.setDefaultEnumTypeHandler(
com.serviceshare.mybatisplus.typehandler.EnumTypeHandler.class);
}
四、两种方案对比
| 维度 | Shadow(同包同名覆盖) | 官方扩展点注册 |
|---|---|---|
| 生效机制 | classpath 顺序巧合 | 显式配置注册 |
| 确定性 | 依赖 pom 声明顺序,脆弱 | 确定、可预期 |
| 依赖顺序敏感性 | 敏感,调顺序即失效 | 不敏感 |
| 同包同名冲突 | 是(拆分包) | 否,类已迁入自有包,拆分包根除 |
| 框架升级影响 | 可能静默失效 | 官方扩展点持续兼容 |
| 可排查性 | 排查 classpath,困难 | 代码可见,易定位 |
| 规范合规 | 违反 JPMS 包唯一性 | 符合 |
| 团队可维护性 | 靠"约定"维持,易被无意破坏 | 代码自文档化 |
为什么更健壮
- 显式:注册行为在代码中一目了然,不依赖任何 classpath 偶然性,注释写明意图。
- 确定:无论依赖顺序、打包方式如何变化,行为始终一致。
- 官方通路:这是框架留给使用者的正规扩展点,随版本演进持续兼容。
- 彻底消除拆分包:类已迁出
org.apache.ibatis.type,生产代码里不再有任何org/apache/ibatis/type/下的自定义类,split package 问题从根上消失;类名尽管仍叫EnumTypeHandler,但因位于自有包,与官方类是两个不同的类,JVM 不会混淆。 - 与 MyBatis-Plus 枚举机制兼容:
setDefaultEnumTypeHandler只作为"默认值"兜底,字段若配置了@EnumValue或实现了IEnum,仍走 MyBatis-Plus 自身的处理器,两者互不干扰。
五、结语
这次重构没有增加任何新功能——兼容逻辑原封不动,只改了类的包路径和注册方式(从"顶替官方类"改为"通过官方扩展点注册")。但正是这两点改动,把一段"碰巧能跑"的代码,变成了"必然能跑"的代码。
如果某个行为的正确性取决于"顺序"、"恰好"、"没人动它",那它就是脆弱的。 健壮性不是"代码不出错",而是"出错也容易发现,升级也不怕,重构也不碎"。
识别代码中的"shadow 式陷阱"
任何"顶替框架类"的方案,本质上都是把第三方库当成了可以随意修改的代码。正确姿势永远是:先查官方扩展点。MyBatis 有 setDefaultEnumTypeHandler,Spring 有 @Primary / @ConditionalOnMissingBean,SPI 有 ServiceLoader 的排序规范……框架几乎总为"定制默认行为"留有正规通路。
| 场景 | 不健壮的做法 | 健壮的做法 |
|---|---|---|
| 枚举处理器 | 同包同名顶替官方类 | setDefaultEnumTypeHandler + 自有包类名 |
| Spring Bean 覆盖 | 同名 Bean 靠注册顺序胜出 | @Primary、@ConditionalOnMissingBean 显式声明 |
| 配置覆盖 | 依赖 spring.factories 加载顺序 |
使用官方配置属性 / EnvironmentPostProcessor |
| 静态方法替换 | 反射修改 final 字段 / 字节码注入 | 框架扩展接口(如 TypeHandler、Interceptor) |
| 依赖版本 | 靠传依赖"恰好"解析到想要的版本 | 显式声明 dependencyManagement 锁定 |
当看到一些不好的代码时,会发现我还算优秀;当看到优秀的代码时,也才意识到持续学习的重要!--buguge
本文来自博客园,转载请注明原文链接:https://www.cnblogs.com/buguge/p/22684773
浙公网安备 33010602011771号