配置屬性熱更新:基于 WatchService 實現(xiàn)方案
前言
在現(xiàn)代應用開發(fā)中,配置屬性的動態(tài)更新能力已成為系統(tǒng)靈活性和可維護性的重要指標。傳統(tǒng)的配置方式往往需要重啟應用才能使新配置生效,這在生產(chǎn)環(huán)境中可能造成不必要的服務中斷。
雖然Spring Cloud Config、Apollo這類配置中心能解決問題,但對于中小項目來說太重了——要部署服務,成本太高。
本文將詳細介紹如何結合Java NIO的WatchService、Spring的Environment、ConfigurationProperties 以及ApplicationEvent,構建一套完整的配置屬性熱更新解決方案。
效果圖
圖片
組件解析
WatchService:文件系統(tǒng)監(jiān)聽機制
WatchService是Java NIO提供的文件系統(tǒng)監(jiān)聽服務,能夠實時監(jiān)測指定目錄或文件的變化,包括創(chuàng)建、修改和刪除等操作。它采用了高效的操作系統(tǒng)原生通知機制,相比傳統(tǒng)的輪詢方式具有更低的資源消耗和更高的響應速度。
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
Path path = Paths.get("config");
path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
while (true) {
WatchKey key = watchService.poll(1, TimeUnit.SECONDS);
if (key == null) continue;
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW) continue;
Path changedFile = (Path) event.context();
if (changedFile.endsWith("application.properties")) {
// 處理配置文件變更
handleConfigChange();
}
}
boolean valid = key.reset();
if (!valid) break;
}
} catch (Exception e) {
// 異常處理
}Environment:配置訪問的核心接口
Spring的Environment接口是訪問應用配置的集中點,它整合了各種配置源(如.properties文件、系統(tǒng)環(huán)境變量、命令行參數(shù)等),并提供了統(tǒng)一的訪問方式。通過Environment,我們可以獲取到最新的配置屬性值。
@Autowired
private Environment environment;
public String getConfigValue(String key) {
return environment.getProperty(key);
}ConfigurationProperties:類型安全的配置綁定
Spring Boot的ConfigurationProperties注解提供了將外部配置綁定到JavaBean的能力,實現(xiàn)了類型安全的配置訪問。它支持分層結構的配置,并提供了屬性校驗等功能,是Spring應用中管理配置的推薦方式。
@ConfigurationProperties(prefix = "app")
@Component
public class AppConfig {
private String name;
private int timeout;
private List<String> allowedOrigins;
// getters and setters
}ApplicationEvent:配置變更的通知機制
Spring的事件機制允許我們定義和發(fā)布自定義事件,實現(xiàn)組件間的解耦通信。在配置熱更新場景中,我們可以通過發(fā)布配置變更事件,通知系統(tǒng)中依賴該配置的各個組件及時更新狀態(tài)。
// 自定義配置變更事件
public class ConfigChangedEvent extends ApplicationEvent {
private final String configKey;
private final String oldValue;
private final String newValue;
public ConfigChangedEvent(Object source, String configKey, String oldValue, String newValue) {
super(source);
this.configKey = configKey;
this.oldValue = oldValue;
this.newValue = newValue;
}
// getters
}
// 發(fā)布事件
@Autowired
private ApplicationEventPublisher publisher;
public void publishConfigChange(String key, String oldValue, String newValue) {
publisher.publishEvent(new ConfigChangedEvent(this, key, oldValue, newValue));
}
// 監(jiān)聽事件
@Component
public class ConfigChangeListener {
@EventListener
public void handleConfigChange(ConfigChangedEvent event) {
// 處理配置變更
log.info("Config {} changed from {} to {}",
event.getConfigKey(),
event.getOldValue(),
event.getNewValue());
}
}實現(xiàn)方案
結合上述技術組件,我們可以構建一個完整的配置熱更新解決方案,其核心流程如下:
- 使用
WatchService監(jiān)聽配置文件的變化 - 當配置文件發(fā)生變更時,重新加載配置
- 更新
Environment中的配置值 - 刷新
ConfigurationProperties綁定的Bean - 發(fā)布配置變更事件,通知相關組件
配置文件監(jiān)聽服務
首先實現(xiàn)一個配置文件監(jiān)聽服務,使用WatchService監(jiān)測配置文件的變化:
@Component
public class ConfigFileWatcher implements CommandLineRunner, DisposableBean {
private static final Logger log = LoggerFactory.getLogger(ConfigFileWatcher.class);
private final WatchService watchService;
private final Path configPath;
private final ConfigReloader configReloader;
private volatile boolean running = true;
private Thread watchThread;
public ConfigFileWatcher(ConfigReloader configReloader) throws IOException {
this.watchService = FileSystems.getDefault().newWatchService();
this.configPath = Paths.get("D:\\gitee\\self-learn\\01_springboot\\test-demo\\src\\main\\resources");
this.configReloader = configReloader;
// 確保配置目錄存在
if (!configPath.toFile().exists()) {
boolean created = configPath.toFile().mkdirs();
if (created) {
log.info("Created config directory: {}", configPath);
}
}
// 注冊監(jiān)聽事件
configPath.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
}
@Override
public void run(String... args) {
watchThread = new Thread(this::watch);
watchThread.setDaemon(true);
watchThread.start();
log.info("Config file watcher started, monitoring: {}", configPath.toAbsolutePath());
}
private void watch() {
while (running) {
try {
WatchKey key = watchService.poll(1, TimeUnit.SECONDS);
if (key == null) continue;
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW) continue;
@SuppressWarnings("unchecked")
WatchEvent<Path> pathEvent = (WatchEvent<Path>) event;
Path changedFile = pathEvent.context();
if (isConfigFile(changedFile.toString())) {
log.info("Detected change in config file: {}", changedFile);
File file = configPath.resolve(changedFile).toFile();
configReloader.reloadConfig(file);
}
}
boolean valid = key.reset();
if (!valid) break;
} catch (Exception e) {
log.error("Error watching config files", e);
}
}
}
private boolean isConfigFile(String fileName) {
return fileName.endsWith(".properties") ||
fileName.endsWith(".yml") ||
fileName.endsWith(".yaml");
}
@Override
public void destroy() throws Exception {
running = false;
if (watchThread != null) {
watchThread.join();
}
watchService.close();
log.info("Config file watcher stopped");
}
}配置重新加載器
實現(xiàn)配置重新加載器,負責讀取更新后的配置并更新Environment:
@Component
public class ConfigReloader {
private static final Logger log = LoggerFactory.getLogger(ConfigReloader.class);
private final Environment environment;
private final ApplicationEventPublisher eventPublisher;
private final ConfigurableApplicationContext applicationContext;
private final Map<String, String> currentConfig = new ConcurrentHashMap<>();
public ConfigReloader(Environment environment,
ApplicationEventPublisher eventPublisher,
ConfigurableApplicationContext applicationContext) {
this.environment = environment;
this.eventPublisher = eventPublisher;
this.applicationContext = applicationContext;
}
@PostConstruct
public void init() {
// 初始化當前配置
loadInitialConfig();
}
private void loadInitialConfig() {
// 從Environment加載初始配置
if (environment instanceof ConfigurableEnvironment) {
ConfigurableEnvironment env = (ConfigurableEnvironment) environment;
for (PropertySource<?> propertySource : env.getPropertySources()) {
loadPropertySource(propertySource);
}
}
}
private void loadPropertySource(PropertySource<?> propertySource) {
try {
if (propertySource.getSource() instanceof Map) {
Map<?, ?> sourceMap = (Map<?, ?>) propertySource.getSource();
sourceMap.forEach((k, v) -> {
if (k != null && v != null) {
currentConfig.put(k.toString(), v.toString());
}
});
}
} catch (Exception e) {
log.error("Error loading property source: {}", propertySource.getName(), e);
}
}
public void reloadConfig(File configFile) {
try {
if (!configFile.exists()) {
log.warn("Config file not found: {}", configFile.getAbsolutePath());
return;
}
Properties newProperties = loadProperties(configFile);
if (newProperties == null) {
return;
}
// 比較新舊配置,找出變更項
Map<String, String> changedProperties = new HashMap<>();
newProperties.forEach((k, v) -> {
String key = k.toString();
String value = v.toString();
String oldValue = currentConfig.get(key);
if (!Objects.equals(oldValue, value)) {
changedProperties.put(key, value);
log.info("Config changed: {} = {}", key, value);
}
});
if (!changedProperties.isEmpty()) {
// 更新Environment
updateEnvironment(changedProperties);
// 刷新ConfigurationProperties
refreshConfigurationProperties();
// 發(fā)布配置變更事件
publishConfigChangeEvents(changedProperties);
} else {
log.info("No changes detected in config file: {}", configFile.getName());
}
} catch (Exception e) {
log.error("Error reloading config file: {}", configFile.getAbsolutePath(), e);
}
}
private Properties loadProperties(File configFile) throws IOException {
if (configFile.getName().endsWith(".yml") || configFile.getName().endsWith(".yaml")) {
YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean();
yamlFactory.setResources(new FileSystemResource(configFile));
return yamlFactory.getObject();
} else {
Properties properties = new Properties();
try (FileInputStream fis = new FileInputStream(configFile)) {
properties.load(fis);
}
return properties;
}
}
private void updateEnvironment(Map<String, String> changedProperties) {
if (environment instanceof ConfigurableEnvironment) {
ConfigurableEnvironment env = (ConfigurableEnvironment) environment;
// 添加一個新的PropertySource覆蓋舊值
MapPropertySource newPropertySource = new MapPropertySource(
"dynamic-config-" + System.currentTimeMillis(),
new HashMap<>(changedProperties)
);
env.getPropertySources().addFirst(newPropertySource);
log.info("Updated environment with {} changed properties", changedProperties.size());
}
}
private void refreshConfigurationProperties() {
// 獲取所有@ConfigurationProperties Bean并刷新
Map<String, Object> configBeans = applicationContext.getBeansWithAnnotation(org.springframework.boot.context.properties.ConfigurationProperties.class);
configBeans.values().forEach(bean -> {
ConfigurationPropertiesBindingPostProcessor binder =
applicationContext.getBean(ConfigurationPropertiesBindingPostProcessor.class);
try {
binder.postProcessBeforeInitialization(bean, bean.getClass().getName());
} catch (Exception e) {
log.error("Error refreshing configuration bean: {}", bean.getClass().getSimpleName(), e);
}
});
log.info("Refreshed {} configuration properties beans", configBeans.size());
}
private void publishConfigChangeEvents(Map<String, String> changedProperties) {
changedProperties.forEach((key, value) -> {
String oldValue = currentConfig.get(key);
eventPublisher.publishEvent(new ConfigChangedEvent(this, key, oldValue, value));
});
}
public String getCurrentConfigValue(String key) {
return currentConfig.get(key);
}
}配置變更事件與監(jiān)聽器
完善配置變更事件和監(jiān)聽器,實現(xiàn)配置變更的通知機制:
public class ConfigChangedEvent extends ApplicationEvent {
private final String configKey;
private final String oldValue;
private final String newValue;
public ConfigChangedEvent(Object source, String configKey, String oldValue, String newValue) {
super(source);
this.configKey = configKey;
this.oldValue = oldValue;
this.newValue = newValue;
}
public String getConfigKey() {
return configKey;
}
public String getOldValue() {
return oldValue;
}
public String getNewValue() {
return newValue;
}
}@Component
public class ConfigChangeLogger {
private static final Logger log = LoggerFactory.getLogger(ConfigChangeLogger.class);
@EventListener
public void logConfigChange(ConfigChangedEvent event) {
log.info("Configuration changed - Key: {}, Old Value: {}, New Value: {}",
event.getConfigKey(),
event.getOldValue(),
event.getNewValue());
}
}@Component
public class TimeoutConfigListener {
private static final Logger log = LoggerFactory.getLogger(TimeoutConfigListener.class);
private int currentTimeout;
@EventListener
public void handleTimeoutChange(ConfigChangedEvent event) {
if ("app.timeout".equals(event.getConfigKey())) {
try {
int newTimeout = Integer.parseInt(event.getNewValue());
currentTimeout = newTimeout;
log.info("Updating service timeout to {}ms", newTimeout);
// 這里可以添加實際更新服務超時設置的邏輯
} catch (NumberFormatException e) {
log.error("Invalid timeout value: {}", event.getNewValue());
}
}
}
public int getCurrentTimeout() {
return currentTimeout;
}
}配置屬性 Bean 示例
@Data
@Component
@ConfigurationProperties(prefix = "app")
public class AppConfig {
private static final Logger log = LoggerFactory.getLogger(AppConfig.class);
private String name;
private int timeout = 30;
private List<String> allowedOrigins = new ArrayList<>();
private Map<String, String> features = new HashMap<>();
@PostConstruct
public void init() {
log.info("Initializing AppConfig: {}", this);
}
@PreDestroy
public void destroy() {
log.info("Destroying AppConfig");
}
}



























