SpringBoot动态更新外部属性文件

摘要

  • 本文内容基于springboot2.2.6
  • SpringBoot可以通过@PropertySource(value = "file:demo.properties")的方式加载外部配置文件,这样打好jar包后只要将这个属性文件放到相同路径即可
  • 如果能够在不重启服务的情况下就可以重新加载这个属性文件,就可以很方便的实现动态更新,那么要怎么做呢?
  • github:https://github.com/hanqunfeng/springbootchapter/tree/master/chapter27

思路

SpringCloud可以通过config组件实现配置的动态加载,我们也可以将数据存在数据库或者缓存中,可是如果只是一个小项目,不想依赖任何中间件,那么就可以通过如下的方式实现。

  • 获取所有注解了@PropertySource的对象,并且获取其value属性数组中是以file:开头的文件路径

  • 判断是否同时注解了@ConfigurationProperties,并且获取其prefix的值

  • 对每个属性文件进行遍历,通过反射找到对象的field名称(去除prefix后的名字),并将属性值赋值给该field

代码

这个类要注册到spring上下文,并在需要的地方调用该对象的refresh方法即可重新加载所有外部属性文件。

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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.*;

/**
* <p>动态加载外部属性处理类</p>
*/
@Slf4j
@Component
public class ExternalPropertiesRefresh {

@Autowired
private ConfigurableListableBeanFactory configurableListableBeanFactory;

/**
* <p>根据属性名获取属性值</p>
*
* @param fieldName bean的属性名称
* @param object bean对象
* @return java.lang.Object get方法返回值
* @author hanqf
*/
private Object getFieldValueByName(String fieldName, Object object) {
try {
String firstLetter = fieldName.substring(0, 1).toUpperCase();
String getter = "get" + firstLetter + fieldName.substring(1);
Method method = object.getClass().getMethod(getter, new Class[]{});
Object value = method.invoke(object, new Object[]{});
return value;
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}


/**
* <p>根据属性名设置属性值</p>
*
* @param fieldName bean的属性名称
* @param object bean对象
* @param paramTypes set方法参数类型
* @param params set方法参数值
* @author hanqf
*/
private void setFieldValueByName(String fieldName, Object object, Class[] paramTypes, Object[] params) {
try {
String firstLetter = fieldName.substring(0, 1).toUpperCase();
String setter = "set" + firstLetter + fieldName.substring(1);
Method method = object.getClass().getMethod(setter, paramTypes);
method.invoke(object, params);

} catch (Exception e) {
log.error(e.getMessage(), e);
}
}


/**
* <p>获取属性名称,去除前缀</p>
*
* @param key 属性key
* @param prefix 属性key前缀
* @return java.lang.String
* @author hanqf
*/
private String fieldName(String key, String prefix) {
if (StringUtils.hasText(prefix)) {
return key.replace(prefix + ".", "");
}
return key;
}

/**
* <p>将属性文件值绑定到bean对象</p>
*
* @param bean
* @param properties
* @param prefix
* @author hanqf
*/
private Object bind(Object bean, Properties[] properties, String prefix) {
String fieldName = "";//属性名称
String pValue = "";//属性值
String[] sp = null; //map属性分割key和value
for (Properties pro : properties) {
Map<String, Map<String, String>> fidleMap = new HashMap<>();
Map<String, Set<String>> fidleSet = new HashMap<>();
Map<String, List<String>> fidleList = new HashMap<>();
//遍历属性
for (Object key : pro.keySet()) {
pValue = (String) (pro.get(key));
fieldName = fieldName((String) key, prefix);

//map
sp = fieldName.split("\\.");
if (sp.length == 2) {
fieldName = sp[0];
}

//list&&set
if (fieldName.indexOf("[") > 0) {
fieldName = fieldName.substring(0, fieldName.indexOf("["));
}

//属性类型
Object object = getFieldValueByName(fieldName, bean);

//类型匹配
if (object instanceof Map) {
if (fidleMap.get(fieldName) != null) {
object = fidleMap.get(fieldName);
} else {
object = new HashMap<String, String>();
}
if (sp.length == 2) {
((Map) object).put(sp[1], pValue);
fidleMap.put(fieldName, (Map<String, String>) object);
}
} else if (object instanceof Set) {
if (fidleSet.get(fieldName) != null) {
object = fidleSet.get(fieldName);
} else {
object = new HashSet<String>();
}
((Set) object).add(pValue);
fidleSet.put(fieldName, (Set<String>) object);
} else if (object instanceof List) {
if (fidleList.get(fieldName) != null) {
object = fidleList.get(fieldName);
} else {
object = new ArrayList<String>();
}
((List) object).add(pValue);
fidleList.put(fieldName, (List<String>) object);
} else if (object instanceof String) {
setFieldValueByName(fieldName, bean, new Class[]{String.class}, new Object[]{pValue});
} else if (object instanceof Integer) {
setFieldValueByName(fieldName, bean, new Class[]{Integer.class}, new Object[]{Integer.valueOf(pValue)});
} else if (object instanceof Long) {
setFieldValueByName(fieldName, bean, new Class[]{Long.class}, new Object[]{Long.valueOf(pValue)});
} else if (object instanceof Double) {
setFieldValueByName(fieldName, bean, new Class[]{Double.class}, new Object[]{Double.valueOf(pValue)});
} else if (object instanceof Float) {
setFieldValueByName(fieldName, bean, new Class[]{Float.class}, new Object[]{Float.valueOf(pValue)});
}
}

//map类型赋值
if (fidleMap.size() > 0) {
for (String fname : fidleMap.keySet()) {
setFieldValueByName(fname, bean, new Class[]{Map.class}, new Object[]{fidleMap.get(fname)});
}
}

//set类型赋值
if (fidleSet.size() > 0) {
for (String fname : fidleSet.keySet()) {
setFieldValueByName(fname, bean, new Class[]{Set.class}, new Object[]{fidleSet.get(fname)});
}
}

//list类型赋值
if (fidleList.size() > 0) {
for (String fname : fidleList.keySet()) {
setFieldValueByName(fname, bean, new Class[]{List.class}, new Object[]{fidleList.get(fname)});
}
}

}

return bean;
}


/**
* <p>刷新指定属性类</p>
* @author hanqf
* @param beanName bean的注册名称,默认类名称首字母小写
*/
@SneakyThrows
public void refresh(String beanName){
Class<?> cls = configurableListableBeanFactory.getType(beanName);
Object bean = configurableListableBeanFactory.getBean(cls);
Properties[] propertiesArray = null;
String prefix = "";
if (cls.getAnnotations() != null && cls.getAnnotations().length > 0) {
for (Annotation annotation : cls.getAnnotations()) {

if (annotation instanceof PropertySource) {
PropertySource propertySource = (PropertySource) annotation;
String[] values = propertySource.value();
if (values.length > 0) {
propertiesArray = new Properties[values.length];
for (int i = 0; i < values.length; i++) {
//如果引用的是外部文件,则重新加载
if (values[i].startsWith("file:")) {
String path = values[i].replace("file:", "");
Properties properties = PropertiesLoaderUtils.loadProperties(new FileSystemResource(path));
propertiesArray[i] = properties;
}
}
}
}

if (annotation instanceof ConfigurationProperties) {
ConfigurationProperties configurationProperties = (ConfigurationProperties) annotation;
prefix = configurationProperties.prefix();
}

}
}

if (propertiesArray != null && propertiesArray.length > 0) {
//将属性绑定到对象
bind(bean, propertiesArray, prefix);

}
}


/**
* <p>重新加载属性文件</p>
*
* @author hanqf
*/
@SneakyThrows
public void refresh() {
String[] ary = configurableListableBeanFactory.getBeanNamesForAnnotation(PropertySource.class);
if (ary != null && ary.length > 0) {
for (String beanName : ary) {
//通过Spring的beanName获取bean的类型
refresh(beanName);
}
}
}
}

下面通过http请求刷新配置文件

启动服务器后,任意修改属性文件的值,然后请求/refresh,即可重新加载全部属性文件,然后请求/demo查看是否生效,也可以请求/propertiesDemo/refresh,指定要刷新的对象。

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
@RestController
public class DemoController {
@Autowired
private ExternalPropertiesRefresh externalPropertiesRefresh;

@Autowired
private PropertiesDemo propertiesDemo;


@RequestMapping("/refresh")
public String refreshpro() {
externalPropertiesRefresh.refresh();
return "refresh properties success";
}

@RequestMapping("/{beanName}/refresh")
public String refreshProByBeanName(@PathVariable String beanName) {
externalPropertiesRefresh.refresh(beanName);
return "refresh properties success for " + beanName;
}

@RequestMapping("/demo")
public PropertiesDemo demo() {
return propertiesDemo;
}

}

PropertiesDemo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
@PropertySource(value = "file:demo.properties",encoding = "utf-8")
@ConfigurationProperties(prefix = "demo.data")
@Data
public class PropertiesDemo {
private Map<String, String> map = new HashMap<>();
private Map<String, String> map2 = new HashMap<>();
private Set<String> set = new HashSet<>();
private Set<String> set2 = new HashSet<>();
private List<String> list = new ArrayList<>();
private List<String> list2 = new ArrayList<>();

private String name;
private Integer age;
private Double salary;
}

demo.properties

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
demo.data.map.client=client
demo.data.map.token=token

demo.data.map2.client=client
demo.data.map2.token=token

demo.data.name=张三
demo.data.age=20
demo.data.salary=12345.67

demo.data.set[0]=beijing
demo.data.set[1]=shanghai
demo.data.set[2]=tianjin

demo.data.set2[0]=guangzhou
demo.data.set2[1]=shenzheng
demo.data.set2[2]=hangzhou


demo.data.list[0]=南极
demo.data.list[1]=北极
demo.data.list[2]=赤道

demo.data.list2[0]=喜马拉雅山
demo.data.list2[1]=噶麦斯山
demo.data.list2[2]=阿尔卑斯山