尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

如何自定义 MyBatis枚举处理器(EnumTypeHandler):分享从 “同包同名 shadow 覆盖” 到 “官方扩展点” 的重构经历

如何自定义 MyBatis枚举处理器(EnumTypeHandler):分享从 “同包同名 shadow 覆盖” 到 “官方扩展点” 的重构经历 场景sby-component-mybatisplus组件中自定义 MyBatis 枚举处理器从同包同名 shadow 覆盖重构为setDefaultEnumTypeHandler官方扩展点注册。本文记录两种方案的原理、对比与关于健壮性代码的思考。一、背景为什么需要自定义“mybatis枚举处理器”我们的业务系统里数据库枚举字段的存储形式存在如下不统一的情况有的存枚举名SIGN有的存小写sign有的存业务 code4MyBatis 默认的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 EnumTypeHandlerE extends EnumE extends BaseTypeHandlerE { 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 EnumE E enumValueOf(ClassE _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()) { EnumAbilityObject 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 中排在mybatisjar 之前org.apache.ibatis.type.EnumTypeHandler就会被加载成我们的版本。而 classpath 顺序由 Maven 依赖解析顺序决定(依赖声明顺序 间接依赖解析规则)所以这个方案隐含地绑定在 pom.xml 的依赖声明顺序上!-- 只有当 sby-component-mybatisplus 声明在 mybatis 相关依赖之前时覆盖才生效 -- dependency groupIdcom.serviceshare/groupId artifactIdsby-component-mybatisplus/artifactId version1.0.0-SNAPSHOT/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId /dependency2.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官方扩展点 setDefaultEnumTypeHandler3.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 EnumTypeHandlerE extends EnumE extends BaseTypeHandlerE { // 兼容逻辑与原来完全一致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 有setDefaultEnumTypeHandlerSpring 有Primary/ConditionalOnMissingBeanSPI 有ServiceLoader的排序规范……框架几乎总为定制默认行为留有正规通路。场景不健壮的做法健壮的做法枚举处理器同包同名顶替官方类setDefaultEnumTypeHandler 自有包类名Spring Bean 覆盖同名 Bean 靠注册顺序胜出Primary、ConditionalOnMissingBean显式声明配置覆盖依赖spring.factories加载顺序使用官方配置属性 /EnvironmentPostProcessor静态方法替换反射修改 final 字段 / 字节码注入框架扩展接口(如TypeHandler、Interceptor)依赖版本靠传依赖恰好解析到想要的版本显式声明dependencyManagement锁定
返回列表