• 基于NACOS和JAVA反射机制动态更新JAVA静态常量非@Value注解


    1.前言

    项目中都会使用常量类文件, 这些值如果需要变动需要重新提交代码,或者基于@Value注解实现动态刷新, 如果常量太多也是很麻烦; 那么 能不能有更加简便的实现方式呢?

    本文讲述的方式是, 一个JAVA类对应NACOS中的一个配置文件,优先使用nacos中的配置,不配置则使用程序中的默认值;

    2.正文

    nacos的配置如下图所示,为了满足大多数情况,配置了 namespace命名空间和group;

     新建个测试工程 cloud-sm.

    bootstrap.yml 中添加nacos相关配置;

    为了支持多配置文件需要注意ext-config节点,group对应nacos的添加的配置文件的group; data-id 对应nacos上配置的data-id

    配置如下:

    server:
      port: 9010
      servlet:
        context-path: /sm
    spring:
      application:
        name: cloud-sm
      cloud:
        nacos:
          discovery:
            server-addr: 192.168.100.101:8848 #Nacos服务注册中心地址
            namespace: 1
          config:
            server-addr: 192.168.100.101:8848 #Nacos作为配置中心地址
            namespace: 1
            ext-config:
              - group: TEST_GROUP
                data-id: cloud-sm.yaml
                refresh: true
              - group: TEST_GROUP
                data-id: cloud-sm-constant.properties
                refresh: true

    接下来是本文重点:

    1)新建注解ConfigModule,用于在配置类上;一个value属性;

    2)新建个监听类,用于获取最新配置,并更新常量值

    实现流程:

    1)项目初始化时获取所有nacos的配置

    2)遍历这些配置文件,从nacos上获取配置

    3)遍历nacos配置文件,获取MODULE_NAME的值

    4)寻找配置文件对应的常量类,从spring容器中寻找 常量类 有注解ConfigModule 且值是 MODULE_NAME对应的

    5)使用JAVA反射更改常量类的值

    6)增加监听,用于动态刷新

    import org.springframework.stereotype.Component;
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    @Component
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE})
    public @interface ConfigModule {
        /**
         *  对应配置文件里面key为( MODULE_NAME ) 的值
         * @return
         */
        String value();
    }
    import com.alibaba.cloud.nacos.NacosConfigProperties;
    import com.alibaba.druid.support.json.JSONUtils;
    import com.alibaba.nacos.api.NacosFactory;
    import com.alibaba.nacos.api.PropertyKeyConst;
    import com.alibaba.nacos.api.config.ConfigService;
    import com.alibaba.nacos.api.config.listener.Listener;
    import com.alibaba.nacos.api.exception.NacosException;
    import com.alibaba.nacos.client.utils.LogUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.slf4j.Logger;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.ApplicationContext;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.PostConstruct;
    import java.io.IOException;
    import java.io.StringReader;
    import java.lang.reflect.Field;
    import java.util.Enumeration;
    import java.util.List;
    import java.util.Map;
    import java.util.Properties;
    import java.util.concurrent.Executor;
    
    /**
     * nacos 自定义监听
     *
     * @author zch
     */
    @Component
    public class NacosConfigListener {
        private Logger LOGGER = LogUtils.logger(NacosConfigListener.class);
        @Autowired
        private NacosConfigProperties configs;
        @Value("${spring.cloud.nacos.config.server-addr:}")
        private String serverAddr;
        @Value("${spring.cloud.nacos.config.namespace:}")
        private String namespace;
        @Autowired
        private ApplicationContext applicationContext;
        /**
         * 目前只考虑properties 文件
         */
        private String fileType = "properties";
        /**
         * 需要在配置文件中增加一条 MODULE_NAME 的配置,用于找到对应的 常量类
         */
        private String MODULE_NAME = "MODULE_NAME";
    
        /**
         * NACOS监听方法
         *
         * @throws NacosException
         */
        public void listener() throws NacosException {
            if (StringUtils.isBlank(serverAddr)) {
                LOGGER.info("未找到 spring.cloud.nacos.config.server-addr");
                return;
            }
            Properties properties = new Properties();
            properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr.split(":")[0]);
            if (StringUtils.isNotBlank(namespace)) {
                properties.put(PropertyKeyConst.NAMESPACE, namespace);
            }
    
            ConfigService configService = NacosFactory.createConfigService(properties);
            // 处理每个配置文件
            for (NacosConfigProperties.Config config : configs.getExtConfig()) {
                String dataId = config.getDataId();
                String group = config.getGroup();
                //目前只考虑properties 文件
                if (!dataId.endsWith(fileType)) continue;
    
                changeValue(configService.getConfig(dataId, group, 5000));
    
                configService.addListener(dataId, group, new Listener() {
                    @Override
                    public void receiveConfigInfo(String configInfo) {
                        changeValue(configInfo);
                    }
    
                    @Override
                    public Executor getExecutor() {
                        return null;
                    }
                });
            }
        }
    
        /**
         * 改变 常量类的 值
         *
         * @param configInfo
         */
        private void changeValue(String configInfo) {
            if(StringUtils.isBlank(configInfo)) return;
            Properties proper = new Properties();
            try {
                proper.load(new StringReader(configInfo)); //把字符串转为reader
            } catch (IOException e) {
                e.printStackTrace();
            }
            String moduleName = "";
            Enumeration enumeration = proper.propertyNames();
            //寻找MODULE_NAME的值
            while (enumeration.hasMoreElements()) {
                String strKey = (String) enumeration.nextElement();
                if (MODULE_NAME.equals(strKey)) {
                    moduleName = proper.getProperty(strKey);
                    break;
                }
            }
            if (StringUtils.isBlank(moduleName)) return;
            Class curClazz = null;
            // 寻找配置文件对应的常量类
            // 从spring容器中 寻找类的注解有ConfigModule 且值是 MODULE_NAME对应的
            for (String beanName : applicationContext.getBeanDefinitionNames()) {
                Class clazz = applicationContext.getBean(beanName).getClass();
                ConfigModule configModule = (ConfigModule) clazz.getAnnotation(ConfigModule.class);
                if (configModule != null && moduleName.equals(configModule.value())) {
                    curClazz = clazz;
                    break;
                }
            }
            if (curClazz == null) return;
            // 使用JAVA反射机制 更改常量
            enumeration = proper.propertyNames();
            while (enumeration.hasMoreElements()) {
                String key = (String) enumeration.nextElement();
                String value = proper.getProperty(key);
                if (MODULE_NAME.equals(key)) continue;
                try {
                    Field field = curClazz.getDeclaredField(key);
                    //忽略属性的访问权限
                    field.setAccessible(true);
                    Class<?> curFieldType = field.getType();
                    //其他类型自行拓展
                    if (curFieldType.equals(String.class)) {
                        field.set(null, value);
                    } else if (curFieldType.equals(List.class)) { // 集合List元素
                        field.set(null, JSONUtils.parse(value));
                    } else if (curFieldType.equals(Map.class)) { //Map
                        field.set(null, JSONUtils.parse(value));
                    }
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    LOGGER.info("设置属性失败:{} {} = {} ", curClazz.toString(), key, value);
                }
            }
        }
    
        @PostConstruct
        public void init() throws NacosException {
            listener();
        }
    }

     3.测试

    1)新建常量类Constant,增加注解@ConfigModule("sm"),尽量测试全面, 添加常量类型有 String, List,Map

    @ConfigModule("sm")
    public class Constant {
    
        public static volatile String TEST = new String("test");
    
        public static volatile List<String> TEST_LIST = new ArrayList<>();
        static {
            TEST_LIST.add("默认值");
        }
        public static volatile Map<String,Object> TEST_MAP = new HashMap<>();
        static {
            TEST_MAP.put("KEY","初始化默认值");
        }
        public static volatile List<Integer> TEST_LIST_INT = new ArrayList<>();
        static {
            TEST_LIST_INT.add(1);
        }
    }

    2)新建个Controller用于测试这些值

    @RestController
    public class TestController {
    
        @GetMapping("/t1")
        public Map<String, Object> test1() {
            Map<String, Object> result = new HashMap<>();
    
            result.put("string" , Constant.TEST);
            result.put("list" , Constant.TEST_LIST);
            result.put("map" , Constant.TEST_MAP);
            result.put("list_int" , Constant.TEST_LIST_INT);
            result.put("code" , 1);
            return result;
        }
    }

    3)当前nacos的配置文件cloud-sm-constant.properties为空

     4)访问测试路径localhost:9010/sm/t1,返回为默认值

    {
        "code": 1,
        "string": "test",
        "list_int": [
            1
        ],
        "list": [
            "默认值"
        ],
        "map": {
            "KEY": "初始化默认值"
        }
    }

    5)然后更改nacos的配置文件cloud-sm-constant.properties;

     6)再次访问测试路径localhost:9010/sm/t1,返回为nacos中的值

    {
        "code": 1,
        "string": "12351",
        "list_int": [
            1,
            23,
            4
        ],
        "list": [
            "123",
            "sss"
        ],
        "map": {
            "A": 12,
            "B": 432
        }
    }

    4.结语

    这种实现方式优点如下:

    1)动态刷新配置,不需要重启即可改变程序中的静态常量值

    2)使用简单,只需在常量类上添加一个注解

    3)避免在程序中大量使用@Value,@RefreshScope注解

     不足:

    此代码是个人业余时间的想法,未经过生产验证,实现的数据类型暂时只写几个,其余的需要自行拓展

  • 相关阅读:
    python jinja2模板分页效果
    vscode配置svn
    解决VSCODE打开新文件覆盖上一个窗口的问题
    vue所用插件整理
    vue bus总线
    create-react-app使用less
    react入门
    ES6|Object
    vue轮播插件--vue-awesome-swiper
    js/jq中遍历对象或者数组的函数(foreach,map,each)
  • 原文地址:https://www.cnblogs.com/changhai/p/13153689.html
Copyright © 2020-2023  润新知