1.前言java
項目中都會使用常量類文件, 這些值若是須要變更須要從新提交代碼,或者基於@Value註解實現動態刷新, 若是常量太多也是很麻煩; 那麼 能不能有更加簡便的實現方式呢?spring
本文講述的方式是, 一個JAVA類對應NACOS中的一個配置文件,優先使用nacos中的配置,不配置則使用程序中的默認值;apache
2.正文
json
nacos的配置以下圖所示,爲了知足大多數狀況,配置了 namespace命名空間和group;bootstrap
新建個測試工程 cloud-sm.api
bootstrap.yml 中添加nacos相關配置;app
爲了支持多配置文件須要注意ext-config節點,group對應nacos的添加的配置文件的group; data-id 對應nacos上配置的data-idide
配置以下:測試
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
接下來是本文重點:ui
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註解
不足:
此代碼是我的業餘時間的想法,未通過生產驗證,實現的數據類型暫時只寫幾個,其他的須要自行拓展