摘要
- 本文内容基于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方法即可重新加载所有外部属性文件。
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.*;
/**
* 动态加载外部属性处理类
*/
@Slf4j
@Component
public class ExternalPropertiesRefresh {
@Autowired
private ConfigurableListableBeanFactory configurableListableBeanFactory;
/**
* 根据属性名获取属性值
*
* @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;
}
}
/**
* 根据属性名设置属性值
*
* @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);
}
}
/**
* 获取属性名称,去除前缀
*
* @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;
}
/**
* 将属性文件值绑定到bean对象
*
* @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> fidleMap = new HashMap<>();
Map> fidleSet = new HashMap<>();
Map> 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();
}
if (sp.length == 2) {
((Map) object).put(sp[1], pValue);
fidleMap.put(fieldName, (Map) object);
}
} else if (object instanceof Set) {
if (fidleSet.get(fieldName) != null) {
object = fidleSet.get(fieldName);
} else {
object = new HashSet();
}
((Set) object).add(pValue);
fidleSet.put(fieldName, (Set) object);
} else if (object instanceof List) {
if (fidleList.get(fieldName) != null) {
object = fidleList.get(fieldName);
} else {
object = new ArrayList();
}
((List) object).add(pValue);
fidleList.put(fieldName, (List) 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;
}
/**
* 刷新指定属性类
* @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);
}
}
/**
* 重新加载属性文件
*
* @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,指定要刷新的对象。
@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
@Component
@PropertySource(value = "file:demo.properties",encoding = "utf-8")
@ConfigurationProperties(prefix = "demo.data")
@Data
public class PropertiesDemo {
private Map map = new HashMap<>();
private Map map2 = new HashMap<>();
private Set set = new HashSet<>();
private Set set2 = new HashSet<>();
private List list = new ArrayList<>();
private List list2 = new ArrayList<>();
private String name;
private Integer age;
private Double salary;
}
demo.properties
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]=阿尔卑斯山