springboot实现动态加载远程配置文件

需求

有个独立的API项目,该项目主要是对外部各个系统提供API接口,为了保证调用的安全,需要对请求进行校验,主要校验包括调用频率,访问IP,是否跨域和Token,其中IP和是否跨域的配置会根据接入方进行相应的修改,为了避免每次有新的接入方就得去修改一次配置文件并重启项目,所以打算使用动态配置的方式。

初级实现方案:API服务每隔5分钟向管理端请求一次数据,管理端添加IP和域白名单的管理,这个实现方案,简单好用,但是弊端也明显,管理端每次修改完配置后,客户端需要等待下次请求后才会加载对应的配置,同时,还需要自己管理获取到的配置文件

更新方案:在springboot启动时,先从远端获取配置文件,并将其加载进Environment对象中,其余的,就都交给Spring了。同时配合spring-cloud-context实现远程配置变更后,本地重新拉取配置并更新

瞎折腾阶段

以下代码基于springboot 2.3.4.RELEASE版本

我们先从run方法开始搞事情
点进去之后,springboot会在这里初始化ConfigurableEnvironment对象

继续向下,由于我这里是启动的SERVLET环境,所以会初始化一个StandardServletEnvironment对象

这里是给ConfigurableEnvironment做一些初始化工作,我们先不管了,重点在这里,listeners.environmentPrepared(environment);,Springboot通过事件,将Environment的加载分发出去

具体的监听器则是配置在spring.factories里面,跟配置加载相关的监听器如图

我们进入到ConfigFileApplicationListener类中,先判断接收到的事件是不是ApplicationEnvironmentPreparedEvent事件,是的话,调用onApplicationEnvironmentPreparedEvent方法,通过loadPostProcessors方法,从spring.factories中读取配置的EnvironmentPostProcessor,通过Order排序后,依次执行


emmm… 跑了这么远,我们要用到的扩展点也就在这儿了,我们只需要自己扩展一个EnvironmentPostProcessor,并注册进去,就好啦

具体实现

  1. 先定义一个实现类,实现EnvironmentPostProcessor接口
    代码中对地址倒序排序是因为注册PropertySources时使用了addAfter方法,导致先加载的使用顺序会靠后,为了使使用顺序与配置文件的书写顺序一致,才在此处对资源顺序进行了反转
package fun.fanx.remote.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.boot.env.RandomValuePropertySource;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.boot.logging.DeferredLog;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.lang.NonNull;

import java.io.IOException;
import java.util.List;

/**
 * 远程配置文件加载
 * @author fan
 */
public class RemoteConfigLoadPostProcessor implements EnvironmentPostProcessor, ApplicationListener<ApplicationPreparedEvent> {
    /**
     * 用于缓存日志, 并在合适的时候打印
     */
    private static final DeferredLog LOGGER = new DeferredLog();


    /**
     * 先初始化一个yml的解析器
     * 加载properties文件的话自己初始化{@link org.springframework.boot.env.PropertiesPropertySourceLoader}对象即可
     */
    private final PropertySourceLoader loader = new YamlPropertySourceLoader();

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        // 获取一个binder对象, 用来绑定配置文件中的远程地址到对象上
        final Binder binder = Binder.get(environment);
        final String[] configsUrl = binder.bind("xc.config.remote", String[].class).orElse(new String[]{});

        final MutablePropertySources propertySources = environment.getPropertySources();

        final int length = configsUrl.length;
        for (int i = 0; i < length; i++) {
            // 配置文件的name不能一致, 一致的话会被覆盖
            try {
                loadProperties("defaultXcRemoteConfigure" + i, configsUrl[i], propertySources);
            } catch (IOException e) {
                LOGGER.error("load fail, url is: " + configsUrl[i], e);
            }
        }
    }

    private void loadProperties(String name, String url, MutablePropertySources destination) throws IOException {
        Resource resource = new UrlResource(url);
        if (resource.exists()) {
            // 如果资源存在的话,使用PropertySourceLoader加载配置文件
            List<PropertySource<?>> load = loader.load(name, resource);
            // 将对应的资源放在RandomValuePropertySource前面,保证加载的远程资源会优先于系统配置使用
            load.forEach(it -> destination.addBefore(RandomValuePropertySource.RANDOM_PROPERTY_SOURCE_NAME, it));
            LOGGER.info("load configuration success from " + url);
        } else {
            LOGGER.error("get configuration fail from " + url + ", don't load this");
        }
    }

    @Override
    public void onApplicationEvent(@NonNull ApplicationPreparedEvent applicationPreparedEvent) {
        // 打印日志
        LOGGER.replayTo(RemoteConfigLoadPostProcessor.class);
    }
}

  1. 将对应的实现类添加到spring.factories文件中
org.springframework.boot.env.EnvironmentPostProcessor=fun.fanx.remote.config.RemoteConfigLoadPostProcessor
# 注册用于打印日志
org.springframework.context.ApplicationListener=fun.fanx.remote.config.RemoteConfigLoadPostProcessor
  1. 配置文件中指定需要加载的远程资源地址, 多个路径会按照顺序读取使用
xc:
  config:
    remote:
      - file:///Users/fan/remote-config/src/main/resources/test.yml # 本地文件测试
      - http://127.0.0.1:8080/properties/dev1.yml
      - http://127.0.0.1:8080/properties/dev2.yml

到此为止,我们就能像使用本地配置文件一样使用服务器上的配置文件了,但是这里还只实现了加载远程配置文件,我们还需要在远程配置文件变更时,实现配置文件的热更新

配合spring-cloud-context实现配置文件动态刷新

  1. 项目引入依赖
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-context</artifactId>
  <version>2.2.0.RELEASE</version><!-- 不同版本的springboot需要使用不同的版本,具体对应关系,可以在https://start.spring.io/获取 -->
</dependency>
  1. 调用ContextRefresher的refresh方法即可,我这儿是暴露了一个接口,方便管理端统一调用
package fun.fanx.remote.config;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.context.ApplicationListener;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 配置管理接口
 * @author fan
 */
@Slf4j
@RefreshScope
@RestController
@RequiredArgsConstructor
@RequestMapping("/environment/api")
public class RefreshController implements ApplicationListener<ApplicationStartedEvent> {
    private final ContextRefresher contextRefresher;

    @Value("${remote.test}")
    private String test;

    /**
     * 刷新配置接口
     * 限制一分钟只能请求一次
     */
    @GetMapping("refresh")
    public String refresh() {
        contextRefresher.refresh();
        return test;
    }


    @Override
    public void onApplicationEvent(@NonNull ApplicationStartedEvent applicationStartedEvent) {
        System.out.println("加载到的配置:" + test);
    }
}

spring cloud的ContextRefresher刷新配置后,使用@ConfigurationProperties注解的配置类会刷新实例,使用@Value注入的参数值,需要配合@RefreshScope注解使用,才能刷新对应的值,具体的,还请自行查阅具体资料

demo地址

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值