基于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注解
不足:
此代码是个人业余时间的想法,未经过生产验证,实现的数据类型暂时只写几个,其余的需要自行拓展