SpringBoot中配置屬性熱更新的輕量級(jí)實(shí)現(xiàn)方案
項(xiàng)目開(kāi)發(fā)中,每次修改配置(比如調(diào)整接口超時(shí)時(shí)間、限流閾值)都要重啟服務(wù),不僅開(kāi)發(fā)效率低,線上重啟還會(huì)導(dǎo)致短暫不可用。
雖然Spring Cloud Config、Apollo這類配置中心能解決問(wèn)題,但對(duì)于中小項(xiàng)目來(lái)說(shuō)太重了——要部署服務(wù),成本太高。
今天分享一個(gè)輕量級(jí)方案,基于SpringBoot原生能力實(shí)現(xiàn)配置熱更新,不用額外依賴,代碼量不到200行。
一、為什么需要“輕量級(jí)”熱更新?
先說(shuō)說(shuō)傳統(tǒng)配置方案的痛點(diǎn)
痛點(diǎn)1:改配置必須重啟服務(wù)
開(kāi)發(fā)環(huán)境中,改個(gè)日志級(jí)別都要重啟服務(wù),浪費(fèi)時(shí)間;生產(chǎn)環(huán)境更麻煩,重啟會(huì)導(dǎo)致流量中斷,影響用戶體驗(yàn)。
痛點(diǎn)2:重量級(jí)配置中心成本高
Spring Cloud Config、Apollo功能強(qiáng)大,但需要單獨(dú)部署服務(wù)、維護(hù)元數(shù)據(jù),小項(xiàng)目用不上這么復(fù)雜的功能,純屬“殺雞用牛刀”。
痛點(diǎn)3:@Value注解不支持動(dòng)態(tài)刷新
即使通過(guò)@ConfigurationProperties綁定配置,默認(rèn)也不會(huì)自動(dòng)刷新,必須結(jié)合@RefreshScope,但@RefreshScope會(huì)導(dǎo)致Bean重建,可能引發(fā)狀態(tài)丟失。
我們需要什么?
- 無(wú)需額外依賴,基于SpringBoot原生API
- 支持properties/yaml文件熱更新
- 不重啟服務(wù),修改配置后自動(dòng)生效
- 對(duì)業(yè)務(wù)代碼侵入小,改造成本低
二、核心原理:3個(gè)關(guān)鍵技術(shù)點(diǎn)
輕量級(jí)熱更新的實(shí)現(xiàn)依賴SpringBoot的3個(gè)原生能力,不需要引入任何第三方框架
2.1 配置文件監(jiān)聽(tīng):WatchService
Java NIO提供的WatchService可以監(jiān)聽(tīng)文件系統(tǒng)變化,當(dāng)配置文件(如application.yml)被修改時(shí),能觸發(fā)回調(diào)事件。
2.2 屬性刷新:Environment與ConfigurationProperties
Spring的Environment對(duì)象存儲(chǔ)了所有配置屬性,通過(guò)反射更新其內(nèi)部的PropertySources,可以實(shí)現(xiàn)配置值的動(dòng)態(tài)替換。
同時(shí),@ConfigurationProperties綁定的Bean需要重新綁定屬性,這一步可以通過(guò)ConfigurationPropertiesBindingPostProcessor實(shí)現(xiàn)。
2.3 事件通知:ApplicationEvent
自定義一個(gè)ConfigRefreshEvent事件,當(dāng)配置更新后發(fā)布事件,業(yè)務(wù)代碼可以通過(guò)@EventListener接收通知,處理特殊邏輯(如重新初始化連接池)。
三、手把手實(shí)現(xiàn):不到200行代碼
3.1 第一步:監(jiān)聽(tīng)配置文件變化
創(chuàng)建ConfigFileWatcher類,使用WatchService監(jiān)聽(tīng)application.yml或application.properties的修改
package com.example.config;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.util.ResourceUtils;
import java.io.IOException;
import java.nio.file.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
public class ConfigFileWatcher {
// 監(jiān)聽(tīng)的配置文件路徑(默認(rèn)監(jiān)聽(tīng)classpath下的application.yaml)
private final String configPath = "classpath:application.yaml";
private WatchService watchService;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final ConfigRefreshHandler refreshHandler;
private long lastProcessTime;
private final long EVENT_DEBOUNCE_TIME = 500; // 500毫秒防抖時(shí)間
// 注入配置刷新處理器(后面實(shí)現(xiàn))
public ConfigFileWatcher(ConfigRefreshHandler refreshHandler) {
this.refreshHandler = refreshHandler;
}
@PostConstruct
public void init() throws IOException {
// 獲取配置文件的實(shí)際路徑
Resource resource = new FileSystemResource(ResourceUtils.getFile(configPath));
Path configDir = resource.getFile().toPath().getParent(); // 監(jiān)聽(tīng)配置文件所在目錄
String fileName = resource.getFilename(); // 配置文件名(如application.yaml)
watchService = FileSystems.getDefault().newWatchService();
// 注冊(cè)文件修改事件(ENTRY_MODIFY)
configDir.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
// 啟動(dòng)線程監(jiān)聽(tīng)文件變化
executor.submit(() -> {
while (true) {
try {
WatchKey key = watchService.take(); // 阻塞等待事件
// 防抖檢查:忽略短時(shí)間內(nèi)重復(fù)事件
if (System.currentTimeMillis() - lastProcessTime < EVENT_DEBOUNCE_TIME) {
continue;
}
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW) {
continue; // 事件溢出,忽略
}
// 檢查是否是目標(biāo)配置文件被修改
Path changedFile = (Path) event.context();
if (changedFile.getFileName().toString().equals(fileName)) {
log.info("檢測(cè)到配置文件修改:{}", fileName);
refreshHandler.refresh(); // 觸發(fā)配置刷新
}
}
boolean valid = key.reset(); // 重置監(jiān)聽(tīng)器
if (!valid) break; // 監(jiān)聽(tīng)器失效,退出循環(huán)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
log.info("配置文件監(jiān)聽(tīng)器啟動(dòng)成功,監(jiān)聽(tīng)路徑:{}", configDir);
}
@PreDestroy
public void destroy() {
executor.shutdownNow();
try {
watchService.close();
} catch (IOException e) {
log.error("關(guān)閉WatchService失敗", e);
}
}
}
3.2 第二步:實(shí)現(xiàn)配置刷新邏輯
創(chuàng)建ConfigRefreshHandler類,核心功能是更新Environment中的屬性,并通知@ConfigurationProperties Bean刷新
import org.springframework.context.ApplicationEvent;
import java.util.Set;
/**
* 自定義配置刷新事件
*/
public class ConfigRefreshedEvent extends ApplicationEvent {
// 存儲(chǔ)變化的配置鍵(可選,方便業(yè)務(wù)判斷哪些配置變了)
private final Set<String> changedKeys;
public ConfigRefreshedEvent(Object source, Set<String> changedKeys) {
super(source);
this.changedKeys = changedKeys;
}
// 獲取變化的配置鍵
public Set<String> getChangedKeys() {
return changedKeys;
}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.*;
@Component
@Slf4j
public class ConfigRefreshHandler implements ApplicationContextAware {
@Autowired
private ConfigurableEnvironment environment;
private ApplicationContext applicationContext;
@Autowired
private ConfigurationPropertiesBindingPostProcessor bindingPostProcessor; // 屬性綁定工具
// 刷新配置的核心方法
public void refresh() {
try {
// 1. 重新讀取配置文件內(nèi)容
Properties properties = loadConfigFile();
// 2. 更新Environment中的屬性
Set<String> changeKeys = updateEnvironment(properties);
// 3. 重新綁定所有@ConfigurationProperties Bean
if (!changeKeys.isEmpty()) {
rebindConfigurationProperties();
}
applicationContext.publishEvent( new ConfigRefreshedEvent(this,changeKeys));
log.info("配置文件刷新完成");
} catch (Exception e) {
log.error("配置文件刷新失敗", e);
}
}
// 讀取配置文件內(nèi)容(支持properties和yaml)
private Properties loadConfigFile() throws IOException {
// 使用Spring工具類讀取classpath下的配置文件
Resource resource = new ClassPathResource("application.yaml");
YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean();
yamlFactory.setResources(resource);
// 獲取解析后的Properties對(duì)象
Properties properties = yamlFactory.getObject();
if (properties == null) {
throw new IOException("Failed to load configuration file");
}
return properties;
}
// 更新Environment中的屬性,返回變化的配置鍵集合
private Set<String> updateEnvironment(Properties properties) {
String sourceName = "Config resource 'class path resource [application.yaml]' via location 'optional:classpath:/'";
Set<String> changedKeys = new HashSet<>();
PropertySource<?> appConfig = environment.getPropertySources().get(sourceName);
if (appConfig instanceof MapPropertySource) {
Map<String, Object> sourceMap = new HashMap<>(((MapPropertySource) appConfig).getSource());
properties.forEach((k, v) -> {
String key = k.toString();
Object oldValue = sourceMap.get(key);
if (!Objects.equals(oldValue, v)) {
changedKeys.add(key);
}
sourceMap.put(key, v);
});
environment.getPropertySources().replace(sourceName, new MapPropertySource(sourceName, sourceMap));
}
return changedKeys;
}
// 重新綁定所有@ConfigurationProperties Bean
private void rebindConfigurationProperties() {
// 獲取所有@ConfigurationProperties Bean的名稱
String[] beanNames = applicationContext.getBeanNamesForAnnotation(org.springframework.boot.context.properties.ConfigurationProperties.class);
for (String beanName : beanNames) {
// 重新綁定屬性(關(guān)鍵:不重建Bean,只更新屬性值)
bindingPostProcessor.postProcessBeforeInitialization(
applicationContext.getBean(beanName), beanName);
log.info("刷新配置Bean:{}", beanName);
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
3.3 第三步:注冊(cè)監(jiān)聽(tīng)器Bean
在SpringBoot配置類中注冊(cè)ConfigFileWatcher,使其隨應(yīng)用啟動(dòng)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HotRefreshConfig {
@Bean
public ConfigFileWatcher configFileWatcher(ConfigRefreshHandler refreshHandler) throws IOException {
return new ConfigFileWatcher(refreshHandler);
}
}
3.4 第四步:使用@ConfigurationProperties綁定屬性
創(chuàng)建業(yè)務(wù)配置類,用@ConfigurationProperties綁定配置,無(wú)需額外注解即可支持熱更新
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "app") // 綁定配置前綴
public class AppConfig {
private int timeout = 3000; // 默認(rèn)超時(shí)時(shí)間3秒
private int maxRetries = 2; // 默認(rèn)重試次數(shù)2次
}
3.5 第五步:測(cè)試熱更新效果
創(chuàng)建測(cè)試Controller,驗(yàn)證配置修改后是否自動(dòng)生效
package com.example.controller;
import com.example.AppConfig;
import com.example.config.ConfigRefreshedEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class ConfigController {
@Autowired
private AppConfig appConfig;
@GetMapping("/config")
public AppConfig getConfig() {
return appConfig; // 返回當(dāng)前配置
}
// 監(jiān)聽(tīng)配置刷新事件,可進(jìn)行業(yè)務(wù)特殊處理
@EventListener(ConfigRefreshedEvent.class)
public void appConfigUpdate(ConfigRefreshedEvent event) {
event.getChangedKeys().forEach(key -> log.info("配置項(xiàng) {} 發(fā)生變化", key));
}
}
四、生產(chǎn)環(huán)境使用
問(wèn)題1:使用外部配置文件
解決方案:配置文件外置通過(guò)環(huán)境變量或啟動(dòng)參數(shù)指定外部路徑,結(jié)合ConfigFileWatcher監(jiān)聽(tīng)外部配置文件
// 修改ConfigFileWatcher的init方法
@PostConstruct
public void init() throws IOException {
// 生產(chǎn)環(huán)境建議監(jiān)聽(tīng)外部配置文件(如/opt/app/application.yml)
Path configPath = Paths.get("/opt/app/application.yml");
if (Files.exists(configPath)) {
watchConfigFile(configPath); // 監(jiān)聽(tīng)外部文件
} else {
log.warn("外部配置文件不存在,使用默認(rèn)配置");
}
}
private void watchConfigFile(Path configPath) throws IOException {
Path configDir = configPath.getParent();
String fileName = configPath.getFileName().toString();
// 后續(xù)邏輯同上...
}
問(wèn)題2:敏感配置解密
解決方案:結(jié)合Jasypt實(shí)現(xiàn)配置在loadConfigFile中解密
// 偽代碼:解密配置
private String decrypt(String value) {
if (value.startsWith("ENC(")) {
return jasyptEncryptor.decrypt(value.substring(4, value.length() - 1));
}
return value;
}
五、總結(jié)
輕量級(jí)配置熱更新方案的核心是“利用SpringBoot原生能力+最小化改造”,適合中小項(xiàng)目或需要快速集成的場(chǎng)景。相比重量級(jí)配置中心,它的優(yōu)勢(shì)在于:
零依賴:無(wú)需部署額外服務(wù),代碼量少
低成本:對(duì)現(xiàn)有項(xiàng)目侵入小,改造成本低
易維護(hù):基于Spring原生API,無(wú)需學(xué)習(xí)新框架
到此這篇關(guān)于SpringBoot中配置屬性熱更新的輕量級(jí)實(shí)現(xiàn)方案的文章就介紹到這了,更多相關(guān)SpringBoot配置屬性熱更新內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Boot2+JPA之悲觀鎖和樂(lè)觀鎖實(shí)戰(zhàn)教程
這篇文章主要介紹了Spring Boot2+JPA之悲觀鎖和樂(lè)觀鎖實(shí)戰(zhàn)教程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10
解析Spring中@Controller@Service等線程安全問(wèn)題
這篇文章主要為大家介紹解析了Spring中@Controller@Service等線程的安全問(wèn)題,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03
基于springboot bean的實(shí)例化過(guò)程和屬性注入過(guò)程
這篇文章主要介紹了基于springboot bean的實(shí)例化過(guò)程和屬性注入過(guò)程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
Spring?Boot實(shí)現(xiàn)多數(shù)據(jù)源連接和切換的解決方案
文章介紹了在SpringBoot中實(shí)現(xiàn)多數(shù)據(jù)源連接和切換的幾種方案,并詳細(xì)描述了一個(gè)使用AbstractRoutingDataSource的實(shí)現(xiàn)步驟,感興趣的朋友一起看看吧2025-01-01
通過(guò)實(shí)例講解springboot整合WebSocket
這篇文章主要介紹了通過(guò)實(shí)例講解springboot整合WebSocket,WebSocket為游覽器和服務(wù)器提供了雙工異步通信的功能,即游覽器可以向服務(wù)器發(fā)送消息,服務(wù)器也可以向游覽器發(fā)送消息。,需要的朋友可以參考下2019-06-06
基于Java和XxlCrawler獲取各城市月度天氣情況實(shí)踐分享
本文主要講解使用Java開(kāi)發(fā)語(yǔ)言,使用XxlCrawler框架進(jìn)行智能的某城市月度天氣抓取實(shí)踐開(kāi)發(fā),文章首先介紹目標(biāo)網(wǎng)站的相關(guān)頁(yè)面及目標(biāo)數(shù)據(jù)的元素,然后講解在信息獲取過(guò)程的一些參數(shù)配置以及問(wèn)題應(yīng)對(duì),需要的朋友可以參考下2024-05-05
spring-boot整合Micrometer+Prometheus的詳細(xì)過(guò)程
這篇文章主要介紹了springboot整合Micrometer+Prometheus的詳細(xì)過(guò)程,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-05-05
Springboot項(xiàng)目Maven依賴沖突的問(wèn)題解決
使用Spring Boot和Maven進(jìn)行項(xiàng)目開(kāi)發(fā)時(shí),依賴沖突是一個(gè)常見(jiàn)的問(wèn)題,本文就來(lái)介紹一下Springboot項(xiàng)目Maven依賴沖突的問(wèn)題解決,具有一定的參考價(jià)值,感興趣的可以了解一下2024-07-07
httpclient staleConnectionCheckEnabled獲取連接流程解析
這篇文章主要為大家介紹了httpclient staleConnectionCheckEnabled獲取連接流程示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11

