【SpringBoot】 热部署 ContextRefresher.refresh() 自定义配置一键刷新 ~

前言

在实际项目中,有时候我们希望能够在不重启应用的情况下动态修改Spring Boot的配置,以便更好地应对变化的需求。本文将探讨如何通过从数据库动态加载配置,并提供一键刷新的机制来实现这一目标。

背景

最近的项目中,我遇到了一个需要动态调整应用配置的场景。在研究和实践中,我总结了一套简单而又有效的方法,可以通过数据库中的配置动态刷新Spring Boot应用的配置,而无需重启。

思路

我自己思路很简单,分为以下几个关键步骤:

  1. 获取应用上下文: 通过ConfigurableApplicationContext获取Spring Boot应用的上下文。
  2. 获取当前环境: 利用Environment对象获取当前应用的环境配置。
  3. 从数据库中获取最新配置: 编写数据库查询逻辑(或者可以有其他的更改途径,这里主要是博主的获取配置的途径),获取最新的配置信息。
  4. 替换当前环境的配置: 使用MutablePropertySources替换当前环境的配置。
  5. 刷新特定Bean: 调用ContextRefresherrefresh方法刷新指定的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上下文。

复制代码
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、获取当前环境

复制代码
/**
 * 获取当前环境
 */
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。 好的,更改过来了。

总结

一定要多思考,如果人永远待在舒适圈的话,人永远不会成长。共勉

觉得作者写的不错的,值得你们借鉴的话,就请点一个免费的赞吧!这个对我来说真的很重要。૮(˶ᵔ ᵕ ᵔ˶)ა

相关推荐
VX:Fegn08957 小时前
计算机毕业设计|基于ssm + vue超市管理系统(源码+数据库+文档)
前端·数据库·vue.js·spring boot·后端·课程设计
徐徐同学7 小时前
cpolar为IT-Tools 解锁公网访问,远程开发再也不卡壳
java·开发语言·分布式
Mr.朱鹏8 小时前
Nginx路由转发案例实战
java·运维·spring boot·nginx·spring·intellij-idea·jetty
VX:Fegn08959 小时前
计算机毕业设计|基于springboot + vue酒店管理系统(源码+数据库+文档)
vue.js·spring boot·课程设计
白露与泡影10 小时前
2026版Java架构师面试题及答案整理汇总
java·开发语言
历程里程碑10 小时前
滑动窗口---- 无重复字符的最长子串
java·数据结构·c++·python·算法·leetcode·django
qq_2290580110 小时前
docker中检测进程的内存使用量
java·docker·容器
我真的是大笨蛋10 小时前
InnoDB行级锁解析
java·数据库·sql·mysql·性能优化·数据库开发
钦拆大仁11 小时前
Java设计模式-单例模式
java·单例模式·设计模式
小手cool11 小时前
在保持数组中对应元素(包括负数和正数)各自组内顺序不变的情况下,交换数组中对应的负数和正数元素
java