目录
- 回顾昨日
- nacos 集成
- Spring Cloud Alibaba 方式
- Nacos Spring Boot 方式
- Apollo 集成
- 自研配置中心对接
- 无配置中心对接
- 实现源码分析
- 兼容 Apollo 和 Nacos NoClassDefFoundError
- Apollo 自动刷新问题
回顾昨日
上篇文章 《一时技痒,撸了个动态线程池,源码放 Github 了》发出后很多读者私下问我这个能不能用到工作中,用肯定是可以用的,本身来说是对线程池的扩展,然后对接了配置中心和监控。
目前用的话主要存在下面几个问题:
还没发布到 Maven 中央仓库(后续会做),可以自己编译打包发布到私有仓库(临时方案)
耦合了 Nacos,如果你项目中没有用 Nacos 或者用的其他的配置中心怎么办?(本文内容)
只能替换业务线程池,像一些框架中的线程池无法替换(构思中)
本文的重点就是介绍如何对接 Nacos 和 Apollo,因为一开始就支持了 Nacos,但是支持的方式是依赖了 Spring Cloud Alibaba ,如果是没有用 Spring Cloud Alibaba 如何支持,也是需要扩展的。
Nacos 集成
Nacos 集成的话分两种方式,一种是你的项目使用了 Spring Cloud Alibaba ,另一种是只用了 Spring Boot 方式的集成。
Spring Cloud Alibaba 方式
加入依赖:
com.cxytiandi
kitty-spring-cloud-starter-dynamic-thread-pool
然后在 Nacos 中增加线程池的配置,比如:
kitty.threadpools.executors[0].threadPoolName=TestThreadPoolExecutor
kitty.threadpools.executors[0].corePoolSize=4
kitty.threadpools.executors[0].maximumPoolSize=4
kitty.threadpools.executors[0].queueCapacity=5
kitty.threadpools.executors[0].queueCapacityThreshold=22
然后在项目中的 bootstrap.properties 中配置要使用的 Nacos data-id。
spring.cloud.nacos.config.ext-config[0].data-id=kitty-cloud-thread-pool.properties
spring.cloud.nacos.config.ext-config[0].group=BIZ_GROUP
spring.cloud.nacos.config.ext-config[0].refresh=true
Nacos Spring Boot 方式
如果你的项目只是用了 Nacos 的 Spring Boot Starter,比如下面:
com.alibaba.boot
nacos-config-spring-boot-starter
那么集成的步骤跟 Spring Cloud Alibaba 方式一样,唯一不同的就是配置的加载方式。使用@NacosPropertySource 进行加载。
@NacosPropertySource(dataId = NacosConstant.HREAD_POOL, groupId = NacosConstant.BIZ_GROUP, autoRefreshed = true, type = ConfigType.PROPERTIES)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
然后需要在 bootstrap.properties 中关闭 Spring Cloud Alibaba Nacos Config 的自动配置。
spring.cloud.nacos.config.enabled=false
Apollo 集成
Apollo 的使用我们都是用它的 client,依赖如下:
com.ctrip.framework.apollo
apollo-client
1.4.0
集成 Thread-Pool 还是老的步骤,先添加 Maven 依赖:
com.cxytiandi
kitty-spring-cloud-starter-dynamic-thread-pool
然后配置线程池配置的 namespace:
apollo.bootstrap.namespaces=thread-pool-config
Properties 不用加后缀,如果是 yaml 文件那么需要加上后缀:
apollo.bootstrap.namespaces=thread-pool-config.yaml
如果你项目中用到了多个 namespace 的话,需要在线程池的 namespace 中指定,主要是监听配置修改需要用到。
kitty.threadpools.apolloNamespace=thread-pool-config.yaml
自研配置中心对接
如果你们项目使用的是自研的配置中心那该怎么使用动态线程池呢?
最好的方式是跟 Nacos 一样,将配置跟 Spring 进行集成,封装成 PropertySource。
Apollo 中集成 Spring 代码参考:https://github.com/ctripcorp/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/config/PropertySourcesProcessor.java[1]
因为配置类是用的@ConfigurationProperties,这样就相当于无缝集成了。
如果没和 Spring 进行集成,那也是有办法的,可以在项目启动后获取你们的配置,然后修改
DynamicThreadPoolProperties 配置类,再初始化线程池即可,具体步骤跟下面的无配置中心对接一致。DynamicThreadPoolManager 提供了 createThreadPoolExecutor()来创建线程池。
无配置中心对接
如果你的项目中没有使用配置中心怎么办?还是可以照样使用动态线程池的。
直接将线程池的配置信息放在项目的 application 配置文件中即可,但是这样的缺点就是无法动态修改配置信息了。
如果想有动态修改配置的能力,可以稍微扩展下,这边我提供下思路。
编写一个 Rest API,参数就是整个线程池配置的内容,可以是 Properties 文件也可以是 Yaml 文件格式。
这个 API 的逻辑就是注入我们的 DynamicThreadPoolProperties,调用 refresh()刷新 Properties 文件,调用 refreshYaml()刷新 Yaml 文件。
然后注入 DynamicThreadPoolManager,调用 refreshThreadPoolExecutor()刷新线程池参数。
实现源码分析
首先,我们要实现的需求是同时适配 Nacos 和 Apollo 两个主流的配置中心,一般有两种做法。
第一种:将跟 Nacos 和 Apollo 相关的代码独立成一个模块,使用者按需引入。
第二种:还是一个项目,内部做兼容。
我这边采取的是第二种,因为代码量不多,没必要拆分成两个。
需要在 pom 中同时增加两个配置中心的依赖,需要设置成可选(optional=true)。
com.alibaba.cloud
spring-cloud-alibaba-nacos-config
true
com.ctrip.framework.apollo
apollo-client
1.4.0
true
然后内部将监听配置动态调整线程池参数的逻辑分开,ApolloConfigUpdateListener 和 NacosConfigUpdateListener。
在自动装配 Bean 的时候按需装配对应的 Listener。
@ImportAutoConfiguration(DynamicThreadPoolProperties.class)
@Configuration
public class DynamicThreadPoolAutoConfiguration {
@Bean
@ConditionalOnClass(value = com.alibaba.nacos.api.config.ConfigService.class)
public NacosConfigUpdateListener nacosConfigUpdateListener() {
return new NacosConfigUpdateListener();
}
@Bean
@ConditionalOnClass(value = com.ctrip.framework.apollo.ConfigService.class)
public ApolloConfigUpdateListener apolloConfigUpdateListener() {
return new ApolloConfigUpdateListener();
}
}
兼容 Apollo 和 Nacos NoClassDefFoundError
通过@ConditionalOnClass 来判断当前项目中使用的是哪种配置中心,然后装配对应的 Listener。上面的代码看上去没问题,在实际使用的过程去报了下面的错误:
Caused by: java.lang.NoClassDefFoundError: Lcom/alibaba/nacos/api/config/ConfigService;
at java.lang.Class.getDeclaredFields0(Native Method) ~[na:1.8.0_40]
at java.lang.Class.privateGetDeclaredFields(Class.java:2583) ~[na:1.8.0_40]
at java.lang.Class.getDeclaredFields(Class.java:1916) ~[na:1.8.0_40]
at org.springframework.util.ReflectionUtils.getDeclaredFields(ReflectionUtils.java:755) ~[spring-core-5.1.8.RELEASE.jar:5.1.8.RELEASE]
... 22 common frames omitted
Caused by: java.lang.ClassNotFoundException: com.alibaba.nacos.api.config.ConfigService
at java.net.URLClassLoader.findClass(URLClassLoader.java:381) ~[na:1.8.0_40]
at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_40]
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331) ~[na:1.8.0_40]
at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_40]
... 26 common frames omitted
比如我的项目是用的 Apollo,然后我集成了动态线程池,在启动的时候就报上面的错误了,错误原因是找不到 Nacos 相关的类。
但其实我已经用了@ConditionalOnClass 来判断,这个是因为你的 DynamicThreadPoolAutoConfiguration 类是生效的,Spring 会去装载 DynamicThreadPoolAutoConfiguration 类,DynamicThreadPoolAutoConfiguration 中有 NacosConfigUpdateListener 的实例化操作,而项目中又没有依赖 Nacos,所以就报错了。
这种情况我们需要将装配的逻辑拆分的更细,直接用一个单独的类去配置,将@ConditionalOnClass 放在类上。
这里我采用了静态内部类的方式,如果项目中没有依赖 Nacos,那么 NacosConfiguration 就不会生效,也就不会去初始化 NacosConfigUpdateListener。
@Configuration
@ConditionalOnClass(value = com.alibaba.nacos.api.config.ConfigService.class)
protected static class NacosConfiguration {
@Bean
public NacosConfigUpdateListener nacosConfigUpdateListener() {
return new NacosConfigUpdateListener();
}
}
@Configuration
@ConditionalOnClass(value = com.ctrip.framework.apollo.ConfigService.class)
protected static class ApolloConfiguration {
@Bean
public ApolloConfigUpdateListener apolloConfigUpdateListener() {
return new ApolloConfigUpdateListener();
}
}
这个地方我顺便提一个点,就是为什么我们平时要多去看看开源框架的源码。因为像这种适配多个框架的逻辑比较常见,那么一些开源框架中肯定也有类似的逻辑。如果你之前有看过其他的框架是怎么实现的,那么这里你就会直接采取那种方式。
比如 Spring Cloud OpenFeign 中对 Http 的客户端做了多个框架的适配,你可以用 HttpClient 也可以用 Okhttp,这不就是跟我们这个一样的逻辑么。
我们看下源码就知道了,如下图:
Apollo 自动刷新问题
在实现的过程中还遇到一个问题也跟大家分享下,就是 Apollo 中@ConfigurationProperties 配置类,在配置信息变更后不会自动刷新,需要配合 RefreshScope 或者 EnvironmentChangeEvent 来实现。
下图是 Apollo 文档的原话:
图片
Nacos 刷新是没问题的,只不过在收到配置变更的消息时,配置信息还没刷新到 Bean 里面去,所以再刷新的时候单独起了一个线程去做,然后在这个线程中睡眠了 1 秒钟(可通过配置调整)。
如果按照 Apollo 文档中给的方式,肯定是可以实现的。但是不太好,因为需要依赖 Spring Cloud Context。主要是考虑到使用者并不一定会用到 Spring Cloud,我们的基础是 Spring Boot。
万一使用者就是在 Spring Boot 项目中用了 Apollo, 然后又用了我的动态线程池,这怎么搞?
最后我采用了手动刷新的方式,当配置发生变更的时候,我会通过 Apollo 的客户端,重新拉取整个配置文件的内容,然后手动刷新配置类。
config.addChangeListener(changeEvent -> {
ConfigFileFormat configFileFormat = ConfigFileFormat.Properties;
String getConfigNamespace = finalApolloNamespace;
if (finalApolloNamespace.contains(ConfigFileFormat.YAML.getValue())) {
configFileFormat = ConfigFileFormat.YAML;
// 去除.yaml后缀,getConfigFile时候会根据类型自动追加
getConfigNamespace = getConfigNamespace.replaceAll("." + ConfigFileFormat.YAML.getValue(), "");
}
ConfigFile configFile = ConfigService.getConfigFile(getConfigNamespace, configFileFormat);
String content = configFile.getContent();
if (finalApolloNamespace.contains(ConfigFileFormat.YAML.getValue())) {
poolProperties.refreshYaml(content);
} else {
poolProperties.refresh(content);
}
dynamicThreadPoolManager.refreshThreadPoolExecutor(false);
log.info("线程池配置有变化,刷新完成");
});
刷新逻辑:
public void refresh(String content) {
Properties properties = new Properties();
try {
properties.load(new ByteArrayInputStream(content.getBytes()));
} catch (IOException e) {
log.error("转换Properties异常", e);
}
doRefresh(properties);
}
public void refreshYaml(String content) {
YamlPropertiesFactoryBean bean = new YamlPropertiesFactoryBean();
bean.setResources(new ByteArrayResource(content.getBytes()));
Properties properties = bean.getObject();
doRefresh(properties);
}
private void doRefresh(Properties properties) {
Map dataMap = new HashMap((Map) properties);
ConfigurationPropertySource sources = new MapConfigurationPropertySource(dataMap);
Binder binder = new Binder(sources);
binder.bind("kitty.threadpools", Bindable.ofInstance(this)).get();
}
目前只支持 Properties 和 Yaml 文件配置格式。
感兴趣的 Star 下呗:https://github.com/yinjihuan/kitty[2]
关于作者:尹吉欢,简单的技术爱好者,《Spring Cloud 微服务-全栈技术与案例解析》, 《Spring Cloud 微服务 入门 实战与进阶》作者, 公众号 猿天地 发起人。个人微信 jihuan900,欢迎勾搭。
参考资料
[1]
PropertySourcesProcessor.java: https://github.com/ctripcorp/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/config/PropertySourcesProcessor.java
[2]
kitty: https://github.com/yinjihuan/kitty
相关推荐
嘘!异步事件这样用真的好么?
一时技痒,撸了个动态线程池,源码放Github了
熬夜之作:一文带你了解Cat分布式监控
笑话:大厂都在用的任务调度框架我能不知道吗???
为什么参与开源项目的程序员找工作时特别抢手?
后台回复 学习资料 领取学习视频
尹吉欢
我不差钱啊