3.获取所有属性配置的 Bean,更新 Bean 的属性值。
<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 观察者执行检查指定路径下的文件(目录),若有更改则循环调用注册在此观察者的所有监听器的对应方法。
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(); } }文件更改监听器
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() 来执行刷新环境配置。
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 的属性值还没有刷新,还需要下一步。
/* * 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,才能对其属性值重新绑定。
/* * 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)实现。
2.postProcessAfterInitialization方法会在每个 Bean 对象的初始化方法调用之后回调。
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刷新器
@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 + '\'' + '}'; } }配置文件添加两个属性,并修改这两个值。
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属性则没有生效。