原创 首发于卫星
"又双叒叕改枚举了?!"
前端同学看着后端刚提交的,稍稍增加了一个新状态值的接口文档,默默叹了口气,熟练的打开了自己维护的 constants.ts 文件。后端同学在心里嘀咕"不就加个状态吗?前端改个下拉框不就行啦?至于每次都要我同步吗?"
这熟悉的一幕,几乎每天都会上演。为了减少前后端同学的扯皮,有没有一种方式,能让一份定义,两端通用 ?答案是:有!而且比你想象的更优雅、更强大。 今天就来揭秘我在项目中使用的这种让前后端都拍案叫绝的"通用枚举",彻底让前后端同学不在扯皮?
通用枚举
共提供两种枚举的返回类型,比如有枚举类UserType如下
less
@Getter
@AllArgsConstructor
public enum UserType{
NORMAL(1, "普通用户", ""),
ADMIN(2, "管理员", ""),
;
private final Integer value;
private final String name;
private final String description;
}
第一种返回枚举名称:ADMIN
第二种返回枚举JSON:{"value":2, "name":"管理员", "description":""}
顶级枚举
定义一个通用枚举接口 IBaseEnum,所有的枚举类都必须实现 IBaseEnum,提供一些通用的方法
typescript
/**
* @description: 通用枚举,所有枚举都必须实现该接口
* @author: Walker
* @date: 2025-05-26 01:09:09
* @version: 1.0.0
*/
public interface IBaseEnum<T> {
/**
* 字典值
*
* @return value
*/
T getValue();
/**
* 字典名称
*
* @return name
*/
String getName();
/**
* 字典描述
*
* @return description
*/
String getDescription();
/**
* 通过value获取枚举
*
* @param value value
* @param clazz clazz
* @param <E> E
* @return result
*/
static <E extends Enum<E> & IBaseEnumJson<T>, T> E getEnum(T value, Class<E> clazz) {
Objects.requireNonNull(value);
return getEnums(clazz).stream()
.filter(e -> ObjectUtil.equal(e.getValue(), value))
.findFirst()
.orElse(null);
}
/**
* 通过value获取name
*
* @param value value
* @param clazz clazz
* @param <E> E
* @return name
*/
static <E extends Enum<E> & IBaseEnumJson<T>, T> String getName(T value, Class<E> clazz) {
Objects.requireNonNull(value);
return getEnums(clazz).stream()
.filter(e -> ObjectUtil.equal(e.getValue(), value))
.map(IBaseEnumJson::getName)
.findFirst()
.orElse(null);
}
/**
* 通过name获取value
*
* @param name name
* @param clazz clazz
* @param <E> E
* @return value
*/
static <E extends Enum<E> & IBaseEnumJson<T>, T> T getCode(String name, Class<E> clazz) {
Objects.requireNonNull(name);
return getEnums(clazz).stream()
.filter(e -> ObjectUtil.equal(e.getName(), name))
.map(IBaseEnumJson::getValue)
.findFirst()
.orElse(null);
}
/**
* 获取所有枚举
*
* @param clazz clazz
* @param <E> E
* @return enums
*/
static <E extends Enum<E> & IBaseEnumJson<T>, T> EnumSet<E> getEnums(Class<E> clazz) {
return EnumSet.allOf(clazz);
}
}
简单枚举
为枚举中的需要序列化的字段添加 *@JsonValue
* 即可实现简单枚举。我们通常会在枚举描述的字段上添加 *@JsonValue
* 注解,但是前端传参就只能传递这个枚举描述字段的值(通常是中文),就不能传枚举名称了。为此需要使用 *@JsonCreator*
注解来自定义处理的逻辑。
typescript
/**
* 为什么不直接使用@JsonCreator:
* 1、在接口中使用该注解添加到from方法上是不生效的
* 2、如果将接口换成抽象类是可以的,但是枚举无法继承抽象类
* 3、单独在每个实现IBaseEnumSimple接口的枚举中添加的话,代价太大了
* // @JsonCreator
* // public static UserType from(Object object) {
* // return IBaseEnumSimple.from(object, UserType.class);
* // }
*
* @description: 通用枚举,所有枚举都必须实现该接口
* @author: Walker
* @date: 2025-05-25 20:37:37
* @version: 1.0.0
*/
@JsonDeserialize(using = DictSimpleDeserializer.class)
public interface IBaseEnumSimple<T> extends IBaseEnum<T> {
// 可以不在此处做限制,开放给具体的继承类使用
@JsonValue
String getName();
/**
* 直接在此方法添加@JsonCreator注解是行不通的,除非在实现IBaseEnumSimple的枚举类中单独使用
*
* @param object object
* @param enumClass enumClass
* @param <E> E
* @return Enum
*/
static <E extends Enum<E> & IBaseEnumSimple<?>> E from(Object object, Class<E> enumClass) {
if (object == null || enumClass == null) {
return null;
}
return Arrays.stream(enumClass.getEnumConstants())
.filter(e -> e.matches(object)).findFirst()
.orElseThrow(() -> new IllegalArgumentException("无效枚举值: " + object));
}
/**
* 枚举匹配逻辑,子类可覆盖
*
* @param object object
* @return boolean
*/
default boolean matches(Object object) {
// 按照value,name或枚举名称匹配
// return String.valueOf(this.getValue()).equals(object.toString())
// || this.getName().equals(object.toString())
// || this.toString().equals(object.toString());
// 通过枚举名称匹配
return this.toString().equals(object.toString());
}
}
scala
/**
* @description: 简单枚举反序列化器
* @author: Walker
* @date: 2025-05-26 01:40:40
* @version: 1.0.0
*/
public class DictSimpleDeserializer<E extends Enum<E> & IBaseEnumSimple<?>> extends JsonDeserializer<E> implements ContextualDeserializer {
private Class<E> enumClass;
@Override
@SuppressWarnings("unchecked")
public JsonDeserializer<?> createContextual(DeserializationContext context, BeanProperty property) {
this.enumClass = (Class<E>) context.getContextualType().getRawClass();
return this;
}
@Override
public E deserialize(JsonParser p, DeserializationContext context) throws IOException {
String value = p.getValueAsString();
return IBaseEnumSimple.from(value, enumClass);
}
}
JSON枚举
枚举接口添加 @JsonFormat(shape = JsonFormat.Shape.*OBJECT*)
即可返回枚举对应的 JSON 字符串
java
/**
* @description: 通用枚举,所有枚举都必须实现该接口
* @author: Walker
* @date: 2025-01-11 00:55:55
* @version: 1.0.0
*/
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public interface IBaseEnumJson<T> extends IBaseEnum<T> {
}
通用枚举示例
简单枚举使用示例,前端可直接传递枚举名称如:NORMAL、ADMIN,也可以通过修改 IBaseEnumSimple
中的 matches 方法修改匹配逻辑
java
@Getter
@AllArgsConstructor
public enum UserType implements IBaseEnumSimple<Integer> {
NORMAL(1, "普通用户", ""),
ADMIN(2, "管理员", ""),
;
@EnumValue
private final Integer value;
private final String name;
private final String description;
}
JSON枚举使用示例,前端可以直接传递枚举名称如:NORMAL、ADMIN
java
@Getter
@AllArgsConstructor
public enum UserType implements IBaseEnumJson<Integer> {
NORMAL(1, "普通用户", ""),
ADMIN(2, "管理员", ""),
;
@EnumValue
private final Integer value;
private final String name;
private final String description;
}
枚举自动注册
数据字典实体、数据字典数据实体
scala
public class SysCodeMaster extends BaseEntity {
private String id;
private String name;
private String code;
private String description;
private Integer status;
}
scala
public class SysCodeItem extends BaseEntity {
private String id;
private String codeId;
private String name;
private String value;
private String description;
private Double displayOrder;
private Integer status;
}
数据字典注解、数据字典数据注解
less
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CodeMaster {
// 数据字典名
String name();
// 字典编码
String code();
// 数据字典项
CodeItem[] values() default {};
// 数据字典项对应的枚举类
Class<? extends IBaseEnum<?>> enumClass() default UnspecifiedEnum.class;
// 枚举表述
String description() default "";
}
less
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CodeItem {
// 名称
String name();
// 值
String value();
// 描述
String description() default "";
}
数据字典自动注册Runner
less
@ConditionalOnProperty(name = "top.walker.dict.register.enabled", havingValue = "true", matchIfMissing = false)
@Component
@RequiredArgsConstructor
public class CodeMasterRunner implements ApplicationRunner {
private final Logger logger = LoggerFactory.getLogger(CodeMasterRunner.class);
private final ISysCodeMasterService codeMasterService;
@Override
public void run(ApplicationArguments args) throws Exception {
logger.info("code master register start......");
// 创建扫描器
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
// 添加过滤器,查找实现了IEntity接口的类
scanner.addIncludeFilter(new AssignableTypeFilter(IEntity.class));
// 扫描BASE_PACKAGES中所有实现IEntity的类
Set<BeanDefinition> beanDefinitions = scanner.findCandidateComponents(ApplicationConstants.BASE_PACKAGES);
for (BeanDefinition beanDefinition : beanDefinitions) {
Class<?> clazz = Class.forName(beanDefinition.getBeanClassName());
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(CodeMaster.class)) {
CodeMaster codeMaster = field.getAnnotation(CodeMaster.class);
SysCodeMaster sysCodeMaster = SysCodeMaster.builder()
.name(codeMaster.name())
.code(codeMaster.code())
.description(codeMaster.description())
.build();
logger.info("dic : {}", JSONUtil.toJsonStr(sysCodeMaster));
Map<String, SysCodeItem> codeItemMap = new HashMap<>();
// 解析enumClass
Class<? extends IBaseEnum<?>> enumClass = codeMaster.enumClass();
if (!UnspecifiedEnum.class.equals(enumClass) && enumClass.isEnum()) {
IBaseEnum<?>[] enumConstants = enumClass.getEnumConstants();
for (IBaseEnum<?> enumConstant : enumConstants) {
SysCodeItem sysCodeItem = SysCodeItem.builder()
.codeId(sysCodeMaster.getId())
.name(enumConstant.getName())
.value(String.valueOf(enumConstant.getValue()))
.description(enumConstant.getDescription())
.build();
codeItemMap.put(String.valueOf(enumConstant.getValue()), sysCodeItem);
}
}
// values 和 enumClass 同时存在时,values 优先级更高,所以 values 放在后面
// 解析values
for (CodeItem codeItem : codeMaster.values()) {
SysCodeItem sysCodeItem = SysCodeItem.builder()
.name(codeItem.name()).value(codeItem.value()).description(codeItem.description())
.build();
codeItemMap.put(codeItem.value(), sysCodeItem);
}
List<SysCodeItem> codeItems = Arrays.asList(codeItemMap.values().toArray(new SysCodeItem[0]));
logger.info(" {}", JSONUtil.toJsonStr(codeItems));
// 注册数据字典
this.registerCode(sysCodeMaster, codeItems);
}
}
}
logger.info("code master register end......");
}
/**
* 注册数据字典和数据字典数据
*
* @param sysCodeMaster 数据字典
* @param codeItems 数据字典数据
*/
private void registerCode(SysCodeMaster sysCodeMaster, List<SysCodeItem> codeItems) {
// TODO
codeMasterService.saveOrUpdateCode(sysCodeMaster, codeItems);
}
}
枚举自动注册示例
枚举注解的两种使用方式
less
public class User extends BaseEntity {
private String name;
private Integer age;
@CodeMaster(name = "性别", code = "sys_sex", values = {
@CodeItem(name = "男", value = "1"),
@CodeItem(name = "女", value = "2"),
@CodeItem(name = "未知", value = "3"),
})
private UserSex sex;
@CodeMaster(name = "类型", code = "sys_type_test", enumClass = UserType.class)
private UserType type;
}
在application配置中开启枚举自动注册
yaml
top:
walker:
dict:
register:
enabled: true
启动项目将枚举自动注册到数据库,前端调用枚举接口后可获取到最新的枚举信息,无需再手动维护枚举类
结束!