Nacos热更新静态变量配置
Springboot项目接入nacos,配置文件统一管理,但静态常量无法通过@Value注解实时热更新(如下所示)。
GlobalVariables.java
@Component
public class GlobalVariables {
//测试热加载配置字段
public static String testInfo;
@Value("${testInfo}")
public void setTestInfo(String value) {
testInfo = value;
}
}
解决思路:
- 项目初始化时获取所有nacos的配置
- 遍历这些配置文件,从nacos上获取配置
- 寻找配置文件对应的常量类,从spring容器中寻找 常量类 有注解NacosConfig
- 使用JAVA反射更改常量类的值
- 增加监听,用于动态刷新
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 配置
配置增加中加入参数 testInfo
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 com.gisquest.common.core.util.PropertiesUtil;
import com.gisquest.xmgzt.config.NacosConfig;
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、总结
这种实现方式优点如下:
- 动态刷新配置,不需要重启即可改变程序中的静态常量值
- 使用简单,只需在常量类上添加一个注解
- 避免在程序中大量使用@Value,@RefreshScope注解