Java枚举如何复用构造逻辑与泛型字段 及Jackson反序列化方案
问题背景
- 项目所有功能模块均继承自
MyFeature,支持全局、分公会维度的开关配置 - 部分功能携带继承自
MyAddon的子功能(addon):子功能仅在父功能启用时生效,同样支持全局、分公会维度的开关配置 - 原有实现为每个带addon的功能定义独立枚举类,枚举值负责罗列可用addon、标记是否支持配置、持有对应addon类的无参构造器,适配Jackson完成YAML字符串到addon类的反序列化
- 现存痛点:不同功能的addon枚举类基础逻辑高度重复,但Java枚举默认继承
Enum类,无法继承自定义抽象类复用逻辑;接口无法持有实例级泛型字段、无法统一构造逻辑,强行用接口默认方法依然需要每个枚举独立维护字段,无法达到减少重复代码的目的。重复代码模板如下:
public enum ---Addons { example_addon(true, ExampleAddon.class); // 不同addon枚举类的枚举值定义互不相同 public final boolean configurable; public final Constructor<? extends ---Addon> handler; ---Addons(boolean configurable, Class<? extends ---Addon> addonHandler) { this.configurable = configurable; if (addonHandler == null) { handler = null; } else { Constructor<? extends ---Addon> setHandler; try { setHandler = addonHandler.getConstructor(); } catch (NoSuchMethodException e) { MyLogger.error("Could not find constructor for addon {}",name()); setHandler = null; } handler = setHandler; } } }
注:代码中
---为对应addon枚举的名称占位符,不同addon枚举类名称不同,同一枚举类内该名称保持统一。
可行解决方案
提供两种可落地的方案,均不需要修改现有YAML配置格式,兼容原有Jackson反序列化逻辑。
方案1:枚举+委托模式(最小改动,兼容现有枚举逻辑)
不需要强行突破枚举的继承限制,将重复的构造器解析、字段存储逻辑抽到独立的通用元数据类中,每个枚举仅做极薄的一层委托调用即可,重复代码量可以压到最低。
首先实现通用的addon元数据持有类,所有枚举共用这一套逻辑:
// 全局通用addon元数据类,所有重复逻辑全部收敛到这里 public final class AddonMetadata<A extends MyAddon> { public final boolean configurable; public final Constructor<? extends A> handler; public AddonMetadata(boolean configurable, Class<? extends A> addonHandler) { this.configurable = configurable; if (addonHandler == null) { this.handler = null; return; } Constructor<? extends A> setHandler; try { setHandler = addonHandler.getConstructor(); } catch (NoSuchMethodException e) { MyLogger.error("Could not find no-arg constructor for addon {}", addonHandler.getName()); setHandler = null; } this.handler = setHandler; } }
改造后的addon枚举仅需要声明枚举值、持有元数据实例,构造方法直接委托给通用类即可,原几十行重复的构造解析逻辑全部消除:
// 以某一个具体功能的Addon枚举为例 public enum FeatureXAddons { example_addon(true, ExampleAddon.class), another_addon(false, AnotherAddon.class); public final AddonMetadata<FeatureXAddon> metadata; FeatureXAddons(boolean configurable, Class<? extends FeatureXAddon> addonHandler) { // 所有逻辑委托给通用元数据类,无重复代码 this.metadata = new AddonMetadata<>(configurable, addonHandler); } }
原有业务代码仅需要把addon.configurable替换为addon.metadata.configurable、addon.handler替换为addon.metadata.handler即可,Jackson原生枚举反序列化逻辑完全不需要改动,兼容性拉满。
方案2:类+静态常量模拟枚举(零重复代码,扩展性更强)
如果可以接受放弃原生枚举,可以用final类加静态常量的方式实现和枚举完全一致的语义,同时支持继承通用抽象基类,彻底消除所有重复代码。
首先实现通用抽象基类,所有公共逻辑、反序列化查找逻辑全部收敛到基类:
public abstract class BaseAddonType<A extends MyAddon> { public final boolean configurable; public final Constructor<? extends A> handler; // 全局缓存:按addon基类分类,存储id与addon类型的映射,供反序列化查找 private static final Map<Class<?>, Map<String, BaseAddonType<?>>> TYPE_CACHE = new ConcurrentHashMap<>(); protected BaseAddonType(String id, boolean configurable, Class<? extends A> addonHandler, Class<A> addonBaseClass) { this.configurable = configurable; // 构造器解析逻辑统一放在基类,子类不需要重复写 if (addonHandler == null) { this.handler = null; } else { Constructor<? extends A> setHandler; try { setHandler = addonHandler.getConstructor(); } catch (NoSuchMethodException e) { MyLogger.error("Could not find no-arg constructor for addon {}", id); setHandler = null; } this.handler = setHandler; } // 自动注册到缓存,不需要额外手动注册 TYPE_CACHE.computeIfAbsent(addonBaseClass, k -> new HashMap<>()) .put(id.toLowerCase(Locale.ROOT), this); } // 通用反序列化查找方法 @SuppressWarnings("unchecked") public static <A extends MyAddon> BaseAddonType<A> getById(Class<A> addonBaseClass, String id) { return (BaseAddonType<A>) TYPE_CACHE.getOrDefault(addonBaseClass, Collections.emptyMap()) .get(id.toLowerCase(Locale.ROOT)); } }
每个具体功能的addon类型集合只需要继承基类,声明静态常量即可,没有任何重复逻辑:
public final class FeatureXAddonType extends BaseAddonType<FeatureXAddon> { // 声明addon常量,写法和枚举值几乎一致 public static final FeatureXAddonType EXAMPLE_ADDON = new FeatureXAddonType("example_addon", true, ExampleAddon.class); public static final FeatureXAddonType ANOTHER_ADDON = new FeatureXAddonType("another_addon", false, AnotherAddon.class); // 构造方法私有,禁止外部随意实例化,和枚举的实例可控性一致 private FeatureXAddonType(String id, boolean configurable, Class<? extends FeatureXAddon> addonHandler) { super(id, configurable, addonHandler, FeatureXAddon.class); } }
最后实现一个轻量级的Jackson反序列化器,注册到ObjectMapper即可实现和枚举完全一致的字符串到类型引用的反序列化:
public class AddonTypeDeserializer<A extends MyAddon> extends JsonDeserializer<BaseAddonType<A>> implements ContextualDeserializer { private Class<A> addonBaseClass; @Override public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { JavaType contextualType = ctxt.getContextualType() != null ? ctxt.getContextualType() : property.getType(); JavaType addonType = contextualType.findTypeParameters(BaseAddonType.class)[0]; AddonTypeDeserializer<A> deserializer = new AddonTypeDeserializer<>(); deserializer.addonBaseClass = (Class<A>) addonType.getRawClass(); return deserializer; } @Override public BaseAddonType<A> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { String addonId = p.getValueAsString(); BaseAddonType<A> addonType = BaseAddonType.getById(addonBaseClass, addonId); if (addonType == null) { throw new JsonMappingException(p, "Invalid addon id '" + addonId + "' for base type " + addonBaseClass.getName()); } return addonType; } }
选型参考
- 若项目已经大量使用现有枚举逻辑、改动业务调用点成本较高,选择方案1,仅需新增一个通用元数据类,改动量极小
- 若为新开发模块、后续需要频繁给addon类型新增公共能力,选择方案2,扩展性更强,后续新增公共字段、方法仅需要修改基类即可
内容的提问来源于stack exchange,提问作者hopperelec
相关产品推荐
相关产品推荐

