Sermant源码(二)如何无侵入接入配置中心Sermant如何将插件类注入SpringBoot环境,无侵入为宿主应用接
前言
本章基于Sermant2.0.0,分析dynamic-config插件为springboot宿主应用无侵入接入配置中心。过程中涉及Sermant的两个核心通用能力(core):1. Sermant自身如何接入配置中心:Sermant自身有众多插件需要动态配置能力,比如流控、负载均衡,不仅仅是dynamic-config插件为宿主提供动态配置;2. 如何将插件类注入springboot环境:Sermant能够将插件中的类,放入spring.factories,并被springboot加载使用;注:本篇配置中心采用Naocs(2.x)。
一、 使用
1、 客户端
使用Sermant-examples官方案例中的spring-provider模块。
pom
无需引入任何三方依赖,只需要是一个springboot项目。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
</dependencies>
用户代码
- 使用Value/ConfigurationProperties注解注入配置;2. 使用RefreshScope标记需要刷新配置的类;
@Controller
@ResponseBody
@RefreshScope
public class FlowController {
@Value("${server.port}")
int port;
@Value("${sermant}")
private String sermant;
@RequestMapping(value = "/flow", method = RequestMethod.GET)
public String flow(@RequestParam(required = false) Integer exRate, HttpServletRequest request) throws Exception {
return "Hello, I am zk rest template provider, my port is " + port + ", sermant value is " + sermant;
}
}
agent配置
agent/config.properties:开启动态配置,使用nacos配置中心,设置service.meta.project=sermant,这将来是nacos的namespaceId。注:可以通过环境变量或-D参数设置。
agent.service.dynamic.config.enable=true
dynamic.config.serverAddress=127.0.0.1:8848
dynamic.config.dynamicConfigType=NACOS
service.meta.project=sermant
插件配置
agent/plugins.yaml:加入dynamic-config
plugins:
- dynamic-config
agent/pluginPackage/dynamic-config/config/config.yaml:1. enableCseAdapter:关闭,是否适配华为云CSE;2. enableDynamicConfig:开启,是否开启动态配置;3. enableOriginConfigCenter:关闭,是否应用自己接入配置中心(如应用自己引入nacos配置中心spring.cloud.nacos.config.enabled=true);
dynamic.config.plugin:
enableCseAdapter: false
enableDynamicConfig: true
enableOriginConfigCenter: false
挂载agent
java -Dspring.application.name=spring-flow-provider
-javaagent:/path/to/sermant-agent/agent/sermant-agent.jar
-jar spring-provider.jar
2、 服务端(sermant-backend)
application.properties:开启动态配置,使用nacos。
dynamic.config.enable=true
dynamic.config.serverAddress=127.0.0.1:8848
dynamic.config.dynamicConfigType=NACOS
3、 使用控制台修改配置
插件类型:其他;project:sermant,对应agent侧service.meta.project,对应nacos侧namespaceId;group:格式service=服务名,对应应用侧dubbo.application.name或spring.application.name,对应nacos侧group;key:任意值,动态配置插件都能读到,对应nacos侧dataId;配置内容:仅支持yaml格式配置文件;
可以去Nacos看到配置变更。
验证配置生效:
curl localhost:8003/flow
Hello, I am zk rest template provider, my port is 8003, sermant value is 配置值
二、sermant如何接入配置中心
sermant在core模块中定义动态配置服务入,在implement模块中实现,不同的plugin可以选择性使用:1. dynamic-config:宿主配置中心插件,让宿主springboot应用通过Value和ConfigurationProperties注入配置中心配置;2. flowcontrol:流控插件,获取流控规则配置;3. loadbalancer:负载均衡插件,获取负载均衡规则配置;4. tag-transmission:流量标签透传插件,获取标签透传配置;...
1、 DynamicConfigService
DynamicConfigService是core中定义的BaseService SPI接口。DynamicConfigService是一个常规的配置中心抽象类,可以对配置进行增删改查和订阅。DynamicConfigService查询配置:注:如果group为空,fixGroup会将group设置为dynamic.config.defaultGroup,默认为sermant。
@Override
public String getConfig(String key) {
return getConfig(key, null);
}
@Override
public String getConfig(String key, String group) {
return doGetConfig(key, fixGroup(group)).orElse(null);
}
DynamicConfigService订阅配置有两种方式:1. 组订阅:批量订阅一个group下的所有配置
@Override
public boolean addGroupListener(String group, DynamicConfigListener listener) {
if (listener == null) {
LOGGER.warning("Empty listener is not allowed. ");
return false;
}
return doAddGroupListener(fixGroup(group), listener);
}
2. 单key订阅:订阅某个group的某个key的配置重载方法ifNotify=true,会读取初始配置,回调Listener。
// 仅订阅,listener无法拿到初始配置
@Override
public boolean addConfigListener(String key, String group, DynamicConfigListener listener) {
if (!checkKey(key)) {
return false;
}
if (listener == null) {
LOGGER.warning("Empty listener is not allowed. ");
return false;
}
return doAddConfigListener(key, fixGroup(group), listener);
}
// 订阅,且读取初始配置,回调listener
public boolean addConfigListener(String key, String group, DynamicConfigListener listener, boolean ifNotify) {
final boolean addResult = addConfigListener(key, group, listener);
if (addResult && ifNotify) {
listener.process(DynamicConfigEvent.initEvent(key, fixGroup(group), getConfig(key, group)));
}
return addResult;
}
DynamicConfigListener,可以接收DynamicConfigEvent配置变化。DynamicConfigEvent,包含group-组、key-配置项(如dataId)、value-配置内容、eventType-初始化/增/删/改。
2、 NacosDynamicConfigService
DynamicConfigService的实现是BufferedDynamicConfigService。根据dynamic.config.serviceType服务类型委派给实际配置中心服务提供方,这里分析nacos提供配置服务。
启动
NacosDynamicConfigService#start:1. 创建Nacos客户端;2. 开启一个3s定时任务,用于发现group新增key,注册监听;(这个后面再看)NacosBufferedClient构造时会创建nacos客户端,注意nacos的namespace就是ServiceMeta#getProject,即service.meta.project=default。
NacosBufferedClient#createNacosClient:虽然这里看起来与nacos建立连接失败将直接导致agent挂载失败。但是实际上nacos2.x客户端都是惰性连接服务端,如果连接失败会异步重试,不会影响sermant启动。
查询配置
NacosDynamicConfigService#doGetConfig:调用nacos查询配置,这里是实时查询无缓存。
单key订阅
NacosDynamicConfigService#doAddConfigListener:调用nacos api创建Listener并订阅配置。其中key对应dataId,group就是group。订阅成功,将nacos的Listener封装为NacosListener放入listeners。NacosDynamicConfigService#instantiateListener:对于每个dataId,sermant注册Listener采用独立单线程线程池,执行配置变更回调。
组订阅
nacos本身不支持group订阅,只能支持dataId订阅。NacosDynamicConfigService#doAddGroupListener:sermant的做法是1. 先根据group查询所有dataId;(注意,naocs2.x客户端是grpc,这里没有用nacos提供的api,而是直接走http,和nacos控制台的配置列表接口一致)2. 循环dataId向Nacos注册订阅;
仅仅这样还不足以支持group订阅,因为dataId会新增,所以sermant在NacosDynamicConfigService启动阶段开启了一个3s定时任务,用于检测group下是否新增dataId。NacosDynamicConfigService#updateConfigListener:1. http调用nacos,查询每组下所有dataId;2. 循环所有组订阅NacosListener;3. 如果组内新增dataId,增量注册Listener;
3、 AbstractGroupConfigSubscriber
core模块定义了ConfigSubscriber接口和AbstractGroupConfigSubscriber抽象类,用于协助plugin接入配置中心。AbstractGroupConfigSubscriber操作DynamicConfigService,subscribe是个模板方法。
AbstractGroupConfigSubscriber#subscribe:1. isReady:子类实现,判断是否需要启动订阅;2. buildGroupSubscriberes:子类实现,批量返回组订阅Listener;3. 循环2的结果,调用DynamicConfigService执行组订阅,注意isNotify=true,这里注册完成后,会同步回调用户的DynamicConfigListener;
4、组订阅的实现
组订阅有三种实现,主要区别在于group的拼接:1. DefaultGroupConfigSubscriber:只有一个group,service=x,服务维度;2. CommonGroupConfigSubscriber:有4个group,不同group的配置优先级从低到高为1)服务维度:app=x&service=y&environment=z;2)应用维度:app=x&environment=y;3)zone维度:app=x&environment=y&zone=z;4)自定义标签维度:customLabel=customLabelValue;注:这些配置都在config.properties的service.meta配置项下。3. CseGroupConfigSubscriber:华为云CSE相关,相较于CommonGroupConfigSubscriber,没有zone维度;
三、如何为宿主应用接入配置中心
1、springboot如何接入配置中心
springboot常见注入配置的扩展点有两个:1. prepareEnvironment阶段,Environment刚创建,通过实现EnvironmentPostProcessor注入;2. prepareContext阶段,ApplicationContext刚创建,通过实现ApplicationContextInitializer注入;Apollo同时实现了两个扩展点:1. apollo.bootstrap.eagerLoad.enabled=true,走EnvironmentPostProcessor,在日志系统初始化前注入配置;2. 否则,走ApplicationContextInitializer;
org.springframework.context.ApplicationContextInitializer=\
com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer
org.springframework.boot.env.EnvironmentPostProcessor=\
com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer
Nacos没有直接使用这两个扩展点,实现springboot定义的配置加载接口,加载时机由springboot管控:1. 实现ConfigDataLoader,背后基于EnvironmentPostProcessor实现,支持通过spring.config.import注入配置(springboot2.4),2. 实现PropertySourceLocator,背后基于ApplicationContextInitializer实现;Sermant也是利用了这个扩展点,dynamic-plugin通过provided依赖springboot,提供SpringEnvironmentProcessor,加入了自己的PropertySource-DynamicConfigPropertySource。注:Sermant如何将SpringEnvironmentProcessor加入SpringBoot,这个放到最后再说。DynamicConfigPropertySource,实际委派单例ConfigHolder,是springboot通往sermant的桥梁。
2、 配置监听
dynamic-config-plugin需要先注册监听到DynamicConfigService,然后将配置放入ConfigHolder为宿主springboot应用提供配置来源。DynamicProperties通过PluginServiceManager拿到DynamicConfigInitializer注册配置监听。注:DynamicProperties是dynamic-config-plugin提供的一个的spring bean,sermant如何做到将plugin类加入springboot最后再看。
DynamicConfigInitializer利用core提供的DefaultGroupConfigSubscriber进行组订阅(group为service维度,如service=abc),初始配置会回调ConfigListener。
ConfigListener将配置给到ConfigHolder。
3、 配置更新
ConfigHolder实例创建时,通过SPI加载当前PluginClassLoader下的ConfigSource实现。ConfigSource有两个实现,enableCseAdapter=false的情况下,启用DefaultDynamicConfigSource。
io.sermant.dynamic.config.CseDynamicConfigSource
io.sermant.dynamic.config.DefaultDynamicConfigSource
ConfigHolder#resolve:当ConfigHolder收到配置变更事件,调用DefaultDynamicConfigSource处理,处理完成后执行配置刷新通知。DefaultDynamicConfigSource解析配置缓存到configSources中,供后续查询。
4、配置热更新
ConfigHolder作为springboot的PropertySource的最终存储,目前已经能够提供配置能力,如果需要配置热更新,需要依赖RefreshScope。dynamic-config-plugin又往springboot中注入了一个SpringEventPublisher。SpringEventPublisher在setApplicationEventPublisher阶段,向ConfigHolder注册监听。ConfigHolder#addListener:RefreshNotifier管理了ConfigSource更新后需要感知配置变化的Listener。
ConfigHolder#resolve:所以当底层ConfigSource更新后,触发SpringEventPublisher发送RefreshEvent,刷新RefreshScope下的Bean。
配置初始化+热更新流程如下:
5、 读取配置
读取配置的流程就较为简单了。ConfigHolder#getConfig:读取配置,循环ConfigSource找配置。
DefaultDynamicConfigSource#getConfig:
四、 如何将sermant插件类注入springboot
1、 为什么要把插件类注入springboot环境
目前sermant有三个依赖springboot生命周期的bean:1. SpringEnvironmentProcessor:在spring.factories中配置为EnvironmentPostProcessor,在Environment创建完毕后,注册sermant的PropertySource。2. DynamicProperties:在spring.factories中配置为EnableAutoConfiguration,在springboot容器早期阶段接入配置中心,注册配置监听。
3. SpringEventPublisher:用于热更新,在spring.factories中配置为EnableAutoConfiguration,目的是拿到ApplicationEventPublisher,可以发送RefreshEvent事件,触发RefreshScope Bean的重新绑定。
2、 定义ClassInjectDefine
为了将plugin类放入springboot环境,需要定义ClassInjectDefine。其中injectClassName是需要注入的类,factoryName对应spring的SPI接口,如org.springframework.boot.autoconfigure.EnableAutoConfiguration。动态配置插件,需要spring的RefreshEventListener存在才能真实生效。
3、拦截获取spring.factories资源
SpringFactoriesLoader#loadSpringFactories:springboot通过spring.factories加载SPI类,返回map的key是SPI接口名,value是实现类名,如key=org.springframework.boot.autoconfigure.EnableAutoConfiguration,value=n个配置实现类名。动态配置插件通过SpringFactoriesInterceptor拦截SpringFactoriesLoader#loadSpringFactories,将plugin中的ClassInjectDefine声明的class加入出参map中。
ClassInjectDefine通过SPI+当前插件的PluginClassLoader加载,即只包含当前插件下的ClassInjectDefine实现。
core包中提供了ClassInjectService,将ClassInjectDefine加入spring.factories。
4、ClassInjectService的作用
core包中定义的ClassInjectService接口,由implement包实现,主要目的是将插件类放到spring.factories中。InjectServiceImpl#injectConfiguration:1. 用SpringFactoriesInterceptor传入的线程上下文类加载器,加载插件中ClassInjectDefine声明的springboot的SPI实现类,如SpringEnvironmentProcessor;(这一步类加载没什么用,应该是老版本逻辑,实际springboot会在需要用到SPI实现的时候,根据classname,反射创建class并实例化,会触发类加载)2. 递归处理ClassInjectDefine依赖的其他ClassInjectDefine;3. 将springboot的SPI实现类名,加入对应springboot的SPI接口名之下,如EnvironmentPostProcessor,插件类进入spring.factories;
5、 增强LaunchedURLClassLoader
虽然将插件类加入了spring.factories,springboot运行时通过LaunchedURLClassLoader还是无法加载插件类。回顾上一章的类加载器结构,LaunchedURLClassLoader与PluginClassLoader不在一个路径上。ClassLoaderDeclarer:所以sermant在core包中提供了对springboot类加载器的增强。
所以如果启用依赖springboot环境的插件,只能通过java -jar启动,用IDEA启动会报错,因为AppClassLoader没增强,无法加载到插件类。
ClassLoaderLoadClassInterceptor#onThrow:这里是plugin类进入springboot的核心点。当LaunchedURLClassLoader加载class失败后:1. 如果className的前缀在inject.essentialPackage配置(io.sermant)中;2. 使用PluginClassFinder循环所有插件的PluginClassLoader来尝试加载这个class;(这里不包含local和线程类加载器逻辑)3. 如果加载成功,changeResult将LaunchedURLClassLoader#loadClass的返回值替换为sermant加载到的类,changeThrowable清除LaunchedURLClassLoader#loadClass调用异常;所以从表面上看,是LaunchedURLClassLoader加载到了类,但是实际上这个类是由sermant的某个插件的类加载器加载到的。
6、 插件如何加载spring类
SpringEnvironmentProcessor这种类通过ClassInjectDefine加入spring.factories,通过LaunchedURLClassLoader增强能从PluginClassLoader加载。但是SpringEnvironmentProcessor使用了众多spring类,而依照类加载的全盘委托原则,这些spring类也需要通过PluginClassLoader加载。回顾PluginClassLoader#loadClass,兜底有一个local加载逻辑。
PluginClassLoader#getClassFromLocalClassLoader:插件类加载器能用线程上下文类加载器加载class,所以PluginClassLoader能加载到宿主类,即spring的类。
总结
1、Sermant接入配置中心
Sermant核心模块提供了动态配置服务DynamicConfigService,供各plugin插件按需使用,目前仅支持三种配置中心:ZooKeeper、Nacos、Kie。DynamicConfigService支持单key订阅,也支持组订阅。对于Naocs实现来说,key=dataId订阅,组订阅=group订阅。
本身Nacos是不支持group订阅的,Sermant的做法是:1. 调用http端点/nacos/v1/cs/configs查询group下所有dataId;2. 循环dataId注册监听;3. 后台定时检测group是否新增dataId,如果有则增量注册dataId监听;组订阅有三种实现,主要区别在于group的拼接:1. DefaultGroupConfigSubscriber:只有一个group,service=x,服务维度;2. CommonGroupConfigSubscriber:有4个group,不同group的配置优先级从低到高为1)服务维度:app=x&service=y&environment=z;2)应用维度:app=x&environment=y;3)zone维度:app=x&environment=y&zone=z;4)自定义标签维度:customLabel=customLabelValue;注:这些配置都在config.properties的service.meta配置项下。3. CseGroupConfigSubscriber:华为云CSE相关,相较于CommonGroupConfigSubscriber,没有zone维度;
2、 Sermant插件类注入springboot
为SpringBoot宿主应用提供配配置中心能力,需要实现PropertySource注入Environment。Sermant核心模块提供了1个服务和1个增强点用于实现将Sermant插件类注入SpringBoot:1. ClassInjectService:根据插件传入ClassInjectDefine,将插件完全限定类名(injectClassName)和其对应springboot SPI扩展点类名(factoryName),注入spring.factories;2. ClassLoaderLoadClassInterceptor:增强LaunchedURLClassLoader,当loadClass抛出异常后,对sermant类尝试寻找PluginClassLoader加载并返回;
3、 为宿主应用接入配置中心
基于core模块提供的上述两种能力,dynamic-config动态配置插件能轻松为springboot宿主应用无侵入接入配置中心。第一步,dynamic-config提供EnvironmentPostProcessor实现SpringEnvironmentProcessor。SpringEnvironmentProcessor创建PropertySource实现DynamicConfigPropertySource注入Environment。当宿主需要配置时,可以从DynamicConfigPropertySource获取,其底层数据在ConfigHolder中。ConfigHolder中使用ConfigSource缓存配置数据在Map中。
第二步,向配置中心发起监听,进行配置初始化。
1. DynamicProperties,是springboot的EnableAutoConfiguration SPI类,在bean初始化阶段,触发动态配置插件初始化;2. DynamicConfigInitializer,创建监听器ConfigListener,使用core提供的DefaultGroupConfigSubscriber进行组订阅,组为service=服务名,服务名可以取自spring.application.name或dubbo.application.name;3. DefaultGroupConfigSubscriber,组订阅会在首次订阅后,调用配置中心获取实时配置,直接回调监听器ConfigListener;4. ConfigListener,调用ConfigHolder将初始化配置解析后存储到ConfigSource(DefaultDynamicConfigSource);5. RefreshNotifier,用于热更新,监听配置变更并转发,只处理非配置初始化情况;6. SpringEventPublisher,是springboot的EnableAutoConfiguration SPI类,通过ApplicationEventPublisherAware钩子拿到ApplicationEventPublisher。通过RefreshNotifier监听ConfigHolder配置变更,发送RefreshEvent触发RefreshScope的bean重新绑定;
欢迎大家评论或私信讨论问题。
本文原创,未经许可不得转载。
欢迎关注公众号【程序猿阿越】。
转载自:https://juejin.cn/post/7401065821596270628