Java通用枚举还能这样做?前后端终于不扯皮了!

原创 首发于卫星

"又双叒叕改枚举了?!"

前端同学看着后端刚提交的,稍稍增加了一个新状态值的接口文档,默默叹了口气,熟练的打开了自己维护的 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

启动项目将枚举自动注册到数据库,前端调用枚举接口后可获取到最新的枚举信息,无需再手动维护枚举类

结束!

相关推荐
夕颜1114 分钟前
关于 Python 的踩坑记录
后端
米粒宝的爸爸17 分钟前
【uniapp】使用uviewplus来实现图片上传和图片预览功能
java·前端·uni-app
LaoZhangAI17 分钟前
2025年虚拟信用卡订阅ChatGPT Plus完整教程(含WildCard停运后最新方案)
前端·后端
大大大水蜜桃32 分钟前
sql练习二
java·数据库·sql
愿你天黑有灯下雨有伞36 分钟前
企业级异常处理方案:Spring Boot自定义异常全局拦截实战
java·spring boot·后端
喵叔哟1 小时前
28.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--单体转微服务--币种服务(二)
java·微服务·.net
cherishSpring1 小时前
Nacos+LoadBalancer实现服务注册与发现
java·开发语言
别来无恙1491 小时前
SpringBoot的配置文件
java·数据库·spring boot
吗喽对你问好1 小时前
Java中List<int[]>()和List<int[]>[]的区别
java·list
蓝倾2 小时前
淘宝获取商品分类接口操作指南
前端·后端·fastapi