在Spring框架中实现属性配置动态刷新,不需要重启应用。
创始人
2025-05-30 13:43:00
0

如何在不重启应用的前提下,在内存中直接修改配置文件中的属性值?

自定义属性源

相当于增加一份配置文件,它可以来源于文件或网络,这取决于你如何拿到数据。下面是一个示例,这个属性源有一个名字:myPropertySource,里面只包含了一个属性:abc_123。

/*** 自定义属性源** @author tianmingxing  on 2023/03/12*/
public class MyPropertySource extends MapPropertySource {public MyPropertySource() {super("myPropertySource", new HashMap<>(1));}@Overridepublic Object getProperty(String name) {if ("abc_123".equals(name)) {return new Random().nextInt()+"";}return super.getProperty(name);}@Overridepublic 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 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 response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());data = OBJECT_MAPPER.readValue(response.body(), new TypeReference>() {});} catch (Exception e) {LOG.error("无法初始化自定义属性源");}}@Overridepublic Object getProperty(String name) {Object o = data.get(name);if (null != o) {return o;}return super.getProperty(name);}@Overridepublic 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();@Overridepublic 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的属性

按照前面的方法,一但Bean初始化完成,通过@Value获取的属性值将不会变化,即使你修改了数据源中的值。

属性如何动态获取

我们虽然定义了数据源,但它只是在Spring容器初始化时,进行了初始化。如果远程接口中的值发生了变化,应用中如何感知到呢?其实有两种方式来实现:一是定时请求远程接口获取最新数据,二是由远程服务主动将接口推送给应用。咱们这里介绍一下第一种方式:

  1. 在启用类中增加@EnableScheduling注解
  2. 增加一个类来定时刷新
/*** 定时更新属性值** @author tianmingxing  on 2023/03/16*/
@Component
public class MyPropertySourceUpdater {@Autowiredprivate MyEnvironmentPostProcessor myEnvironment;/*** 示例代码,具体过程没有演示,大家可以自己去扩展*/@Scheduled(fixedRate = 5_000) // 每5秒钟执行一次public void update() {// 1. 从远程接口获取数据,结果中可增加一个数据是否有修改的标识,如果没有则需要自己对比,或者简单点直接全部覆盖。// 2. 更新属性,由于是引用传递,直接改数据对象即可。myEnvironment.updateProperty(name, value);}
}

找出使用了@Value的Bean

如果使用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> annotatedBeans = new HashMap<>();/*** 提取属性名称的正则*/private final Pattern placeholder = Pattern.compile("\\$\\{([\\w._-]*)?:?.*}");@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {return bean;}@Overridepublic 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 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> getAnnotatedBeans() {return annotatedBeans;}
}

现在只需要监听哪个属性发生变化,然后通过getAnnotatedBeans找到对应的Bean,就可以来更新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 beanWrapperMap = new HashMap<>();@Autowiredprivate 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);}}

相关内容

热门资讯

深度学习笔记--修改替换Pyt... 目录 1--前言 2--问题描述 2--代码 3--测试 1--前言         最近复现一篇...
软考高级常见英语词汇 第1章英语词汇及历年真题1、备考常见计算机技术词汇BI--- Business Intelligen...
警惕车圈“恒大”式危机言论,中... 5月30日下午,比亚迪集团品牌及公关处总经理李云飞就网传“汽车圈恒大”一事在其社交媒体进行长文回应。...
每日一题 排序子序列 🎉🎉🎉点进来你就是我的人了 博主主页:...
什么是装箱?什么是拆箱?装箱和... 参考答案 1、什么是装箱?什么是拆箱? 装箱:基本类型转变...
DPDK — L3 Forwa... 目录 文章目录目录L3 Forwarding Application安装部署部署拓扑编译运行 L3f...
今天,乐高乐园内测:大牌主题乐... 今天,#上海乐高乐园度假区 正式启动内测及试运营。整体测试工作将分为内部测试和试运营两个阶段先后开展...
python——内存管理和垃圾... python内存管理和垃圾回收机制 这个知识点是面试时大多都会问到的,所以在此做整理以...
使用ebpf 监控golang... 一、背景 使用ebpf 监控grpc-go的应用,grpc-go http2 clie...
android studio学... 文章目录一、安装studio二、创建一个项目三、框架结构3.1 drawable目录3.2 res目...
50万起拍一半岛的经营权,拍卖... 红星资本局5月31日消息,一座面积1044.97亩的半岛的经营权以50万元价格开始拍卖,这一拍卖在网...
特朗普癫了?突然反咬中美贸易协... 知道他会反口,没想到这么快反口。不然为什么叫“疯王”呢?从昨晚到现在,特朗普又搞出三件大事:反咬中国...
[ 2204听力 ] 理论课 ... 逻辑关系 转折 、因果 、提问(?) 、强调(!) 、否定(-) 、对比 、举例(ex) 、 [ 理...
BFC块级格式化上下文 一、概念 BFC - Block Formatting Context 块级格式化上下文 BFC的定...
实验2---mybatis映射... mybatis映射文件 1.目的 通过该实验,掌握mybatis映射文件中结果映射re...
马斯克黯然下课,临走前给特朗普... 黯然下课,马斯克要走了。5月底,马斯克正式宣布,即将卸任“政府效率部”部长职务,为其在特朗普政府的任...
100天精通Python(可视... 文章目录0. 专栏导读1. 普通折线图2. 网格折线图3. 趋势折线图4. 对比折线图5. 百分比折...
济南大学202204(汉诺塔递... #include #include #include void hanoi(int n, char ...
从数据中获得成功!学会如何使用... 在当今数字化的世界中,社交媒体已成为企业推广产品和服务的主要渠道之一。然而࿰...
考研复试——离散数学 三年疫情都没有笔试,今年恢复,大概率会有笔试。 2023年3月19号&#...