需求
有个独立的API项目,该项目主要是对外部各个系统提供API接口,为了保证调用的安全,需要对请求进行校验,主要校验包括调用频率,访问IP,是否跨域和Token,其中IP和是否跨域的配置会根据接入方进行相应的修改,为了避免每次有新的接入方就得去修改一次配置文件并重启项目,所以打算使用动态配置的方式。
初级实现方案:API服务每隔5分钟向管理端请求一次数据,管理端添加IP和域白名单的管理,这个实现方案,简单好用,但是弊端也明显,管理端每次修改完配置后,客户端需要等待下次请求后才会加载对应的配置,同时,还需要自己管理获取到的配置文件
更新方案:在springboot启动时,先从远端获取配置文件,并将其加载进Environment对象中,其余的,就都交给Spring了。同时配合spring-cloud-context实现远程配置变更后,本地重新拉取配置并更新
瞎折腾阶段
我们先从run方法开始搞事情以下代码基于springboot 2.3.4.RELEASE版本

点进去之后,springboot会在这里初始化ConfigurableEnvironment对象

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

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

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

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


emmm...... 跑了这么远,我们要用到的扩展点也就在这儿了,我们只需要自己扩展一个EnvironmentPostProcessor,并注册进去,就好啦
具体实现
- 先定义一个实现类,实现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);
}
}
- 将对应的实现类添加到spring.factories文件中
org.springframework.boot.env.EnvironmentPostProcessor=fun.fanx.remote.config.RemoteConfigLoadPostProcessor
# 注册用于打印日志
org.springframework.context.ApplicationListener=fun.fanx.remote.config.RemoteConfigLoadPostProcessor
- 配置文件中指定需要加载的远程资源地址, 多个路径会按照顺序读取使用
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实现配置文件动态刷新
- 项目引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
<version>2.2.0.RELEASE</version><!-- 不同版本的springboot需要使用不同的版本,具体对应关系,可以在https://start.spring.io/获取 -->
</dependency>
- 调用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注解使用,才能刷新对应的值,具体的,还请自行查阅具体资料
网友评论