Spring boot enables dynamic loading of remote configuration files

demand

There is an independent API project, which mainly provides API interfaces for external systems. In order to ensure the security of the call, the request needs to be verified, including the call frequency, access IP, cross domain and Token. The IP and cross domain configuration will be modified according to the receiver, In order to avoid having to modify the configuration file and restart the project every time there is a new access party, we intend to use dynamic configuration.

Primary implementation scheme: the API service requests data from the management end every 5 minutes, and the management end adds the management of IP and domain white list. This implementation scheme is simple and easy to use, but the disadvantages are also obvious. After the management end modifies the configuration each time, the client needs to wait for the next request before loading the corresponding configuration. At the same time, it also needs to manage the obtained configuration file by itself

Update scheme: when springboot starts, first get the configuration file from the remote end and load it into the Environment object, and leave the rest to Spring. At the same time, cooperate with Spring cloud context to realize the remote configuration change. After the change, pull the configuration locally and update it

Blind toss stage

The following code is based on springboot 2.3.4 Release version

Let's start with the run method
After clicking in, springboot initializes the ConfigurableEnvironment object here

Continue down. Since I'm starting the SERVLET environment here, a standardservlet environment object will be initialized

Here is some initialization work for the ConfigurableEnvironment. Let's ignore it first. The focus is here, listeners environmentPrepared(Environment);, Springboot distributes the loading of the Environment through events

The specific listener is configured in spring In factories, the listeners related to configuration loading are shown in the figure

We go to the ConfigFileApplicationListener class and first judge whether the received event is an ApplicationEnvironmentPreparedEvent event. If so, call the onApplicationEnvironmentPreparedEvent method and use the loadPostProcessors method to get the event from spring Read the configured EnvironmentPostProcessor in factories, sort by Order, and execute in turn


emmm... After running so far, the extension point we need to use is here. We just need to extend an EnvironmentPostProcessor and register it

Concrete implementation

  1. First, define an implementation class to implement the EnvironmentPostProcessor interface
    The reverse order of addresses in the code is because the addAfter method is used when registering PropertySources, resulting in the use order loaded first will be later. In order to make the use order consistent with the writing order of the configuration file, the resource order is reversed here
package fun.fanx.remote.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.boot.env.RandomValuePropertySource;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.boot.logging.DeferredLog;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.lang.NonNull;

import java.io.IOException;
import java.util.List;

/**
 * Remote profile loading
 * @author fan
 */
public class RemoteConfigLoadPostProcessor implements EnvironmentPostProcessor, ApplicationListener<ApplicationPreparedEvent> {
    /**
     * It is used to cache logs and print them when appropriate
     */
    private static final DeferredLog LOGGER = new DeferredLog();


    /**
     * First initialize a yml parser
     * If you load the properties file, you can initialize the {@ link org.springframework.boot.env.PropertiesPropertySourceLoader} object yourself
     */
    private final PropertySourceLoader loader = new YamlPropertySourceLoader();

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        // The binder is used to obtain the address of an object in the remote configuration file
        final Binder binder = Binder.get(environment);
        final String[] configsUrl = binder.bind("xc.config.remote", String[].class).orElse(new String[]{});

        final MutablePropertySources propertySources = environment.getPropertySources();

        final int length = configsUrl.length;
        for (int i = 0; i < length; i++) {
            // The name of the configuration file cannot be consistent. If it is consistent, it will be overwritten
            try {
                loadProperties("defaultXcRemoteConfigure" + i, configsUrl[i], propertySources);
            } catch (IOException e) {
                LOGGER.error("load fail, url is: " + configsUrl[i], e);
            }
        }
    }

    private void loadProperties(String name, String url, MutablePropertySources destination) throws IOException {
        Resource resource = new UrlResource(url);
        if (resource.exists()) {
            // If the resource exists, use PropertySourceLoader to load the configuration file
            List<PropertySource<?>> load = loader.load(name, resource);
            // Put the corresponding resources in front of RandomValuePropertySource to ensure that the loaded remote resources will take precedence over the system configuration
            load.forEach(it -> destination.addBefore(RandomValuePropertySource.RANDOM_PROPERTY_SOURCE_NAME, it));
            LOGGER.info("load configuration success from " + url);
        } else {
            LOGGER.error("get configuration fail from " + url + ", don't load this");
        }
    }

    @Override
    public void onApplicationEvent(@NonNull ApplicationPreparedEvent applicationPreparedEvent) {
        // Print log
        LOGGER.replayTo(RemoteConfigLoadPostProcessor.class);
    }
}

  1. Add the corresponding implementation class to spring In the factories file
org.springframework.boot.env.EnvironmentPostProcessor=fun.fanx.remote.config.RemoteConfigLoadPostProcessor
# Register for log printing
org.springframework.context.ApplicationListener=fun.fanx.remote.config.RemoteConfigLoadPostProcessor
  1. The remote resource address to be loaded is specified in the configuration file, and multiple paths will be read and used in order
xc:
  config:
    remote:
      - file:///Users/fan/remote-config/src/main/resources/test.yml # local file test
      - http://127.0.0.1:8080/properties/dev1.yml
      - http://127.0.0.1:8080/properties/dev2.yml

So far, we can use the configuration file on the server like using the local configuration file, but only the remote configuration file is loaded here. We also need to realize the hot update of the configuration file when the remote configuration file is changed

Cooperate with spring cloud context to realize dynamic refresh of configuration file

  1. Project introduction dependency
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-context</artifactId>
  <version>2.2.0.RELEASE</version><!-- Different versions springboot Different versions are required,Specific correspondence,Can be in https://start.spring.io / get -- >
</dependency>
  1. Just call the refresh method of ContextRefresher. I have exposed an interface here to facilitate the unified call of the management end
package fun.fanx.remote.config;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.context.ApplicationListener;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Configuration management interface
 * @author fan
 */
@Slf4j
@RefreshScope
@RestController
@RequiredArgsConstructor
@RequestMapping("/environment/api")
public class RefreshController implements ApplicationListener<ApplicationStartedEvent> {
    private final ContextRefresher contextRefresher;

    @Value("${remote.test}")
    private String test;

    /**
     * Refresh configuration interface
     * Limit one request per minute
     */
    @GetMapping("refresh")
    public String refresh() {
        contextRefresher.refresh();
        return test;
    }


    @Override
    public void onApplicationEvent(@NonNull ApplicationStartedEvent applicationStartedEvent) {
        System.out.println("Configuration loaded into:" + test);
    }
}

After the ContextRefresher of spring cloud refreshes the configuration, the configuration class annotated with @ ConfigurationProperties will refresh the instance. The parameter Value injected with @ Value needs to be used with the @ RefreshScope annotation to refresh the corresponding Value. For details, please refer to the specific data yourself

demo address

Keywords: Java Spring Spring Boot

Added by Transwarp-Tim on Thu, 10 Mar 2022 14:15:27 +0200