如何在不重启应用的前提下,在内存中直接修改配置文件中的属性值?
相当于增加一份配置文件,它可以来源于文件或网络,这取决于你如何拿到数据。下面是一个示例,这个属性源有一个名字:myPropertySource,里面只包含了一个属性:abc_123。
/**
* 自定义属性源
*
* @author tianmingxing on 2023/03/12
*/
public class MyPropertySource extends MapPropertySource {
public MyPropertySource() {
super("myPropertySource", new HashMap<>(1));
}
@Override
public Object getProperty(String name) {
if ("abc_123".equals(name)) {
return new Random().nextInt()+"";
}
return super.getProperty(name);
}
@Override
public boolean containsProperty(String name) {
if ("abc_123".equals(name)) {
return true;
}
return super.containsProperty(name);
}
}
事实上你可以从外部网络,比如某个HTTP接口获取一堆属性值,对于你来说无非是将它们映射成KV结构。当然你用其它结构也可以,但是要保证能将一堆属性存储下来,并且能够根据名字(键)快速查找出来。下面示例通过一个接口来获取配置属性:
/**
* 自定义属性源,从HTTP接口获取配置属性集。
*
* @author tianmingxing on 2023/03/12
*/
public class MyPropertySource extends MapPropertySource {
private static final Logger LOG = LoggerFactory.getLogger(MyPropertySource.class);
private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static Map<String, Object> data = Collections.emptyMap();
public MyPropertySource() {
super("MyPropertySource", data);
// 获取远程数据,如果你喜欢用HttpClient或OkHttp也可以的。
HttpRequest request = HttpRequest.newBuilder()
.header("Accept", "application/json")
.GET()
.uri(URI.create("https://www.example.com/api/v1/myPropertySource"))
.build();
try {
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
data = OBJECT_MAPPER.readValue(response.body(), new TypeReference<Map<String, Object>>() {
});
} catch (Exception e) {
LOG.error("无法初始化自定义属性源");
}
}
@Override
public Object getProperty(String name) {
Object o = data.get(name);
if (null != o) {
return o;
}
return super.getProperty(name);
}
@Override
public boolean containsProperty(String name) {
if (data.containsKey(name)) {
return true;
}
return super.containsProperty(name);
}
/**
* 更新属性
*
* @param name
* @param value
* @return 上一个值
*/
public Object updateProperty(String name, Object value) {
if (data.containsKey(name)) {
return data.put(name, value);
}
return null;
}
}
虽然定义了数据源,但Spring框架还不知道,我们希望在Spring容器启动时能将自定义数据源加载进去。
/**
* 自定义属性配置源
*
* @author tianmingxing on 2023/03/12
*/
@Order
public class MyEnvironmentPostProcessor implements EnvironmentPostProcessor {
private final MyPropertySource myPropertySource = new MyPropertySource();
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
if (!environment.getPropertySources().contains("myPropertySource")) {
// 实际上会有多个属性源,我们希望自己的源排在第一位,这样自己的配置就可以覆盖其它来源的属性值了。
environment.getPropertySources().addFirst(myPropertySource);
}
}
/**
* 更新属性
*
* @param name
* @param value
*/
public void updateProperty(String name, Object value) {
myPropertySource.updateProperty(name, value);
}
}
假设我们从HTTP接口拿到了下面的属性集:
my.k1 = v1
k2 = v2
my.k3 = v3
在Bean中直接使用@Value来获取特定属性。按照前面的顺序,Spring会先从我们自定义的属性源中查找,如果查找不到则第二个属性源中找。
@Value("${my.k3}")
private String k3;
@Value("${k2}")
private String k2;
到这里,读取外部配置并让Spring知道,这个目的总算完成了,但你不是说要动态修改属性吗?
按照前面的方法,一但Bean初始化完成,通过@Value获取的属性值将不会变化,即使你修改了数据源中的值。
我们虽然定义了数据源,但它只是在Spring容器初始化时,进行了初始化。如果远程接口中的值发生了变化,应用中如何感知到呢?其实有两种方式来实现:一是定时请求远程接口获取最新数据,二是由远程服务主动将接口推送给应用。咱们这里介绍一下第一种方式:
/**
* 定时更新属性值
*
* @author tianmingxing on 2023/03/16
*/
@Component
public class MyPropertySourceUpdater {
@Autowired
private MyEnvironmentPostProcessor myEnvironment;
/**
* 示例代码,具体过程没有演示,大家可以自己去扩展
*/
@Scheduled(fixedRate = 5_000) // 每5秒钟执行一次
public void update() {
// 1. 从远程接口获取数据,结果中可增加一个数据是否有修改的标识,如果没有则需要自己对比,或者简单点直接全部覆盖。
// 2. 更新属性,由于是引用传递,直接改数据对象即可。
myEnvironment.updateProperty(name, value);
}
}
如果使用Environment获取属性,则每次获取属性都是最新的,不存在动态刷新的问题。
@Autowired
private Environment environment;
public void test() {
String k1 = environment.getProperty(name);
}
但在项目中使用@Value获取属性比较多,我们需要在属性发生变化时,通知对应的Bean同步更新属性值。
/**
* 在postProcessAfterInitialization方法中,我们使用反射获取Bean的所有字段,并检查这些字段是否使用了@Value注解。
* 如果是,则将相关信息记录在annotatedBeans列表中。
*
* @author tianmingxing on 2023/03/16
*/
@Slf4j
@Component
public class ValueAnnotationBeanPostProcessor implements BeanPostProcessor {
/**
* 映射属性名称与关联Bean的关系
* key: 属性名称, value:一个或多个所关联的bean
*/
private final Map<String, List<String>> annotatedBeans = new HashMap<>();
/**
* 提取属性名称的正则
*/
private final Pattern placeholder = Pattern.compile("\\$\\{([\\w._-]*)?:?.*}");
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Field[] fields = bean.getClass().getDeclaredFields();
for (Field field : fields) {
Value valueAnnotation = field.getAnnotation(Value.class);
if (valueAnnotation != null) {
field.setAccessible(true);
try {
log.debug(beanName + "." + field.getName() + ": " + field.get(bean) + ", value = " + valueAnnotation.value());
// valueAnnotation.value()取出来是"${my.k1}",需要截取出里面的属性名称
Matcher matcher = placeholder.matcher(valueAnnotation.value());
if (matcher.find()) {
String propertyName = matcher.group(1);
List<String> values = annotatedBeans.get(propertyName);
if (null == values) {
values = new ArrayList<>(1);
}
values.add(beanName);
annotatedBeans.put(propertyName, values);
}
} catch (IllegalAccessException e) {
throw new RuntimeException("无法访问注解字段: " + field.getName(), e);
}
}
}
return bean;
}
public Map<String, List<String>> getAnnotatedBeans() {
return annotatedBeans;
}
}
现在只需要监听哪个属性发生变化,然后通过getAnnotatedBeans找到对应的Bean,就可以来更新Bean的属性了。
具体怎么监听的方式,上面有介绍,这里不再赘述。只是在对比哪些值发生改变时,需要特别处理下,为了简化应用中对比的难度,可以由提供接口的服务方,明确指出哪些字段有更新,而不是每次都返回全部。
在监听到某个属性发生变化后,找到其关联到的所有Bean,然后逐一进行属性更新。
/**
* 动态更新Bean的属性值,不需要销毁的方式。
*
* @author tianmingxing on 2023/03/16
*/
@Component
public class BeanPropertyUpdater {
/**
* 缓存BeanWrapper避免每次重新创建。
* 不过存在一个风险:如果bean被销毁再重建,那缓存起来的BeanWrapper就不起作用了。
* 一般这种情况较少,如果确实有个把Bean需要这么玩,可以加上对Bean生命周期的监听,然后再移除这边的缓存。
* 如果属性变化不是特别频繁,这里不缓存也是可以的。
* key:Bean名称,value:BeanWrapper
*/
private final Map<String, BeanWrapper> beanWrapperMap = new HashMap<>();
@Autowired
private ApplicationContext context;
/**
* 更新Bean的属性值
*
* @param beanName
* @param propertyName
* @param propertyValue
*/
public void updateBeanProperty(String beanName, String propertyName, Object propertyValue) {
Object bean = context.getBean(beanName);
BeanWrapper beanWrapper = beanWrapperMap.get(beanName);
if (null == beanWrapper) {
beanWrapper = new BeanWrapperImpl(bean);
beanWrapperMap.put(beanName, beanWrapper);
}
beanWrapper.setPropertyValue(propertyName, propertyValue);
}
}