使用注解实现在springboot启动时,动态检测枚举与应用程序中关联的字典数据之间的数据一致性

在许多Spring Boot应用中,枚举类型在表示固定值集时发挥着至关重要的作用。然而,随着微服务业务的不断积累,确保枚举与应用中存储的数据保持一致性变得至关重要,特别是在处理字典数据时,我们需要同时维护字典表和枚举值。所以想到了使用注解的方式来动态扫描枚举值,并和字典表中的数据进行比对,来同时维护字典表和枚举值。

引言

在本博客中,我们将探讨一种通过在Spring Boot应用程序中使用注解扫描和验证枚举。我们的目标是检测枚举与应用程序中关联的字典数据之间的任何不一致之处。

枚举注解

让我们从介绍 EnumClassAnnotation 注解开始,这个注解用于标记枚举类

package com.ruoyi.common.core.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @ClassName: EnumAnnotation
 * @Description:
 * @Author: yujky
 * @Date: 2023/12/25 11:05
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnumClassAnnotation {
    /**
     * 字典,对应sys_dic_data中的数据 注意:该参数必填,否则不会校验枚举值是否合法
     *
     * @return String
     */
    String dicData() default "";
}

该注解用于标记需要进行枚举验证的类,并且需要指定一个对应于sys_dic_data表中数据的字典名称。

枚举公共属性接口

为了简化枚举类的实现和提高代码的可读性,我们定义了一个 CommonEnumService 接口,用于规范枚举的公共属性:

package com.ruoyi.common.core.service;

/**
 * @ClassName: CommonEnumService
 * @Description: 枚举公共属性
 * @Author: yujky
 * @Date: 2023/12/25 11:32
 */
public interface CommonEnumService {
    String getLabel();

    String getValue();
}

枚举扫描器

为了扫描并验证枚举,我们使用了一个枚举扫描器 EnumClassScanner,该扫描器负责扫描包路径下带有 EnumClassAnnotation 注解的枚举类。

package com.ruoyi.common.core.config;

import com.ruoyi.common.core.annotation.EnumClassAnnotation;
import com.ruoyi.common.core.service.CommonEnumService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 扫描并检测枚举类的值是否符合规范
 * 枚举类要被扫描到需要满足如下两个条件
 * 1. 枚举类需标注 @EnumClassAnnotation 注解
 * 2. 实现 CommonEnumService 公共接口
 */
public class EnumClassScanner {
    private static final Logger logger = LoggerFactory.getLogger(EnumClassScanner.class);

    public static Map scanEnumsWithAnnotation(String basePackage) {
        try {
            // 创建一个类路径扫描器,用于扫描指定包路径下的类。
            ClassPathScanningCandidateComponentProvider scanner =
                new ClassPathScanningCandidateComponentProvider(false);
            // 添加一个过滤器,只保留带有 EnumClassAnnotation 注解的类。
            scanner.addIncludeFilter(new AnnotationTypeFilter(EnumClassAnnotation.class));

            // 使用扫描器扫描指定包路径下的类,并将符合条件的类的元数据(BeanDefinition)作为流(Stream)进行处理。
            Set enumClasses = scanner.findCandidateComponents(basePackage).stream()
                .map(beanDefinition -> {
                    try {
                        // 通过类的全限定名加载类对象。
                        Class clazz = Class.forName(beanDefinition.getBeanClassName());
                        // 检查该类是否是 CommonEnumService 接口的子类且是枚举类,是的话就返回该类。
                        if (CommonEnumService.class.isAssignableFrom(clazz) && clazz.isEnum()) {
                            return (Class) clazz;
                        }
                        return null;
                    } catch (ClassNotFoundException e) {
                        logger.error("类加载异常: {}", e.getMessage(), e);
                        // 可以考虑抛出运行时异常或者记录日志,具体根据业务需求和实际情况处理
                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());

            if (!enumClasses.isEmpty()) {
                Map enumServiceMap = new HashMap(enumClasses.size());
                for (Class enumClass : enumClasses) {
                    logger.info("============= Enum class with annotation: {} =============", enumClass.getSimpleName());

                    EnumClassAnnotation enumClassAnnotation = enumClass.getAnnotation(EnumClassAnnotation.class);
                    if (enumClassAnnotation != null) {
                        // 获取注解的属性值
                        String dicData = enumClassAnnotation.dicData();

                        if (StringUtils.isNotBlank(dicData)) {
                            // 获取枚举值数组。
                            CommonEnumService[] enumValues = enumClass.getEnumConstants();
                            enumServiceMap.put(dicData, enumValues);
                            for (CommonEnumService enumValue : enumValues) {
                                logger.info("Label: {}  value: {}", enumValue.getLabel(), enumValue.getValue());
                            }
                        }
                    }
                }
                return enumServiceMap;
            } else {
                logger.info("未扫描到带有 EnumClassAnnotation 注解的枚举类。");
                return Collections.emptyMap();
            }
        } catch (Exception e) {
            logger.error("扫描异常: {}", e.getMessage(), e);
            // 可以考虑抛出运行时异常或者记录日志,具体根据业务需求和实际情况处理
            return null;
        } finally {
            logger.info("============== 扫描完成 ===============");
        }
    }
}

应用启动时的检测

为了在应用启动时执行枚举一致性检测,我们实现了一个 ApplicationRunner,在应用启动时运行。这个检测会扫描枚举并确保它们与字典数据保持一致。

package com.ruoyi.netty.common.runner;

import cn.hutool.core.collection.CollUtil;
import com.ruoyi.common.core.config.EnumClassScanner;
import com.ruoyi.common.core.constant.CacheNames;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.service.CommonEnumService;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.redis.utils.CacheUtils;
import com.ruoyi.system.api.domain.SysDictData;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.stream.Collectors;

/**
 * @ClassName: NettyApplicationRunner
 * @Description: 初始化 netty 模块对应业务数据
 * @Author: yujky
 * @Date: 2023/12/25 22:01
 */
@Slf4j
@RequiredArgsConstructor
@Component
public class NettyApplicationRunner implements ApplicationRunner {

    public static void detectEnumValueChange(Map enumsMap) {
        if (CollUtil.isEmpty(enumsMap)) {
            return;
        }
        List errMsgMap = new ArrayList();
        enumsMap.forEach((dicData, enumServices) -> {
            List sysDictData = CacheUtils.get(CacheNames.SYS_DICT, dicData);

            if (CollUtil.isNotEmpty(sysDictData)) {
                Map dicValueLabelMap = sysDictData.stream().collect(Collectors.toMap(SysDictData::getDictValue, SysDictData::getDictLabel));

                Map valueLabelMap = Arrays.stream(enumServices)
                    .collect(Collectors.toList())
                    .stream()
                    .collect(Collectors.toMap(CommonEnumService::getValue, CommonEnumService::getLabel));
                Iterator iterator = dicValueLabelMap.entrySet().iterator();
                while (iterator.hasNext()) {
                    Map.Entry entry = iterator.next();
                    String k = entry.getKey();
                    String v = entry.getValue();

                    if (valueLabelMap.containsKey(k)) {
                        String enumLabel = valueLabelMap.get(k);
                        if (!enumLabel.equals(v)) {
                            String errMsg = StringUtils.format("枚举类标签匹配错误,sysDictData: {}, 错误标签 {}, 正确标签应为: {}", sysDictData, enumLabel, v);
                            errMsgMap.add(errMsg);
                        }
                        valueLabelMap.remove(k);
                        iterator.remove(); // 使用迭代器安全地移除元素
                    }
                }

                if (CollUtil.isNotEmpty(dicValueLabelMap)) {
                    errMsgMap.add(StringUtils.format("枚举类存在遗漏的枚举值:{}", dicValueLabelMap));
                }

                if (CollUtil.isNotEmpty(valueLabelMap)) {
                    errMsgMap.add(StringUtils.format("枚举类存在多余的枚举值:{}", valueLabelMap));
                }
            }
        });

        if (CollUtil.isNotEmpty(errMsgMap)) {
            throw new ServiceException(errMsgMap.toString());
        }
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("============= 枚举类型合法性校验中... =============");
        Map enumsWithAnnotation = EnumClassScanner.scanEnumsWithAnnotation("com.ruoyi.netty");
        detectEnumValueChange(enumsWithAnnotation);
        log.info("============= 枚举类型校验合法 =============");
    }
}

使用示例

package com.ruoyi.netty.api.enums;

import com.ruoyi.common.core.annotation.EnumClassAnnotation;
import com.ruoyi.common.core.service.CommonEnumService;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @ClassName: NoticeTempBusinessTypeEnum
 * @Description: 消息模版-业务类型枚举
 * @Author: yujky
 * @Date: 2023/12/10 11:49
 */
@Getter
@AllArgsConstructor
@EnumClassAnnotation(dicData = "sys_notice_temp_business_type")
public enum NoticeTempBusinessTypeEnum implements CommonEnumService {

    /**
     * system_update_notice-系统更新提醒
     */
    SYSTEM_UPDATE_NOTICE("system_update_notice", "系统更新提醒"),
    /**
     * remote_login_alarm-用户登录异常提醒
     */
    REMOTE_LOGIN_ALARM("remote_login_alarm", "用户登录异常提醒"),
    /**
     * register_notice-用户注册提醒
     */
    REGISTER_NOTICE("register_notice", "用户注册提醒");

    private final String code;
    private final String desc;

    public static String code2Desc(String code) {
        for (NoticeTempBusinessTypeEnum typeEnum : NoticeTempBusinessTypeEnum.values()) {
            if (typeEnum.code.equals(code)) {
                return typeEnum.desc;
            }
        }
        return null;
    }

    @Override
    public String getLabel() {
        return desc;
    }

    @Override
    public String getValue() {
        return code;
    }
}

结论

通过使用注解、枚举扫描器和应用启动检测,我们可以有效地确保枚举与应用程序中的字典数据一致。这种方法有助于在应用开发过程中减少枚举错误,提高代码质量。

希望这篇博客能够帮助您更好地管理和维护Spring Boot应用中的枚举类型。如果您有任何问题或建议,请随时在评论中分享。

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值