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

总结

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

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

相关推荐
就叫_这个吧35 分钟前
Activiti7工作流入门+案例应用
java·activiti7·工作流
Z_W_H_43 分钟前
minikube start --driver=docker --memory=4000mb出现PROVIDER_DOCKER_NEWGRP
java·docker·minikube
用户67570498850244 分钟前
从失真到绝对精准:Go语言中如何进行高精度计算?
后端·go
折哥的程序人生 · 物流技术专研1 小时前
第3篇:手写促销策略引擎(满减、打折、立减)
java·设计模式·策略模式·开闭原则·编程实战·扩充系列·促销引擎
程序员cxuan1 小时前
GPT-5.6 官宣解禁,明天全量发布!
人工智能·后端·程序员
郑州光合科技余经理1 小时前
海外外卖平台源码改造实战——多语言核心代码实现
java·开发语言·前端·后端·mysql·架构·php
耀耀_很无聊1 小时前
12_Redis Lua 脚本踩坑:为什么 tonumber(ARGV[x]) 会得到 nil?
java·redis·lua
奶油话梅糖2 小时前
锐捷常用命令速查表
java·网络·windows
喜欢打篮球的普通人2 小时前
Trition程序编写:从“Hello CUDA“到“Hello Triton“:向量加法背后的编译黑魔法
开发语言·后端·rust
code_std2 小时前
java WebSocket 使用
java·开发语言·websocket