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註解
不足:
此程式碼是個人業餘時間的想法,未經過生產驗證,實現的資料型別暫時只寫幾個,其餘的需要自行擴充