基於NACOS和JAVA反射機制動態更新JAVA靜態常量非@Value註解

若星漢浮雲發表於2020-06-18

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註解

 不足:

此程式碼是個人業餘時間的想法,未經過生產驗證,實現的資料型別暫時只寫幾個,其餘的需要自行擴充

相關文章