Spring Boot 2系列(六十三):动态刷新环境配置和Bean属性值

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 提供了文件更改监听器组件,可直接引用。添加依赖,如下:

1
2
3
4
5
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>

创建一个随系统启动的文件更改监视器,在监视器里维护观察者,观察者里维护监听者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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;

/**
* 文件更改监听器的运行器。
*
* @author gxing
* @date 2023/1/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创建两个观察者。如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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 监听文件更新的事件
*
* @author gxing
* @date 2023/1/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 观察者的调用。在具体的方法里完成自己的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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;

/**
* 配置文件更改监听器
*
* @author gxing
* @date 2023/1/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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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上下文刷新器
* @author gxing
* @date 2023/1/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 {

}
}

环境变更事件类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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}.
*
* @author Dave Syer
*/
@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 才会生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/*
* 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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/*
* 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 中的两个方法。

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

代码示例

配置文件更改监听器

ApplicationConfigFileAlterationListener 继承了 FileAlterationListenerAdaptor,也实现了 CommandLineRunner。

是把文件更改监视器与文件更新监听器合并了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@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

1
2
com.gxitsky.name=gxing
nickName=gxitsky

environmentRefresher.refreshEnvironment(); 前后添加打加 Profile。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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());

}

修改配置文件的两个属性值,观察输出日志:

1
2
3
4
5
6
7
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属性则没有生效。

Spring Boot 2系列(六十三):动态刷新环境配置和Bean属性值

http://blog.gxitsky.com/2023/01/31/SpringBoot-63-Auto-Refresh-Environment-Config-Data/

作者

光星

发布于

2023-01-31

更新于

2023-02-01

许可协议

评论