枚举字典聚合
概述
Dante Cloud 选择了 Java 枚举的方式来做 字典,同时提供了支持跨模块的聚合管理方式来支持字典管理。这样做的好处有以下几点:
- Java 枚举可以很好的和 Java 代码融合,不管是作为类型分类、类型判断还是作为实体的参数都很方便。
- 使用枚举聚合的机制,实现了 Java 枚举的完全动态管理,无需手动录入数据。
- 利用 JPA 实体支持枚举属性以及支持使用枚举作为查询参数的特性,将 Java 枚举与实际 SQL 查询进行关联,而无需单独设计
字典表进行关联查询。 - 提供统一的接口,前端使用字典时
随用随取,并采用前端缓存避免反复查询后端。既提高的数据使用效率又避免大量查询或者缓存数据影响性能。
[一]Dante Cloud枚举聚合设计
Dante Cloud 枚举聚合仍旧采用了系统常量的两种方式:Customizer 设计模式和管理与聚合分离的数据模型设计
[1]Customizer 设计模式
因为 Dante Cloud 是高度模块化的系统,枚举可能分散在不同的模块之中。采用 Customizer 模式,将服务下的所有枚举聚合。
这一点与 Jackson 和错误码体系的方式完全相同。详见:【Spring Boot 中的 Customizer 模式】
[2]管理与聚合分离的数据模型设计
各服务下的枚举字典,会在服务启动后首先汇总至管理服务中的枚举聚合表(sys_dictionary)中,然后管理服务会分析新增的枚举信息并同步至枚举管理表(sys_enum)中进行管理。
这一点与 REST API 权限的管理与聚合完全相同。详见:【OAuth 2 中的鉴权和动态接口鉴权】
[二]枚举聚合开发
[1]定义枚举
Dante Cloud 中使用的枚举主要有两种类型:用于枚举聚合支持前端显示的枚举和仅用于后端代码逻辑的枚举
用于枚举聚合支持前端显示的枚举
用于枚举聚合支持前端显示的枚举,必须要继承 DictionaryEnum 接口。该接口主要有两个用途:
- 用途一:标记枚举的值,通过该种方式如果使用该枚举作为实体或者 DTO 的属性,无需编写 Jackson 反序列化处理
- 用途二:提供统一的聚合信息读取方式,拿到枚举可以直接获取到对应信息,不需要通过反射动态读取,提升代码性能
以下为支持枚举聚合的实例代码:
@Schema(name = "接口映射类别")
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum MappingCategory implements DictionaryEnum {
/**
* 权限表达式
*/
REST("REST", "REST API"),
GRPC("GRPC", "gRPC");
private static final Map<String, MappingCategory> INDEX_MAP = new HashMap<>();
private static final List<Dictionary> DICTIONARIES = new ArrayList<>();
static {
for (MappingCategory category : MappingCategory.values()) {
INDEX_MAP.put(category.name(), category);
DICTIONARIES.add(category.getDictionary(category.name(), category.ordinal()));
}
}
@Schema(name = "枚举值")
private final String value;
@Schema(name = "文字")
private final String label;
MappingCategory(String value, String label) {
this.value = value;
this.label = label;
}
public static MappingCategory get(String name) {
return INDEX_MAP.get(name);
}
public static List<Dictionary> getDictionaries() {
return DICTIONARIES;
}
@Override
public String getValue() {
return value;
}
@Override
public String getLabel() {
return label;
}
}说明:
value和label是必需的属性,除此以外你可以根据需要灵活增加其它属性。INDEX_MAP是一个内置字典,与枚举聚合无关,仅是为了快速从值找到对应的枚举。例如:上例中通过字符串REST就可以直接找到对应的 MappingCategory.REST 枚举项。DICTIONARIES是枚举项的快速生成错误,通过静态方法在该枚举实例化时就可以拿到该枚举中的所有条目,避免通过反射之类的操作再次遍历。
仅用于后端代码逻辑的枚举
仅用于后端代码逻辑的枚举,就是任意形式的 Java enum 类,可以随意定义。因为只用于后端,所以不需要实现 DictionaryEnum 接口。
[2]定义模块的 Customizer 类
系统定义了 EnumDictionaryBuilderCustomizer 接口。枚举定义完成之后,需要按照 功能 模块定义该接口的实现类,以实现字典的聚合。
以下为系统枚举聚合的 Customizer 类示例:
public class UpmsEnumDictionaryBuilderCustomizer implements EnumDictionaryBuilderCustomizer {
@Override
public void customize(EnumDictionaryBuilder builder) {
builder.append(Gender.getDictionaries());
builder.append(DataItemStatus.getDictionaries());
builder.append(OrganizationCategory.getDictionaries());
builder.append(Identity.getDictionaries());
builder.append(ApplicationType.getDictionaries());
builder.append(ElementCategory.getDictionaries());
builder.append(MenuScenario.getDictionaries());
}
}Customizer 类定义完成之后,在 @Configuration 类或者 @AutoConfiguration 中,将其定义为 @Bean 即可,示例代码如下所示:
@Bean
public EnumDictionaryBuilderCustomizer upmsEnumDictionaryBuilderCustomizer() {
UpmsEnumDictionaryBuilderCustomizer customizer = new UpmsEnumDictionaryBuilderCustomizer();
log.debug("[Herodotus] |- Strategy [Upms EnumDictionary Builder Customizer] Configure.");
return customizer;
}[三]注意事项
[1] Customizer 类的细粒度
在 Java 多模块工程中,通常都会划分很多的模块,枚举类大概率会分散在不同的模块当中。这时,就需要掌握好 EnumDictionaryBuilderCustomizer 实现类的细粒度。
因为如果定义过多的 EnumDictionaryBuilderCustomizer 实现类,会出现 Customizer 实现类多,但每个类中注册的枚举都很少,徒增维护复杂度。如果 EnumDictionaryBuilderCustomizer 过少,可能就会出现模块间不必要的过度依赖,反而增加了模块与模块间的耦合性。
定义的标准按照:功能模块的划分定义,而不是按照物理代码模块的划分定义
假设,有一个功能 A(例如:Dante Cloud 中的 OSS),这个功能相关的代码在 物理 层面被划分为 a、b、c、d 四个代码模块。假设这四个代码模块,每个模块中都有枚举定义。那么定义 EnumDictionaryBuilderCustomizer 的细粒度可以参考以下建议规范:
- 假设,a、b、c、d 四个模块是存在依赖关系的,即,在某一个模块中,可以访问到四个模块中所有的枚举。那么统一定义一个
EnumDictionaryBuilderCustomizer即可。 - 假设,a、b 模块存在依赖关系,c、d 也存在依赖关系,即其中一个模块可以访问到两个模块中所有的。但是,a、b 与 c、d 之间不存在任何依赖关系,即 a 模块中的枚举,c 和 d 是无法访问到的。这时就可以定义两个
EnumDictionaryBuilderCustomizer实现类。一个用于注册 a 和 b 模块中的枚举,另一个负责 c 和 d 模块中的枚举。
[2] Customizer Bean的名称
Spring 中定义 @Bean 有两个关键的要素:Bean 的类型和 Bean 的名称。
如果 Bean 的类型不同,那么多个 Bean 定义为相同的名称也是可以的。如果 Bean 的类型相同,那么多个 Bean 的名称一定要不同。如果应用中出现相同类型、相同名称的 Bean,启动应用时就会出错。
@Bean
public EnumDictionaryBuilderCustomizer upmsEnumDictionaryBuilderCustomizer() {
UpmsEnumDictionaryBuilderCustomizer customizer = new UpmsEnumDictionaryBuilderCustomizer();
log.debug("[Herodotus] |- Strategy [Upms EnumDictionary Builder Customizer] Configure.");
return customizer;
}如上所示,定义枚举 Customizer Bean 的类型,可以统一使用 EnumDictionaryBuilderCustomizer,这样方便使用 Spring Boot 中的工厂模式实现自动注入。但是在这种情况下,Bean 的名称就必须不同,否则启动应用就会出错。
Spring 中 Bean 的名称可以通过很多方式指定,其中常用的方式有两种:
- 一种方式也是默认方式,就是通过 Bean 定义方法的
方法名来指定。例如:前面的例子代码中upmsEnumDictionaryBuilderCustomizer就是这个 Bean 的名称 - 另一种方式,就是在
@Bean注解上,手动设置 Bean 的名称。
Dante Cloud 代码中,默认均是使用第一种方式来指定 Bean 的名称,这种方式好维护、好调试。
正因为如此,如果你定义了自己的 EnumDictionaryBuilderCustomizer 的实现类,那么一定要注意保证定义 EnumDictionaryBuilderCustomizer Bean 的 方法名 与现有代码中其它的 EnumDictionaryBuilderCustomizer 实现类 Bean 定义的方法名不同才行。
假设,你为自己的两个模块 A 和 B 分别定义了 EnumDictionaryBuilderCustomizer 的实现类的 Bean,那么在定义 Bean 时,一定要区分方法名,如下实例代码所示:
// A 模块 `EnumDictionaryBuilderCustomizer` 的实现类的 Bean
@Bean
public EnumDictionaryBuilderCustomizer aEnumDictionaryBuilderCustomizer() {
return new AEnumDictionaryBuilderCustomizer();
}
// B 模块 `EnumDictionaryBuilderCustomizer` 的实现类的 Bean
@Bean
public EnumDictionaryBuilderCustomizer bEnumDictionaryBuilderCustomizer() {
return new BEnumDictionaryBuilderCustomizer();
}