前言
在实际项目中,有时候我们希望能够在不重启应用的情况下动态修改Spring Boot的配置,以便更好地应对变化的需求。本文将探讨如何通过从数据库动态加载配置,并提供一键刷新的机制来实现这一目标。
背景
最近的项目中,我遇到了一个需要动态调整应用配置的场景。在研究和实践中,我总结了一套简单而又有效的方法,可以通过数据库中的配置动态刷新Spring Boot应用的配置,而无需重启。
思路
我自己思路很简单,分为以下几个关键步骤:
- 获取应用上下文: 通过
ConfigurableApplicationContext
获取Spring Boot应用的上下文。 - 获取当前环境: 利用
Environment
对象获取当前应用的环境配置。 - 从数据库中获取最新配置: 编写数据库查询逻辑(或者可以有其他的更改途径,这里主要是博主的获取配置的途径),获取最新的配置信息。
- 替换当前环境的配置: 使用
MutablePropertySources
替换当前环境的配置。 - 刷新特定Bean: 调用
ContextRefresher
的refresh
方法刷新指定的Bean。
思考
本来是想着直接看看能不能用 SpringBoot 的机制刷新Bean,真的 SpringBoot 上下文自己封装的 refresh
只能在加载的时候刷新一次,对于第二次的刷新有着严格的要求。本来说要探究一下源码的,算啦吧!SpringCloud 都已经有现成的热部署的工具了ContextRefresher
,它真的为了分布式做了太多了,我哭死,所以我们来看看 SpringCloud 现成的ContextRefresher
是如何实现热部署的吧。
参考环境
- SpringBoot 2.5.8
- SpringCloud 2021.0.1
具体步骤
1、Maven 依赖 和 配置
父项目
<!-- SpringCloud 微服务 -->
<spring-cloud.version>2021.0.1</spring-cloud.version>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
子项目
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
</dependency>
配置源加入,启动加载进Spring上下文。
management.endpoints.web.exposure.include="*"
2、 添加注解 @RefreshScope
给需要更改配置的 Bean 加上 @RefreshScope
注解。
@RefreshScope
public class ConfigService{
@Value("${grpc.client.xyregiserve.port}")
private String regiservePort;
@Value("${grpc.client.xyregiserve.url}")
private String regiserveUrl;
}
3、 获取应用上下文
@Autowired
private ConfigurableApplicationContext applicationContext;
4、获取当前环境
/**
* 获取当前环境
*/
ConfigurableEnvironment environment = applicationContext.getEnvironment();
5、从数据库中获取最新配置
ManagedChannelUtils.runWithManagedChannel(regiserveUrl, regiservePort, channel -> {
try {
PullConfigServiceGrpc.PullConfigServiceBlockingStub pullConfigServiceBlockingStub = PullConfigServiceGrpc.newBlockingStub(channel);
/**
* 从配置中心拉取配置
*/
PullConfigResponse response = pullConfigServiceBlockingStub.getConfigByTag(PullConfigRequest
.newBuilder()
.setStr("user")
.build());
/**
* 调用成功
*/
if (response.getStatus() == 200) {
/**
* 获取到的配置 类型转化
*/
Map<String, Object> newConfig = JSON.parseObject(response.getData(), new TypeReference<Map<String, Object>>() {
});
} else {
throw new Exception("ServerConfig return code error");
}
} catch (Exception e) {
e.printStackTrace();
}
});
6、 替换当前环境的配置
/**
* 替换或添加新的PropertySource
*/
if (propertySources.contains(PROPERTY_SOURCE_NAME)) {
propertySources.replace(PROPERTY_SOURCE_NAME, newPropertySource);
} else {
propertySources.addFirst(newPropertySource);
}
7、异步刷新
/**
* 异步刷新
*/
Executors.newSingleThreadExecutor().execute(() -> contextRefresher.refresh());
8、刷新接口
在配置源更改配置之后,调用这个接口就可以刷新配置了。
@RestController
@RequestMapping("/refresh/config")
public class RefreshConfig {
@Autowired
private ConfigService configService;
/**
* 刷新配置
* @return
* @throws Exception
*/
@GetMapping
public AjaxResult refresh() throws Exception {
configService.refreshConfig();
return AjaxResult.success();
}
}
完整代码
这个是博主的刷新配置的 ConfigService ,仅供参考。
package com.yanxi.user.web.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.yanxi.user.web.common.util.ManagedChannelUtils;
import com.yanxi.user.web.grpc.pullConfigService.PullConfigRequest;
import com.yanxi.user.web.grpc.pullConfigService.PullConfigResponse;
import com.yanxi.user.web.grpc.pullConfigService.PullConfigServiceGrpc;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
@Service
@RefreshScope
public class ConfigService{
@Value("${grpc.client.xyregiserve.port}")
private String regiservePort;
@Value("${grpc.client.xyregiserve.url}")
private String regiserveUrl;
@Autowired
private ConfigurableApplicationContext applicationContext;
@Autowired
private ContextRefresher contextRefresher;
private static final String PROPERTY_SOURCE_NAME = "databaseProperties";
private static final Logger logger = Logger.getLogger(PullConfigLoader.class.getName());
public Map<String, Object> refreshConfig() throws Exception {
ManagedChannelUtils.runWithManagedChannel(regiserveUrl, regiservePort, channel -> {
try {
PullConfigServiceGrpc.PullConfigServiceBlockingStub pullConfigServiceBlockingStub = PullConfigServiceGrpc.newBlockingStub(channel);
/**
* 拉取配置
*/
PullConfigResponse response = pullConfigServiceBlockingStub.getConfigByTag(PullConfigRequest
.newBuilder()
.setStr("user")
.build());
/**
* 调用成功
*/
if (response.getStatus() == 200) {
logger.info("ServerConfig loading Success");
/**
* 类型转化
*/
Map<String, Object> newConfig = JSON.parseObject(response.getData(), new TypeReference<Map<String, Object>>() {
});
/**
* 获取当前环境
*/
ConfigurableEnvironment environment = applicationContext.getEnvironment();
/**
* 创建新的PropertySource
*/
MapPropertySource newPropertySource = new MapPropertySource(PROPERTY_SOURCE_NAME, newConfig);
/**
* 获取PropertySourcess
*/
MutablePropertySources propertySources = environment.getPropertySources();
/**
* 替换或添加新的PropertySource
*/
if (propertySources.contains(PROPERTY_SOURCE_NAME)) {
propertySources.replace(PROPERTY_SOURCE_NAME, newPropertySource);
} else {
propertySources.addFirst(newPropertySource);
}
/**
* 异步刷新
*/
Executors.newSingleThreadExecutor().execute(() -> contextRefresher.refresh());
} else {
throw new Exception("ServerConfig return code error");
}
} catch (Exception e) {
e.printStackTrace();
}
});
return null;
}
}
测试
做了一个测试类。
更改前配置源a的值为333。
接口调用测试类,a的值为也为333。
更改数据源a的值为222。
不用重启项目,调用配置刷新接口。
不用重启项目,调用测试类,a的值为也为222。 好的,更改过来了。
总结
一定要多思考,如果人永远待在舒适圈的话,人永远不会成长。共勉
觉得作者写的不错的,值得你们借鉴的话,就请点一个免费的赞吧!这个对我来说真的很重要。૮(˶ᵔ ᵕ ᵔ˶)ა