前言
在实际项目中,有时候我们希望能够在不重启应用的情况下动态修改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 依赖 和 配置
父项目
            
            
              xml
              
              
            
          
          <!-- 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>
        子项目
            
            
              xml
              
              
            
          
          <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-context</artifactId>
</dependency>
        配置源加入,启动加载进Spring上下文。
            
            
              ini
              
              
            
          
          management.endpoints.web.exposure.include="*"
        2、 添加注解 @RefreshScope
给需要更改配置的 Bean 加上 @RefreshScope注解。
            
            
              java
              
              
            
          
          @RefreshScope
public class ConfigService{
    @Value("${grpc.client.xyregiserve.port}")
    private String regiservePort;
    @Value("${grpc.client.xyregiserve.url}")
    private String regiserveUrl;
}
        3、 获取应用上下文
            
            
              java
              
              
            
          
          @Autowired
private ConfigurableApplicationContext applicationContext;
        4、获取当前环境
            
            
              ini
              
              
            
          
          /**
 * 获取当前环境
 */
ConfigurableEnvironment environment = applicationContext.getEnvironment();
        5、从数据库中获取最新配置
            
            
              java
              
              
            
          
          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、 替换当前环境的配置
            
            
              java
              
              
            
          
          /**
 * 替换或添加新的PropertySource
 */
if (propertySources.contains(PROPERTY_SOURCE_NAME)) {
    propertySources.replace(PROPERTY_SOURCE_NAME, newPropertySource);
} else {
    propertySources.addFirst(newPropertySource);
}
        7、异步刷新
            
            
              java
              
              
            
          
          /**
 * 异步刷新
 */
Executors.newSingleThreadExecutor().execute(() -> contextRefresher.refresh());
        8、刷新接口
在配置源更改配置之后,调用这个接口就可以刷新配置了。
            
            
              java
              
              
            
          
          @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 ,仅供参考。
            
            
              java
              
              
            
          
          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。 好的,更改过来了。

总结
一定要多思考,如果人永远待在舒适圈的话,人永远不会成长。共勉
觉得作者写的不错的,值得你们借鉴的话,就请点一个免费的赞吧!这个对我来说真的很重要。૮(˶ᵔ ᵕ ᵔ˶)ა