Nacos热更新静态变量配置

Nacos热更新静态变量配置

Springboot项目接入nacos,配置文件统一管理,但静态常量无法通过@Value注解实时热更新(如下所示)。

GlobalVariables.java

@Component
public class GlobalVariables {
  
    //测试热加载配置字段
    public static String testInfo;
  
    @Value("${testInfo}")
    public void setTestInfo(String value) {
        testInfo = value;
    }
}

解决思路:

  1. 项目初始化时获取所有nacos的配置
  2. 遍历这些配置文件,从nacos上获取配置
  3. 寻找配置文件对应的常量类,从spring容器中寻找 常量类 有注解NacosConfig
  4. 使用JAVA反射更改常量类的值
  5. 增加监听,用于动态刷新

1、bootstrap.yml 配置

spring:
  application:
    name: test
  cloud:
    nacos:
      config:
        # nacos的ip地址和端口
        server-addr: 127.0.0.1:8848
        # nacos登录用户名
        username: nacos
        # nacos登录密码
        password: nacos
        # nacos命名空间id为 dev
        namespace: 07e01034-cba5-45b2-88cf-e14d3bf1fa60
        # 创建的配置的group
        group: DEFAULT_GROUP
        # 配置文件的后缀名
        file-extension: yaml
        prefix: ${spring.application.name}

2、nacos 配置

image-20240516111024406

配置增加中加入参数 testInfo

image-20240516111100712

3、增加注解

NacosConfig.java

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 NacosConfig {

}

GlobalVariables.java

@NacosConfig
@Component
public class GlobalVariables {
  
    //测试热加载配置字段
    public static String testInfo;
  
    @Value("${testInfo}")
    public void setTestInfo(String value) {
        testInfo = value;
    }
}

4、增加监听事件

NacosConfigListener.java

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 wwj
 */
@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;
    @Value("${spring.cloud.nacos.config.username:}")
    private String username;
    @Value("${spring.cloud.nacos.config.password:}")
    private String password;
    @Autowired
    private ApplicationContext applicationContext;
    /**
     * 目前只考虑yaml 文件
     */
    private String fileType = "yaml";
    /**
     * 需要在配置文件中增加一条 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);
        }
        properties.put(PropertyKeyConst.USERNAME, username);
        properties.put(PropertyKeyConst.PASSWORD, password);

        ConfigService configService = NacosFactory.createConfigService(properties);
        List<NacosConfigProperties.Config> sharedConfigs = configs.getSharedConfigs();
        // 处理每个配置文件
        for (NacosConfigProperties.Config config : sharedConfigs) {
            String dataId = config.getDataId();
            String group = config.getGroup();
            //目前只考虑yaml 文件
            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();
        //yaml转Properties
        String s = PropertiesUtil.castToProperties(configInfo);
        try {
            proper.load(new StringReader(s)); //把字符串转为reader
        } catch (IOException e) {
            e.printStackTrace();
        }
        Enumeration enumeration = proper.propertyNames();

        // 寻找配置文件对应的常量类
        //从spring容器中 寻找类的注解有NacosConfig
        for (String beanName : applicationContext.getBeanDefinitionNames()) {
            Class curClazz = applicationContext.getBean(beanName).getClass();
            NacosConfig configModule = (NacosConfig) curClazz.getAnnotation(NacosConfig.class);
            if (configModule != null) {
                // 使用JAVA反射机制 更改常量
                while (enumeration.hasMoreElements()) {
                    String key = (String) enumeration.nextElement();
                    String value = proper.getProperty(key);
                    try {
                        Field field = curClazz.getDeclaredField(key);
                        System.out.println(field);
                        //忽略属性的访问权限
                        field.setAccessible(true);
                        Class<?> curFieldType = field.getType();
                        //其他类型自行拓展
                        if (curFieldType.equals(String.class)) {
                            field.set(null, value);
                        } else if (curFieldType.equals(Integer.class)) { // Integer元素
                            field.set(null, value);
                        } else if (curFieldType.equals(Boolean.class)) { // Boolean元素
                            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();
    }
}

PropertiesUtil.java (Properties工具类)

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.util.CollectionUtils;
import org.yaml.snakeyaml.Yaml;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Deng.Weiping
 * @since 2023/11/28 13:57
 */
public class PropertiesUtil {

    /**
     * yaml 转 Properties
     *
     * @param input
     * @return
     */
    public static String castToProperties(String input) {
        Map<String, Object> propertiesMap = new LinkedHashMap<>();
        Map<String, Object> yamlMap = new Yaml().load(input);
        flattenMap("", yamlMap, propertiesMap);
        StringBuffer strBuff = new StringBuffer();
        propertiesMap.forEach((key, value) -> strBuff.append(key)
                .append("=")
                .append(value)
                .append(StrUtil.LF));
        return strBuff.toString();
    }

    /**
     * Properties 转 Yaml
     *
     * @param input
     * @return
     */
    public static String castToYaml(String input) {
        try {
            Map<String, Object> properties = readProperties(input);
            return properties2Yaml(properties);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static Map<String, Object> readProperties(String input) {
        // 使用 LinkedHashMap 保证顺序
        Map<String, Object> propertiesMap = new LinkedHashMap<>();
        for (String line : input.split(StrUtil.LF)) {
            if (StrUtil.isNotBlank(line)) {
                // 使用正则表达式解析每一行中的键值对
                Pattern pattern = Pattern.compile("\\s*([^=\\s]*)\\s*=\\s*(.*)\\s*");
                Matcher matcher = pattern.matcher(line);
                if (matcher.matches()) {
                    String key = matcher.group(1);
                    String value = matcher.group(2);
                    propertiesMap.put(key, value);
                }
            }
        }
        return propertiesMap;
    }

    /**
     * 递归 Map 集合,转为 Properties集合
     *
     * @param prefix
     * @param yamlMap
     * @param treeMap
     */
    private static void flattenMap(String prefix, Map<String, Object> yamlMap, Map<String, Object> treeMap) {
        yamlMap.forEach((key, value) -> {
            String fullKey = prefix + key;
            if (value instanceof LinkedHashMap) {
                flattenMap(fullKey + ".", (LinkedHashMap) value, treeMap);
            } else if (value instanceof ArrayList) {
                List values = (ArrayList) value;
                for (int i = 0; i < values.size(); i++) {
                    String itemKey = String.format("%s[%d]", fullKey, i);
                    Object itemValue = values.get(i);
                    if (itemValue instanceof String) {
                        treeMap.put(itemKey, itemValue);
                    } else {
                        flattenMap(itemKey + ".", (LinkedHashMap) itemValue, treeMap);
                    }
                }
            } else {
                treeMap.put(fullKey, null != value ? value.toString() : null);
            }
        });
    }

    /**
     * properties 格式转化为 yaml 格式字符串
     *
     * @param properties
     * @return
     */
    private static String properties2Yaml(Map<String, Object> properties) {
        if (CollUtil.isEmpty(properties)) {
            return null;
        }
        Map<String, Object> map = parseToMap(properties);
        StringBuffer stringBuffer = map2Yaml(map);
        return stringBuffer.toString();
    }

    /**
     * 递归解析为 LinkedHashMap
     *
     * @param propMap
     * @return
     */
    private static Map<String, Object> parseToMap(Map<String, Object> propMap) {
        Map<String, Object> resultMap = new LinkedHashMap<>();
        try {
            if (CollectionUtils.isEmpty(propMap)) {
                return resultMap;
            }
            propMap.forEach((key, value) -> {
                if (key.contains(".")) {
                    String currentKey = key.substring(0, key.indexOf("."));
                    if (resultMap.get(currentKey) != null) {
                        return;
                    }
                    Map<String, Object> childMap = getChildMap(propMap, currentKey);
                    Map<String, Object> map = parseToMap(childMap);
                    resultMap.put(currentKey, map);
                } else {
                    resultMap.put(key, value);
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
        return resultMap;
    }


    /**
     * 获取拥有相同父级节点的子节点
     *
     * @param propMap
     * @param currentKey
     * @return
     */
    private static Map<String, Object> getChildMap(Map<String, Object> propMap, String currentKey) {
        Map<String, Object> childMap = new LinkedHashMap<>();
        try {
            propMap.forEach((key, value) -> {
                if (key.contains(currentKey + ".")) {
                    key = key.substring(key.indexOf(".") + 1);
                    childMap.put(key, value);
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
        return childMap;
    }

    /**
     * map集合转化为yaml格式字符串
     *
     * @param map
     * @return
     */
    public static StringBuffer map2Yaml(Map<String, Object> map) {
        //默认deep 为零,表示不空格,deep 每加一层,缩进两个空格
        return map2Yaml(map, 0);
    }

    /**
     * 把Map集合转化为yaml格式 String字符串
     *
     * @param propMap map格式配置文件
     * @param deep    树的层级,默认deep 为零,表示不空格,deep 每加一层,缩进两个空格
     * @return
     */
    private static StringBuffer map2Yaml(Map<String, Object> propMap, int deep) {
        StringBuffer yamlBuffer = new StringBuffer();
        try {
            if (CollectionUtils.isEmpty(propMap)) {
                return yamlBuffer;
            }
            String space = getSpace(deep);
            for (Map.Entry<String, Object> entry : propMap.entrySet()) {
                Object valObj = entry.getValue();
                if (entry.getKey().contains("[") && entry.getKey().contains("]")) {
                    String key = entry.getKey().substring(0, entry.getKey().indexOf("[")) + ":";
                    yamlBuffer.append(space + key + "\n");
                    propMap.forEach((itemKey, itemValue) -> {
                        if (itemKey.startsWith(key.substring(0, entry.getKey().indexOf("[")))) {
                            yamlBuffer.append(getSpace(deep + 1) + "- ");
                            if (itemValue instanceof Map) {
                                StringBuffer valStr = map2Yaml((Map<String, Object>) itemValue, 0);
                                String[] split = valStr.toString().split(StrUtil.LF);
                                for (int i = 0; i < split.length; i++) {
                                    if (i > 0) {
                                        yamlBuffer.append(getSpace(deep + 2));
                                    }
                                    yamlBuffer.append(split[i]).append(StrUtil.LF);
                                }
                            } else {
                                yamlBuffer.append(itemValue + "\n");
                            }
                        }
                    });
                    break;
                } else {
                    String key = space + entry.getKey() + ":";
                    if (valObj instanceof String) { //值为value 类型,不用再继续遍历
                        yamlBuffer.append(key + " " + valObj + "\n");
                    } else if (valObj instanceof List) { //yaml List 集合格式
                        yamlBuffer.append(key + "\n");
                        List<String> list = (List<String>) entry.getValue();
                        String lSpace = getSpace(deep + 1);
                        for (String str : list) {
                            yamlBuffer.append(lSpace + "- " + str + "\n");
                        }
                    } else if (valObj instanceof Map) { //继续递归遍历
                        Map<String, Object> valMap = (Map<String, Object>) valObj;
                        yamlBuffer.append(key + "\n");
                        StringBuffer valStr = map2Yaml(valMap, deep + 1);
                        yamlBuffer.append(valStr.toString());
                    } else {
                        yamlBuffer.append(key + " " + valObj + "\n");
                    }
                }

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return yamlBuffer;
    }

    /**
     * 获取缩进空格
     *
     * @param deep
     * @return
     */
    private static String getSpace(int deep) {
        StringBuffer buffer = new StringBuffer();
        if (deep == 0) {
            return "";
        }
        for (int i = 0; i < deep; i++) {
            buffer.append("  ");
        }
        return buffer.toString();
    }
}

5、总结

这种实现方式优点如下:

  1. 动态刷新配置,不需要重启即可改变程序中的静态常量值
  2. 使用简单,只需在常量类上添加一个注解
  3. 避免在程序中大量使用@Value,@RefreshScope注解
posted @ 2024-05-16 13:18  丨吴丨  阅读(36)  评论(0编辑  收藏  举报