1. 概览
枚举作为 Java 5 的重要特征,相信大家并不陌生,但在实际开发过程中,当 name 和 ordrial 发生变化时,如果处理不当非常容易引起系统bug。这种兼容性bug非常难以定位,需要从框架层次进行避免,而非仅靠开发人员的主观意识。
1.1. 背景
枚举很好用,特别是提供的 name 和 ordrial 特性,但这点对重构造成了一定影响,比如:
- 某个枚举值业务语义发生变化,需要将其进行 rename 操作,以更好的表达新业务语义
- 新增、删除或者为了展示调整了枚举定义顺序
这些在业务开发中非常常见,使用 IDE 的 refactor 功能可以快速且准确的完成重构工作。但,如果系统将这些暴露出去或者存储到数据库等存储引擎就变得非常麻烦,不管是 name 还是 ordrial 的变更都会产生兼容性问题。
对此,最常见的解决方案便是放弃使用 name 和 ordrial,转而使用控制能力更强的 code。
1.2. 目标
提供一组工具,以方便的基于 code 使用枚举,快速完成对现有框架的集成:
- 完成与 Spring MVC 的集成,基于 code 使用枚举;加强返回值,以对象的方式进行返回,信息包括 code、name、description
- 提供统一的枚举字典,自动扫描系统中的枚举并将其以 restful 的方式暴露给前端
- 使用 code 进行数据存储操作,避免重构的影响
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
2. 快速入门
2.1. 添加 starter
在 Spring boot 项目的 pom 中增加如下依赖:
<groupId>com.geekhalo.lego</groupId> <artifactId>lego-starter</artifactId> <version>0.1.19-enum-SNAPSHOT</version>
2.2. 统一枚举结构
如何统一枚举行为呢?公共父类肯定是不行的,但可以为其提供一个接口,在接口中完成行为的定义。
2.2.1. 定义枚举接口
除了在枚举中自定义 code 外,通常还会为其提供描述信息,构建接口如下:
public interface CodeBasedEnum { int getCode(); } public interface SelfDescribedEnum { default String getName(){ return name(); } String name(); String getDescription(); } public interface CommonEnum extends CodeBasedEnum, SelfDescribedEnum{ }
整体结构如下:
在定义枚举时便可以直接使用CommonEnum这个接口。
2.2.2. 实现枚举接口
有了统一的枚举接口,在定义枚举时便可以直接实现接口,从而完成对枚举的约束。
public enum NewsStatus implements CommonEnum { DELETE(1, "删除"), ONLINE(10, "上线"), OFFLINE(20, "下线"); private final int code; private final String desc; NewsStatus(int code, String desc) { this.code = code; this.desc = desc; } @Override public int getCode() { return this.code; } @Override public String getDescription() { return this.desc; } }
2.3. 自动注册 CommonEnum
有了统一的 CommonEnum 最大的好处便是可以进行统一管理,对于统一管理,第一件事便是找到并注册所有的 CommonEnum。
以上是核心处理流程:
- 首先通过 Spring 的 ResourcePatternResolver 根据配置的 basePackage 对classpath进行扫描
- 扫描结果以Resource来表示,通过 MetadataReader 读取 Resource 信息,并将其解析为 ClassMetadata
- 获得 ClassMetadata 之后,找出实现 CommonEnum 的类
- 将 CommonEnum 实现类注册到两个 Map 中进行缓存
备注:此处万万不可直接使用反射技术,反射会触发类的自动加载,将对众多不需要的类进行加载,从而增加 metaspace 的压力。
在需要 CommonEnum 时,只需注入 CommonEnumRegistry Bean 便可以方便的获得 CommonEnum 的具体实现。
2.4. Spring MVC 接入层
Web 层是最常见的接入点,对于 CommonEnum 我们倾向于:
- 参数使用 code 来表示,避免 name、ordrial 变化导致业务异常
- 丰富返回值,包括枚举的 code、name、description 等
2.4.1. 入参转化
Spring MVC 存在两种参数转化扩展:
- 对于普通参数,比如 RequestParam 或 PathVariable 直接从 ConditionalGenericConverter 进行扩展
- 基于 CommonEnumRegistry 提供的 CommonEnum 信息,对 matches 和 getConvertibleTypes方法进行重写
- 根据目标类型获取所有的 枚举值,并根据 code 和 name 进行转化
- 对于 Json 参数,需要对 Json 框架进行扩展(以 Jackson 为例)
- 遍历 CommonEnumRegistry 提供的所有 CommonEnum,依次进行注册
- 从 Json 中读取信息,根据 code 和 name 转化为确定的枚举值
两种扩展核心实现见:
@Order(1) @Component public class CommonEnumConverter implements ConditionalGenericConverter { @Autowired private CommonEnumRegistry enumRegistry; @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { Class<?> type = targetType.getType(); return enumRegistry.getClassDict().containsKey(type); } @Override public Set<ConvertiblePair> getConvertibleTypes() { return enumRegistry.getClassDict().keySet().stream() .map(cls -> new ConvertiblePair(String.class, cls)) .collect(Collectors.toSet()); } @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { String value = (String) source; List<CommonEnum> commonEnums = this.enumRegistry.getClassDict().get(targetType.getType()); return commonEnums.stream() .filter(commonEnum -> commonEnum.match(value)) .findFirst() .orElse(null); } } static class CommonEnumJsonDeserializer extends JsonDeserializer{ private final List<CommonEnum> commonEnums; CommonEnumJsonDeserializer(List<CommonEnum> commonEnums) { this.commonEnums = commonEnums; } @Override public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException { String value = jsonParser.readValueAs(String.class); return commonEnums.stream() .filter(commonEnum -> commonEnum.match(value)) .findFirst() .orElse(null); } }
2.4.2. 增强返回值
默认情况下,对于枚举类型在转换为 Json 时,只会输出 name,其他信息会出现丢失,对于展示非常不友好,对此,需要对 Json 序列化进行能力增强。
首先,需要定义 CommonEnum 对应的返回对象,具体如下:
@Value @AllArgsConstructor(access = AccessLevel.PRIVATE) @ApiModel(description = "通用枚举") public class CommonEnumVO { @ApiModelProperty(notes = "Code") private final int code; @ApiModelProperty(notes = "Name") private final String name; @ApiModelProperty(notes = "描述") private final String desc; public static CommonEnumVO from(CommonEnum commonEnum){ if (commonEnum == null){ return null; } return new CommonEnumVO(commonEnum.getCode(), commonEnum.getName(), commonEnum.getDescription()); } public static List<CommonEnumVO> from(List<CommonEnum> commonEnums){ if (CollectionUtils.isEmpty(commonEnums)){ return Collections.emptyList(); } return commonEnums.stream() .filter(Objects::nonNull) .map(CommonEnumVO::from) .filter(Objects::nonNull) .collect(Collectors.toList()); } }
CommonEnumVO 是一个标准的 POJO,只是增加了 Swagger 相关注解。
CommonEnumJsonSerializer 是自定义序列化的核心,会将 CommonEnum 封装为 CommonEnumVO 并进行写回,具体如下:
static class CommonEnumJsonSerializer extends JsonSerializer{ @Override public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { CommonEnum commonEnum = (CommonEnum) o; CommonEnumVO commonEnumVO = CommonEnumVO.from(commonEnum); jsonGenerator.writeObject(commonEnumVO); } }
2.4.3. 效果展示
首先,新建一个测试枚举 NewsStatus,具体如下:
public enum NewsStatus implements CommonEnum { DELETE(1, "删除"), ONLINE(10, "上线"), OFFLINE(20, "下线"); private final int code; private final String desc; NewsStatus(int code, String desc) { this.code = code; this.desc = desc; } @Override public int getCode() { return this.code; } @Override public String getDescription() { return this.desc; } }
然后新建 EnumController,具体如下:
@RestController @RequestMapping("enum") public class EnumController { @GetMapping("paramToEnum") public RestResult<CommonEnumVO> paramToEnum(@RequestParam("newsStatus") NewsStatus newsStatus){ return RestResult.success(CommonEnumVO.from(newsStatus)); } @GetMapping("pathToEnum/{newsStatus}") public RestResult<CommonEnumVO> pathToEnum(@PathVariable("newsStatus") NewsStatus newsStatus){ return RestResult.success(CommonEnumVO.from(newsStatus)); } @PostMapping("jsonToEnum") public RestResult<CommonEnumVO> jsonToEnum(@RequestBody NewsStatusRequestBody newsStatusRequestBody){ return RestResult.success(CommonEnumVO.from(newsStatusRequestBody.getNewsStatus())); } @GetMapping("bodyToJson") public RestResult<NewsStatusResponseBody> bodyToJson(){ NewsStatusResponseBody newsStatusResponseBody = new NewsStatusResponseBody(); newsStatusResponseBody.setNewsStatus(Arrays.asList(NewsStatus.values())); return RestResult.success(newsStatusResponseBody); } @Data public static class NewsStatusRequestBody { private NewsStatus newsStatus; } @Data public static class NewsStatusResponseBody { private List<NewsStatus> newsStatus; } }
执行结果如下:
整体符合预期:
- 使用 code 作为请求参数可以自动转化为对应的 CommonEnum
- 使用 CommonEnum 作为返回值,返回标准的 CommonEnumVO 对象结构