Dynamic update of JAVA static constant non @ Value annotation based on NACOS and JAVA reflection mechanism

1. Preface

Constant class files are used in projects. If the values need to be changed, the code needs to be resubmitted, or the @ Value annotation is used to realize dynamic refresh. If there are too many constants, it is also very troublesome. Can there be a simpler way to implement them?

This article describes the way that a JAVA class corresponds to a configuration file in Nacos, and the configuration in Nacos is preferred. If not, the default value in the program is used;

2. Text

The configuration of nacos is shown in the figure below. In order to meet most situations, the namespace namespace and group are configured;

 

 

Create a new test project cloud SM

bootstrap.yml Add nacos related configuration in;

In order to support multiple configuration files, you need to pay attention to the EXT config node. Group corresponds to the group of the added configuration file of nacos. Data ID corresponds to the data ID configured on nacos

The configuration is as follows:

server:
  port: 9010
  servlet:
    context-path: /sm
spring:
  application:
    name: cloud-sm
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.100.101:8848 #Address of Nacos service registry
        namespace: 1
      config:
        server-addr: 192.168.100.101:8848 #Nacos as configuration center address
        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

Next is the focus of this paper:

1) Create a new annotation ConfigModule, which is used on the configuration class; a value attribute;

2) New listening class to get latest configuration and update constant value

Implementation process:

1) Get configuration of all nacos during project initialization

2) Traverse these configuration files to get the configuration from nacos

3) Traverse nacos configuration file to get module_ Value of name

4) Find the constant class corresponding to the configuration file. Find the constant class from the spring container with the annotation ConfigModule and the value is MODULE_NAME

5) Using JAVA reflection to change the value of a constant class

6) Add monitor for dynamic refresh

 

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 {
    /**
     *  The key in the corresponding configuration file is (module_ Value of 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 Custom listening
 *
 * @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;
    /**
     * Currently, only the properties file is considered
     */
    private String fileType = "properties";
    /**
     * A module needs to be added to the configuration file_ Name to find the corresponding constant class
     */
    private String MODULE_NAME = "MODULE_NAME";

    /**
     * NACOS Listener method 
     *
     * @throws NacosException
     */
    public void listener() throws NacosException {
        if (StringUtils.isBlank(serverAddr)) {
            LOGGER.info("not found 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);
        // Process each profile
        for (NacosConfigProperties.Config config : configs.getExtConfig()) {
            String dataId = config.getDataId();
            String group = config.getGroup();
            //At present, only properties file
            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;
                }
            });
        }
    }

    /**
     * Changing the value of a constant class
     *
     * @param configInfo
     */
    private void changeValue(String configInfo) {
        if(StringUtils.isBlank(configInfo)) return;
        Properties proper = new Properties();
        try {
            proper.load(new StringReader(configInfo)); //Convert string to reader
        } catch (IOException e) {
            e.printStackTrace();
        }
        String moduleName = "";
        Enumeration enumeration = proper.propertyNames();
        //seek MODULE_NAME Value of
        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;
        // Find the constant class corresponding to the configuration file
        // from spring The annotations for finding classes in the container are ConfigModule And the value is MODULE_NAME Corresponding
        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;
        // use JAVA Reflection mechanism changes constant
        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);
                //Ignore access to properties
                field.setAccessible(true);
                Class<?> curFieldType = field.getType();
                //Other types of self expansion
                if (curFieldType.equals(String.class)) {
                    field.set(null, value);
                } else if (curFieldType.equals(List.class)) { // aggregate List element
                    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("Failed to set properties:{} {} = {} ", curClazz.toString(), key, value);
            }
        }
    }

    @PostConstruct
    public void init() throws NacosException {
        listener();
    }
}

3. Test

1) Create a new Constant class, add the annotation @ ConfigModule("sm"), try to test comprehensively, and add Constant types such as 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("Default");
    }
    public static volatile Map<String,Object> TEST_MAP = new HashMap<>();
    static {
        TEST_MAP.put("KEY","Initialize defaults");
    }
    public static volatile List<Integer> TEST_LIST_INT = new ArrayList<>();
    static {
        TEST_LIST_INT.add(1);
    }
}

2) Create a new Controller to test these values

@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) Current nacos configuration file cloud SM- constant.properties Empty

4) access test path localhost:9010/sm/t1 , return to default

{
    "code": 1,
    "string": "test",
    "list_int": [
        1
    ],
    "list": [
        "Default"
    ],
    "map": {
        "KEY": "Initialize defaults"
    }
}

5) Then change the configuration file cloud sm of nacos- constant.properties ;

6) access the test path again localhost:9010/sm/t1 , returned as a value in nacos

{
    "code": 1,
    "string": "12351",
    "list_int": [
        1,
        23,
        4
    ],
    "list": [
        "123",
        "sss"
    ],
    "map": {
        "A": 12,
        "B": 432
    }
}

4. Conclusion

The advantages of this implementation are as follows:

1) Refresh the configuration dynamically and change the static constant value in the program without restarting

2) Easy to use, just add a comment on the constant class

3) Avoid extensive use in programs@ Value,@RefreshScope annotation

Insufficient:

This code is the idea of personal spare time. Without production verification, only a few data types are written for the time being. The rest need to be expanded by themselves

Tags: Java Spring Attribute Druid

Posted on Thu, 18 Jun 2020 22:05:31 -0400 by Frame