• Spring Boot如何实现修改了配置文件参数不重启应用也能使修改生效?
  • 发布于 2个月前
  • 785 热度
    0 评论
Spring Boot 应用,修改了配置文件中的参数,希望不重启应用使修改生效,使注入配置参数的 Bean 更新生效。要满足配置文件修改,就需要将Spring Boot的配置文件外部化,而不是在 Spring Boot Jar 包类路径下的文件。Spring Boot的配置文件外部化支持两个路径:file:./和file:./config/,即与 Jar 同级目录中的配置文件,和 Jar 所在目录的 config 子目录中的配置文件。

本文主要参考了 spring-cloud-context 的 refresh 接口的实现。

实现思路
主要思路是:
1.定时监听配置文件是否修改。
2.发生了修改,读取配置文件添加到应用环境(Environment)。

3.获取所有属性配置的 Bean,更新 Bean 的属性值。


实现步骤
文件更改监视器
文件监听的实现原理:创建一个线程随系统启动运行,然后定时循环获取指定文件(目录)的元数据(文件名,大小,最后修改时间等),再与旧的元数据比较得出文件是否发生变化,可做更详细的判断得出是文件(目录)的增、删、改的具体操作,再发布具体事件通知业务方,业务方监听到事件做出相应的逻辑操作。Apache 的 commons-io 提供了文件更改监听器组件,可直接引用。添加依赖,如下:
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>
创建一个随系统启动的文件更改监视器,在监视器里维护观察者,观察者里维护监听者。
package com.gxing.refresh.env.environment;

import com.gxing.refresh.env.content.EnvironmentRefresher;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.ResourceUtils;

import java.io.File;

/**
 * 文件更改监听器的运行器。
 *
 * @堆代码 duidaima.com
 * @date 2023/7/29
 */
@Component
public class FileAlterationMonitorRunner implements CommandLineRunner {

    /* file:./; file:./config/; file:./config/ */
    private static final String DEFAULT_EXTERNAL_CONFIG_FILE_LOCATION = "file:./config/";
    /* config file suffix */
    private static final String[] CONFIG_FILE_SUFFIX = new String[]{".yml", ".properties"};

    private ConfigFileAlterationListener configFileAlterationListener;

    public FileAlterationMonitorRunner(ConfigFileAlterationListener configFileAlterationListener) {
        this.configFileAlterationListener = configFileAlterationListener;
    }

    @Override
    public void run(String... args) throws Exception {
        // 当前目录下的外部配置文件
        File file = ResourceUtils.getFile(DEFAULT_EXTERNAL_CONFIG_FILE_LOCATION);
        // yml 或 properties 文件
        SuffixFileFilter filter = new SuffixFileFilter(CONFIG_FILE_SUFFIX);
        // 创建文件修改观察者
        FileAlterationObserver observer = new FileAlterationObserver(file, filter);
        // 给观察者添加监听器
        observer.addListener(configFileAlterationListener);
        // 创建一个监视线程,以指定的间隔触发任何已注册的FileAlternationObserver。
        FileAlterationMonitor monitor = new FileAlterationMonitor(2000);
        // 给监视线程创建观察者
        monitor.addObserver(observer);
        // 启动监视线程
        monitor.start();
    }
}
分析:FileAlterationMonitor 实现了 Runnable 接口创建线程。在 run()方法里按时间间隔循环调用所有 FileAlterationObserver 文件更改观察者。FileAlterationObserver 观察者执行检查指定路径下的文件(目录),若有更改则循环调用注册在此观察者的所有监听器的对应方法。

注意:commons-io 提供的 FileAlterationObserver 只监视指定的文件(目录),不会深入的监视子目录或子文件。Spring Boot 的配置文件外部化支持两个路径file:./和file:./config/,配置修改动态刷新若也要支持这两个路径,可把这两个路径定义为一个 List,遍历List创建两个观察者。如下示例:
package com.gxing.refresh.env.environment;

import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.ResourceUtils;

import java.io.File;
import java.util.Arrays;
import java.util.List;

/**
 * 应用配置文件更改监听器, 即是 Monitor启动器,也是文件更改 Listener
 * onFileChange 监听文件更新的事件
 *
 * @堆代码 duidaima.com
 * @date 2023/7/29
 */
@Component
public class FileAlterationMonitorRunner implements CommandLineRunner {

    /* file:./; file:./config/; */
    private static final List<String> DEFAULT_EXTERNAL_CONFIG_FILE_LOCATIONS = Arrays.asList("file:./", "file:./config/");
    /* config file suffix */
    private static final String[] CONFIG_FILE_SUFFIX = new String[]{".yml", ".properties"};

    private ConfigFileAlterationListener configFileAlterationListener;

    public FileAlterationMonitorRunner(ConfigFileAlterationListener configFileAlterationListener) {
        this.configFileAlterationListener = configFileAlterationListener;
    }

    @Override
    public void run(String... args) throws Exception {
        // yml 或 properties 文件
        SuffixFileFilter filter = new SuffixFileFilter(DEFAULT_CONFIG_FILE_SUFFIX);
        // 创建一个监视线程,以指定的间隔触发任何已注册的FileAlternationObserver。
        FileAlterationMonitor monitor = new FileAlterationMonitor(2000);
        for (String configFileLocation : DEFAULT_EXTERNAL_CONFIG_FILE_LOCATIONS) {
            // 当前目录下的外部配置文件
            File file = ResourceUtils.getFile(configFileLocation);
            // 创建文件修改观察者
            FileAlterationObserver observer = new FileAlterationObserver(file, filter);
            // 给观察者添加监听器
            observer.addListener(configFileAlterationListener);
            // 给监视线程创建观察者
            monitor.addObserver(observer);
            // 启动监视线程
        }
        monitor.start();
    }
}
文件更改监听器
ConfigFileAlterationListener 文件更改监听器接收 FileAlterationObserver 观察者的调用。在具体的方法里完成自己的逻辑。
package com.gxing.refresh.env.environment;

import com.gxing.refresh.env.content.EnvironmentRefresher;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.ResourceUtils;

import java.io.File;

/**
 * 配置文件更改监听器
 *
 * @堆代码 duidaima.com
 * @date 20237/29
 */
@Component
public class ConfigFileAlterationListener extends FileAlterationListenerAdaptor {

    private EnvironmentRefresher environmentRefresher;

    public ConfigFileAlterationListener(EnvironmentRefresher environmentRefresher) {
        this.environmentRefresher = environmentRefresher;
    }

    /**
     * File changed Event.
     *
     * @param file The file changed (ignored)
     */
    @Override
    public void onFileChange(final File file) {
        // file 在这用不到, 读取所有配置文件
        environmentRefresher.refreshEnvironment();
    }
}
分析:ConfigFileAlterationListener 注册了 EnvironmentRefresher 应用环境刷新器,在监听文件修改事件的方法里触发environmentRefresher.refreshEnvironment() 来执行刷新环境配置。

Environment刷新器
EnvironmentRefresher 是核刷新环境核心类,负责重载所有配置文件,并发布应用级的配置文件修改事件,传入修改的 Keys。
package com.gxing.refresh.env.content;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.gxing.refresh.env.environment.EnvironmentChangeEvent;
import com.gxitsky.utils.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.Banner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.StandardServletEnvironment;

import java.util.*;

/**
 * 应用Environment上下文刷新器
 * @date 2023/7/31
 */
@Slf4j
@Component
public class EnvironmentRefresher {

    private static final String REFRESH_ARGS_PROPERTY_SOURCE = "refreshArgs";

    private static final String[] DEFAULT_PROPERTY_SOURCES = new String[]{
            // order matters, if cli args aren't first, things get messy
            CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME,
            "defaultProperties"};

    private Set<String> standardSources = new HashSet<>(
            Arrays.asList(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME,
                    StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
                    StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME,
                    StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME,
                    StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME,
                    "configurationProperties"));

    private ConfigurableApplicationContext context;
    private ConfigurableEnvironment environment;

    public EnvironmentRefresher(ConfigurableApplicationContext context) {
        this.context = context;
        this.environment = context.getEnvironment();
    }

    public synchronized Set<String> refreshEnvironment() {
        Map<String, Object> before = this.extract(environment.getPropertySources());
        addConfigFilesToEnvironment();
        Map<String, Object> after = this.extract(environment.getPropertySources());
        Set<String> keys = this.changes(before, after).keySet();
        try {
            log.info("Environment properties be changed, keys:{}", JSON.toJsonString(keys));
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
        return keys;
    }

    private ConfigurableApplicationContext addConfigFilesToEnvironment() {
        ConfigurableApplicationContext capture = null;
        try {
            StandardEnvironment environment = copyEnvironment(this.environment);
            SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
                    .bannerMode(Banner.Mode.OFF).web(WebApplicationType.NONE)
                    .environment(environment);
            capture = builder.run();
            if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
                environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE);
            }
            MutablePropertySources target = this.environment.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);
                            // update targetName to preserve ordering
                            targetName = name;
                        } 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;
    }

    private StandardEnvironment copyEnvironment(ConfigurableEnvironment input) {
        StandardEnvironment environment = new StandardEnvironment();
        MutablePropertySources capturedPropertySources = environment.getPropertySources();
        // Only copy the default property source(s) and the profiles over from the main
        // environment (everything else should be pristine, just like it was on startup).
        for (String name : DEFAULT_PROPERTY_SOURCES) {
            if (input.getPropertySources().contains(name)) {
                if (capturedPropertySources.contains(name)) {
                    capturedPropertySources.replace(name,
                            input.getPropertySources().get(name));
                } else {
                    capturedPropertySources.addLast(input.getPropertySources().get(name));
                }
            }
        }
        environment.setActiveProfiles(input.getActiveProfiles());
        environment.setDefaultProfiles(input.getDefaultProfiles());
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("spring.jmx.enabled", false);
        map.put("spring.main.sources", "");
        // gh-678 without this apps with this property set to REACTIVE or SERVLET fail
        map.put("spring.main.web-application-type", "NONE");
        capturedPropertySources
                .addFirst(new MapPropertySource(REFRESH_ARGS_PROPERTY_SOURCE, map));
        return environment;
    }

    private Map<String, Object> changes(Map<String, Object> before, Map<String, Object> after) {
        Map<String, Object> result = new HashMap<String, Object>();
        for (String key : before.keySet()) {
            if (!after.containsKey(key)) {
                result.put(key, null);
            } else if (!equal(before.get(key), after.get(key))) {
                result.put(key, after.get(key));
            }
        }
        for (String key : after.keySet()) {
            if (!before.containsKey(key)) {
                result.put(key, after.get(key));
            }
        }
        return result;
    }

    private boolean equal(Object one, Object two) {
        if (one == null && two == null) {
            return true;
        }
        if (one == null || two == null) {
            return false;
        }
        return one.equals(two);
    }

    private Map<String, Object> extract(MutablePropertySources propertySources) {
        Map<String, Object> result = new HashMap<String, Object>();
        List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>();
        for (PropertySource<?> source : propertySources) {
            sources.add(0, source);
        }
        for (PropertySource<?> source : sources) {
            if (!this.standardSources.contains(source.getName())) {
                extract(source, result);
            }
        }
        return result;
    }

    private void extract(PropertySource<?> parent, Map<String, Object> result) {
        if (parent instanceof CompositePropertySource) {
            try {
                List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>();
                for (PropertySource<?> source : ((CompositePropertySource) parent)
                        .getPropertySources()) {
                    sources.add(0, source);
                }
                for (PropertySource<?> source : sources) {
                    extract(source, result);
                }
            } catch (Exception e) {
                return;
            }
        } else if (parent instanceof EnumerablePropertySource) {
            for (String key : ((EnumerablePropertySource<?>) parent).getPropertyNames()) {
                result.put(key, parent.getProperty(key));
            }
        }
    }

    @Configuration(proxyBeanMethods = false)
    protected static class Empty {

    }
}
环境变更事件类
package com.gxing.refresh.env.environment;

import org.springframework.context.ApplicationEvent;
import org.springframework.core.env.Environment;

import java.util.Set;

/**
 * Event published to signal a change in the {@link Environment}.
 *
 */
@SuppressWarnings("serial")
public class EnvironmentChangeEvent extends ApplicationEvent {

    private Set<String> keys;

    public EnvironmentChangeEvent(Set<String> keys) {
        // Backwards compatible constructor with less utility (practically no use at all)
        this(keys, keys);
    }

    public EnvironmentChangeEvent(Object context, Set<String> keys) {
        super(context);
        this.keys = keys;
    }

    /**
     * @return The keys.
     */
    public Set<String> getKeys() {
        return this.keys;
    }

}
分析:到这一步,重载了配置文件,刷新了应用的 Environment ,但绑定配置的 Bean 的属性值还没有刷新,还需要下一步。

配置属性重新绑定器
此绑定器是一个 ApplicationListener 监听器,监听 EnvironmentRefresher 发布的配置文件发生修改事件,调用 rebind()方法重新绑定@ConfigurationProperties 注解的 Bean 的属性值,即只对使用了 @ConfigurationProperties 注解的 Bean 才会生效。
/*
 * Copyright 2012-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.gxing.refresh.env.properties;

import com.gxing.refresh.env.environment.EnvironmentChangeEvent;
import com.gxing.refresh.env.utils.ProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.Environment;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Listens for {@link EnvironmentChangeEvent} and rebinds beans that were bound to the
 * {@link Environment} using {@link ConfigurationProperties
 * <code>@ConfigurationProperties</code>}. When these beans are re-bound and
 * re-initialized, the changes are available immediately to any component that is using
 * the <code>@ConfigurationProperties</code> bean.
 * 
 * @author Dave Syer
 */
@Component
@ManagedResource
public class ConfigurationPropertiesRebinder implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {

    private ConfigurationPropertiesBeans beans;

    private ApplicationContext applicationContext;

    private Map<String, Exception> errors = new ConcurrentHashMap<>();

    public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {
        this.beans = beans;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * A map of bean name to errors when instantiating the bean.
     *
     * @return The errors accumulated since the latest destroy.
     */
    public Map<String, Exception> getErrors() {
        return this.errors;
    }

    @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) {
                    // TODO: determine a more general approach to fix this.
                    // see https://github.com/spring-cloud/spring-cloud-commons/issues/571
                    if (getNeverRefreshable().contains(bean.getClass().getName())) {
                        return false; // ignore
                    }
                    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;
    }

    @ManagedAttribute
    public Set<String> getNeverRefreshable() {
        String neverRefresh = this.applicationContext.getEnvironment().getProperty(
                "spring.cloud.refresh.never-refreshable",
                "com.zaxxer.hikari.HikariDataSource");
        return StringUtils.commaDelimitedListToSet(neverRefresh);
    }

    @ManagedAttribute
    public Set<String> getBeanNames() {
        return new HashSet<>(this.beans.getBeanNames());
    }

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

}
分析:这里需要从 Bean 容器中拿出所有配置属性的 Bean,才能对其属性值重新绑定。

配置属性的所有Bean
创建一个容器维护所有是配置属性的 Bean。
/*
 * Copyright 2012-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.gxing.refresh.env.properties;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.context.properties.ConfigurationPropertiesBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Collects references to <code>@ConfigurationProperties</code> beans in the context and
 * its parent.
 *
 * @author Dave Syer
 */
@Component
public class ConfigurationPropertiesBeans implements BeanPostProcessor, ApplicationContextAware {

    private Map<String, ConfigurationPropertiesBean> beans = new HashMap<>();

    private ApplicationContext applicationContext;

    private ConfigurableListableBeanFactory beanFactory;

    private ConfigurationPropertiesBeans parent;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException {
        this.applicationContext = applicationContext;
        if (applicationContext.getAutowireCapableBeanFactory() instanceof ConfigurableListableBeanFactory) {
            this.beanFactory = (ConfigurableListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
        }
        if (applicationContext.getParent() != null && applicationContext.getParent()
                .getAutowireCapableBeanFactory() instanceof ConfigurableListableBeanFactory) {
            ConfigurableListableBeanFactory listable = (ConfigurableListableBeanFactory) applicationContext
                    .getParent().getAutowireCapableBeanFactory();
            String[] names = listable
                    .getBeanNamesForType(ConfigurationPropertiesBeans.class);
            if (names.length == 1) {
                this.parent = (ConfigurationPropertiesBeans) listable.getBean(names[0]);
                this.beans.putAll(this.parent.beans);
            }
        }
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName);
        if (propertiesBean != null) {
            this.beans.put(beanName, propertiesBean);
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)
            throws BeansException {
        return bean;
    }

    public Set<String> getBeanNames() {
        return new HashSet<String>(this.beans.keySet());
    }

}
分析:ConfigurationPropertiesBeans 实现了 BeanPostProcessor,重写了 postProcessBeforeInitialization方法,在此方法中过滤出是 @ConfigurationProperties 注解的的 Bean,依赖的是 ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName)实现。

BeanPostProcessor 称为是 Bean 的后置处理器。Spring 容器创建 Bean 对象后,在初始化前后都会调用 BeanPostProcessor 中的两个方法。
1.postProcessBeforeInitialization方法会在每个 Bean 对象的初始化方法调用之前回调。

2.postProcessAfterInitialization方法会在每个 Bean 对象的初始化方法调用之后回调。


代码示例
配置文件更改监听器
ApplicationConfigFileAlterationListener 继承了 FileAlterationListenerAdaptor,也实现了 CommandLineRunner。
是把文件更改监视器与文件更新监听器合并了。
package com.gxing.refresh.env.environment;

import com.gxing.refresh.env.content.EnvironmentRefresher;
import com.gxing.refresh.env.properties.Profile;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.ResourceUtils;

import java.io.File;

/**
 * 应用配置文件更改监听器, 即是 Monitor启动器,也是文件更改 Listener
 * onFileChange 监听文件更新的事件
 *
 * @author gxing
 * @date 2023/1/29
 */
@Slf4j
@Component
public class ApplicationConfigFileAlterationListener extends FileAlterationListenerAdaptor implements CommandLineRunner {

    /* file:./config/ */
    private static final String DEFAULT_EXTERNAL_CONFIG_FILE_LOCATION = "file:./config/";
    /* config file suffix */
    private static final String[] CONFIG_FILE_SUFFIX = new String[]{".yml", ".properties"};

    private EnvironmentRefresher environmentRefresher;

    public ApplicationConfigFileAlterationListener(EnvironmentRefresher environmentRefresher) {
        this.environmentRefresher = environmentRefresher;
    }

    /**
     * @desc 随系统启动开启文件修改的监听器.
     * @author gxing
     * @date 2023/1/31 13:59
     */
    @Override
    public void run(String... args) throws Exception {
        // 当前目录下的外部配置文件
        File file = ResourceUtils.getFile(DEFAULT_EXTERNAL_CONFIG_FILE_LOCATION);
        // yml 或 properties 文件
        SuffixFileFilter filter = new SuffixFileFilter(CONFIG_FILE_SUFFIX);
        // 创建文件修改观察者
        FileAlterationObserver observer = new FileAlterationObserver(file, filter);
        // 给观察者添加监听器
        observer.addListener(this);
        // 创建一个监视线程,以指定的间隔触发任何已注册的FileAlternationObserver。
        FileAlterationMonitor monitor = new FileAlterationMonitor(5000);
        // 给监视线程创建观察者
        monitor.addObserver(observer);
        // 启动监视线程
        monitor.start();
    }

    /**
     * File changed Event.
     *
     * @param file The file changed (ignored)
     */
    @Override
    public void onFileChange(final File file) {
        // file 在这用不到, 读取所有配置文件
        environmentRefresher.refreshEnvironment();
    }
}
Environment刷新器
同上

配置属性重新绑定器
同上

配置属性的所有Bean
同上

测试配置文件更新
创建一个 ConfigurationProperties 的 Bean。
@Configuration
@ConfigurationProperties(prefix = "com.gxitsky")
public class Profile {

    private String name;

    @Value("${nickName}")
    private String nickName;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        this.nickName = nickName;
    }

    @Override
    public String toString() {
        return "Profile{" +
                "name='" + name + '\'' +
                ", nickName='" + nickName + '\'' +
                '}';
    }
}
配置文件添加两个属性,并修改这两个值。
application.properties
com.gxitsky.name=gxing
nickName=gxitsky
在 environmentRefresher.refreshEnvironment(); 前后添加打加 Profile。
@Autowired
private Profile profile;

@Override
public void onFileChange(final File file) {
    log.info("File Changed: {}", file.getAbsolutePath());
    System.out.println(profile.toString());

    // file 在这用不到, 读取所有配置文件
    environmentRefresher.refreshEnvironment();

    System.out.println(profile.toString());

}
修改配置文件的两个属性值,观察输出日志:
2023-02-01 10:19:47.814  INFO 8772 --- [      Thread-10] .ApplicationConfigFileAlterationListener : File Changed: E:\git\gitee\gxing-demo\.\config\application.properties
Profile{name='gxing', nickName='xing'}
2023-02-01 10:19:47.843  INFO 8772 --- [      Thread-10] o.s.boot.SpringApplication               : Starting application using Java 1.8.0_241 on DESKTOP-NAPJLIF with PID 8772 (started by Administrator in E:\git\gitee\gxing-demo)
2023-02-01 10:19:47.844  INFO 8772 --- [      Thread-10] o.s.boot.SpringApplication               : No active profile set, falling back to 1 default profile: "default"
2023-02-01 10:19:47.847  INFO 8772 --- [      Thread-10] o.s.boot.SpringApplication               : Started application in 0.029 seconds (JVM running for 17.878)
2023-02-01 10:19:47.848  INFO 8772 --- [      Thread-10] c.g.r.env.content.EnvironmentRefresher   : Environment properties be changed, keys:["nickName","com.gxitsky.name"]
Profile{name='gxing1', nickName='xing'}
可以看到系统监听到了 \config\application.properties 文件发生更改,使用 @ConfigurationProperties(prefix = "com.gxitsky")注解注入的 name 属性已经生效;使用 @Value 注解的 nickName属性则没有生效。
用户评论