spring boot 配置文件动态更新原理 以Nacos为例

Posted 二奎

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了spring boot 配置文件动态更新原理 以Nacos为例相关的知识,希望对你有一定的参考价值。

配置文件的动态更新 

通常获取配置文件的方式

1, @Value

2. @ConfigurationProperties(Prefix)

如果是在运行时要动态更新的话,

第一种方式要在bean上加@RefreshScope 

第二种方式是自动支持的。

以Nacos为为例,我们可以看下源码是如何实现的:

Nacos获取配置中心是通过单独一个线程的长轮询获取的:

com.alibaba.nacos.client.config.impl.ClientWorker.LongPollingRunnable

当获取到更新配置后,publishEvent

org.springframework.cloud.alibaba.nacos.refresh.NacosContextRefresher#registerNacosListener

private void registerNacosListener(final String group, final String dataId) {

        Listener listener = listenerMap.computeIfAbsent(dataId, i -> new Listener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                refreshCountIncrement();
                String md5 = "";
                if (!StringUtils.isEmpty(configInfo)) {
                    try {
                        MessageDigest md = MessageDigest.getInstance("MD5");
                        md5 = new BigInteger(1, md.digest(configInfo.getBytes("UTF-8")))
                                .toString(16);
                    }
                    catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
                        log.warn("[Nacos] unable to get md5 for dataId: " + dataId, e);
                    }
                }
                refreshHistory.add(dataId, md5);
                applicationContext.publishEvent(
                        new RefreshEvent(this, null, "Refresh Nacos config"));
                if (log.isDebugEnabled()) {
                    log.debug("Refresh Nacos config group " + group + ",dataId" + dataId);
                }
            }

            @Override
            public Executor getExecutor() {
                return null;
            }
        });

        try {
            configService.addListener(dataId, group, listener);
        }
        catch (NacosException e) {
            e.printStackTrace();
        }
    }

 

当收到{@link RefreshEvent}时调用{@link RefreshEventListener#refresh}。

只在收到{@link ApplicationReadyEvent}后响应{@link RefreshEvent},

因为RefreshEvents可能在应用程序生命周期中来得太早。(译文)

public class RefreshEventListener implements SmartApplicationListener {

    private static Log log = LogFactory.getLog(RefreshEventListener.class);

    private ContextRefresher refresh;

    private AtomicBoolean ready = new AtomicBoolean(false);

    public RefreshEventListener(ContextRefresher refresh) {
        this.refresh = refresh;
    }

    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
        return ApplicationReadyEvent.class.isAssignableFrom(eventType)
                || RefreshEvent.class.isAssignableFrom(eventType);
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationReadyEvent) {
            handle((ApplicationReadyEvent) event);
        }
        else if (event instanceof RefreshEvent) {
            handle((RefreshEvent) event);
        }
    }

    public void handle(ApplicationReadyEvent event) {
        this.ready.compareAndSet(false, true);
    }

    public void handle(RefreshEvent event) {
        if (this.ready.get()) { // don‘t handle events before app is ready
            log.debug("Event received " + event.getEventDesc());
            Set<String> keys = this.refresh.refresh();
            log.info("Refresh keys changed: " + keys);
        }
    }

}

 

org.springframework.cloud.context.refresh.ContextRefresher

public synchronized Set<String> refresh() {
        Set<String> keys = refreshEnvironment();
        this.scope.refreshAll();
        return keys;
    }

    public synchronized Set<String> refreshEnvironment() {
        Map<String, Object> before = extract(
                this.context.getEnvironment().getPropertySources());
        addConfigFilesToEnvironment();
        Set<String> keys = changes(before,
                extract(this.context.getEnvironment().getPropertySources())).keySet();
        this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
        return keys;
    }

ConfigurableApplicationContext addConfigFilesToEnvironment() {
        ConfigurableApplicationContext capture = null;
        try {
            StandardEnvironment environment = copyEnvironment(
                    this.context.getEnvironment());
            SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
                    .bannerMode(Mode.OFF).web(WebApplicationType.NONE)
                    .environment(environment);
            // Just the listeners that affect the environment (e.g. excluding logging
            // listener because it has side effects)
            builder.application()
                    .setListeners(Arrays.asList(new BootstrapApplicationListener(),
                            new ConfigFileApplicationListener()));
            capture = builder.run();
            if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
                environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE);
            }
            MutablePropertySources target = this.context.getEnvironment()
                    .getPropertySources();
            String targetName = null;
            for (PropertySource<?> source : environment.getPropertySources()) {
                String name = source.getName();
                if (target.contains(name)) {
                    targetName = name;
                }
                if (!this.standardSources.contains(name)) {
                    if (target.contains(name)) {
                        target.replace(name, source);
                    }
                    else {
                        if (targetName != null) {
                            target.addAfter(targetName, source);
                        }
                        else {
                            // targetName was null so we are at the start of the list
                            target.addFirst(source);
                            targetName = name;
                        }
                    }
                }
            }
        }
        finally {
            ConfigurableApplicationContext closeable = capture;
            while (closeable != null) {
                try {
                    closeable.close();
                }
                catch (Exception e) {
                    // Ignore;
                }
                if (closeable.getParent() instanceof ConfigurableApplicationContext) {
                    closeable = (ConfigurableApplicationContext) closeable.getParent();
                }
                else {
                    break;
                }
            }
        }
        return capture;
    }

然后就重新走了一次启动的流程

/**
     * Run the Spring application, creating and refreshing a new
     * {@link ApplicationContext}.
     * @param args the application arguments (usually passed from a Java main method)
     * @return a running {@link ApplicationContext}
     */
    public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        configureHeadlessProperty();
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
            configureIgnoreBeanInfo(environment);
            Banner printedBanner = printBanner(environment);
            context = createApplicationContext();
            exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                    new Class[] { ConfigurableApplicationContext.class }, context);
            prepareContext(context, environment, listeners, applicationArguments, printedBanner);
            refreshContext(context);
            afterRefresh(context, applicationArguments);
            stopWatch.stop();
            if (this.logStartupInfo) {
                new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
            }
            listeners.started(context);
            callRunners(context, applicationArguments);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, listeners);
            throw new IllegalStateException(ex);
        }

        try {
            listeners.running(context);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, null);
            throw new IllegalStateException(ex);
        }
        return context;
    }

 

org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#onApplicationEvent

public void onApplicationEvent(EnvironmentChangeEvent event) {
        if (this.applicationContext.equals(event.getSource())
                // Backwards compatible
                || event.getKeys().equals(event.getSource())) {
            rebind();
        }
    }

ConfigurationPropertiesRebinder 看见这个名字就知道是怎么回事了。

找到所有的ConfigurationPropertiesBeans, 遍历它们

@ManagedOperation
    public void rebind() {
        this.errors.clear();
        for (String name : this.beans.getBeanNames()) {
            rebind(name);
        }
    }

    @ManagedOperation
    public boolean rebind(String name) {
        if (!this.beans.getBeanNames().contains(name)) {
            return false;
        }
        if (this.applicationContext != null) {
            try {
                Object bean = this.applicationContext.getBean(name);
                if (AopUtils.isAopProxy(bean)) {
                    bean = ProxyUtils.getTargetObject(bean);
                }
                if (bean != null) {
                    this.applicationContext.getAutowireCapableBeanFactory()
                            .destroyBean(bean);
                    this.applicationContext.getAutowireCapableBeanFactory()
                            .initializeBean(bean, name);
                    return true;
                }
            }
            catch (RuntimeException e) {
                this.errors.put(name, e);
                throw e;
            }
            catch (Exception e) {
                this.errors.put(name, e);
                throw new IllegalStateException("Cannot rebind to " + name, e);
            }
        }
        return false;
    }

org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#postProcessBeforeInitialization

@Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        ConfigurationProperties annotation = getAnnotation(bean, beanName, ConfigurationProperties.class);
        if (annotation != null) {
            bind(bean, beanName, annotation);
        }
        return bean;
    }

这个BeanPostProcessor里就对我们要更新的Bean进行更新最新的配置值了。如下列如红色部分

private void bind(Object bean, String beanName, ConfigurationProperties annotation) {
        ResolvableType type = getBeanType(bean, beanName);
        Validated validated = getAnnotation(bean, beanName, Validated.class);
        Annotation[] annotations = (validated != null) ? new Annotation[] { annotation, validated }
                : new Annotation[] { annotation };
        Bindable<?> target = Bindable.of(type).withExistingValue(bean).withAnnotations(annotations);
        try {
            this.configurationPropertiesBinder.bind(target);
        }
        catch (Exception ex) {
            throw new ConfigurationPropertiesBindException(beanName, bean, annotation, ex);
        }
    }

 

 

以上是关于spring boot 配置文件动态更新原理 以Nacos为例的主要内容,如果未能解决你的问题,请参考以下文章

更新 Spring Boot 以生成 WAR 文件而不是 JAR 文件

Spring Boot 多数据源配置

无法使用 Spring Boot 和 Thymeleaf 以动态创建的形式获取更新的值

spring boot apollo 热加载 重启

Spring Boot实战源码解析Spring Boot自动配置原理

spring boot 自动配置原理