【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。 好的,更改过来了。

总结

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

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

相关推荐
儿时可乖了13 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
ruleslol15 分钟前
java基础概念37:正则表达式2-爬虫
java
xmh-sxh-131431 分钟前
jdk各个版本介绍
java
XINGTECODE1 小时前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
天天扭码1 小时前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶1 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺1 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端
小曲程序1 小时前
vue3 封装request请求
java·前端·typescript·vue
凡人的AI工具箱1 小时前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
陈王卜1 小时前
django+boostrap实现发布博客权限控制
java·前端·django