当前位置:网站首页>Kitty中的动态线程池支持Nacos,Apollo多配置中心了

Kitty中的动态线程池支持Nacos,Apollo多配置中心了

2020-11-06 01:32:25 尹吉欢

目录

  • 回顾昨日
  • nacos集成
    • Spring Cloud Alibaba 方式
    • Nacos Spring Boot 方式
  • Apollo集成
  • 自研配置中心对接
  • 无配置中心对接
  • 实现源码分析
    • 兼容Apollo和Nacos NoClassDefFoundError
    • Apollo自动刷新问题

回顾昨日

上篇文章 《一时技痒,撸了个动态线程池,源码放Github了》(https://mp.weixin.qq.com/s/JM9idgFPZGkRAdCpw0NaKw)发出后很多读者私下问我这个能不能用到工作中,用肯定是可以用的,本身来说是对线程池的扩展,然后对接了配置中心和监控。

目前用的话主要存在下面几个问题:

  • 还没发布到Maven中央仓库(后续会做),可以自己编译打包发布到私有仓库(临时方案)
  • 耦合了Nacos,如果你项目中没有用Nacos或者用的其他的配置中心怎么办?(本文内容)
  • 只能替换业务线程池,像一些框架中的线程池无法替换(构思中)

本文的重点就是介绍如何对接Nacos 和 Apollo,因为一开始就支持了Nacos,但是支持的方式是依赖了Spring Cloud Alibaba ,如果是没有用Spring Cloud Alibaba 如何支持,也是需要扩展的。

Nacos集成

Nacos集成的话分两种方式,一种是你的项目使用了Spring Cloud Alibaba ,另一种是只用了Spring Boot 方式的集成。

Spring Cloud Alibaba方式

加入依赖:

  
  1. <dependency>
  2. <groupId>com.cxytiandi</groupId>
  3. <artifactId>kitty-spring-cloud-starter-dynamic-thread-pool</artifactId>
  4. </dependency>

然后在Nacos中增加线程池的配置,比如:

  
  1. kitty.threadpools.executors[0].threadPoolName=TestThreadPoolExecutor
  2. kitty.threadpools.executors[0].corePoolSize=4
  3. kitty.threadpools.executors[0].maximumPoolSize=4
  4. kitty.threadpools.executors[0].queueCapacity=5
  5. kitty.threadpools.executors[0].queueCapacityThreshold=22

然后在项目中的bootstrap.properties中配置要使用的Nacos data-id。

  
  1. spring.cloud.nacos.config.ext-config[0].data-id=kitty-cloud-thread-pool.properties
  2. spring.cloud.nacos.config.ext-config[0].group=BIZ_GROUP
  3. spring.cloud.nacos.config.ext-config[0].refresh=true

Nacos Spring Boot方式

如果你的项目只是用了Nacos的Spring Boot Starter,比如下面:

  
  1. <dependency>
  2. <groupId>com.alibaba.boot</groupId>
  3. <artifactId>nacos-config-spring-boot-starter</artifactId>
  4. </dependency>

那么集成的步骤跟Spring Cloud Alibaba方式一样,唯一不同的就是配置的加载方式。使用@NacosPropertySource进行加载。

  
  1. @NacosPropertySource(dataId = NacosConstant.HREAD_POOL, groupId = NacosConstant.BIZ_GROUP, autoRefreshed = true, type = ConfigType.PROPERTIES)
  2. public class Application {
  3. public static void main(String[] args) {
  4. SpringApplication.run(Application.class, args);
  5. }
  6. }

然后需要在bootstrap.properties中关闭Spring Cloud Alibaba Nacos Config的自动配置。

  
  1. spring.cloud.nacos.config.enabled=false

Apollo集成

Apollo的使用我们都是用它的client,依赖如下:

  
  1. <dependency>
  2. <groupId>com.ctrip.framework.apollo</groupId>
  3. <artifactId>apollo-client</artifactId>
  4. <version>1.4.0</version>
  5. </dependency>

集成Thread-Pool还是老的步骤,先添加Maven依赖:

  
  1. <dependency>
  2. <groupId>com.cxytiandi</groupId>
  3. <artifactId>kitty-spring-cloud-starter-dynamic-thread-pool</artifactId>
  4. </dependency>

然后配置线程池配置的namespace:

  
  1. apollo.bootstrap.namespaces=thread-pool-config

Properties不用加后缀,如果是yaml文件那么需要加上后缀:

  
  1. apollo.bootstrap.namespaces=thread-pool-config.yaml

如果你项目中用到了多个namespace的话,需要在线程池的namespace中指定,主要是监听配置修改需要用到。

  
  1. 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

因为配置类是用的@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)。

  
  1. <dependency>
  2. <groupId>com.alibaba.cloud</groupId>
  3. <artifactId>spring-cloud-alibaba-nacos-config</artifactId>
  4. <optional>true</optional>
  5. </dependency>
  6. <dependency>
  7. <groupId>com.ctrip.framework.apollo</groupId>
  8. <artifactId>apollo-client</artifactId>
  9. <version>1.4.0</version>
  10. <optional>true</optional>
  11. </dependency>

然后内部将监听配置动态调整线程池参数的逻辑分开,ApolloConfigUpdateListener和NacosConfigUpdateListener。

在自动装配Bean的时候按需装配对应的Listener。

  
  1. @ImportAutoConfiguration(DynamicThreadPoolProperties.class)
  2. @Configuration
  3. public class DynamicThreadPoolAutoConfiguration {
  4. @Bean
  5. @ConditionalOnClass(value = com.alibaba.nacos.api.config.ConfigService.class)
  6. public NacosConfigUpdateListener nacosConfigUpdateListener() {
  7. return new NacosConfigUpdateListener();
  8. }
  9. @Bean
  10. @ConditionalOnClass(value = com.ctrip.framework.apollo.ConfigService.class)
  11. public ApolloConfigUpdateListener apolloConfigUpdateListener() {
  12. return new ApolloConfigUpdateListener();
  13. }
  14. }

兼容Apollo和Nacos NoClassDefFoundError

通过@ConditionalOnClass来判断当前项目中使用的是哪种配置中心,然后装配对应的Listener。上面的代码看上去没问题,在实际使用的过程去报了下面的错误:

  
  1. Caused by: java.lang.NoClassDefFoundError: Lcom/alibaba/nacos/api/config/ConfigService;
  2. at java.lang.Class.getDeclaredFields0(Native Method) ~[na:1.8.0_40]
  3. at java.lang.Class.privateGetDeclaredFields(Class.java:2583) ~[na:1.8.0_40]
  4. at java.lang.Class.getDeclaredFields(Class.java:1916) ~[na:1.8.0_40]
  5. at org.springframework.util.ReflectionUtils.getDeclaredFields(ReflectionUtils.java:755) ~[spring-core-5.1.8.RELEASE.jar:5.1.8.RELEASE]
  6. ... 22 common frames omitted
  7. Caused by: java.lang.ClassNotFoundException: com.alibaba.nacos.api.config.ConfigService
  8. at java.net.URLClassLoader.findClass(URLClassLoader.java:381) ~[na:1.8.0_40]
  9. at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_40]
  10. at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331) ~[na:1.8.0_40]
  11. at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_40]
  12. ... 26 common frames omitted

比如我的项目是用的Apollo,然后我集成了动态线程池,在启动的时候就报上面的错误了,错误原因是找不到Nacos相关的类。

但其实我已经用了@ConditionalOnClass来判断,这个是因为你的DynamicThreadPoolAutoConfiguration类是生效的,Spring会去装载DynamicThreadPoolAutoConfiguration类,DynamicThreadPoolAutoConfiguration中有NacosConfigUpdateListener的实例化操作,而项目中又没有依赖Nacos,所以就报错了。

这种情况我们需要将装配的逻辑拆分的更细,直接用一个单独的类去配置,将@ConditionalOnClass放在类上。

这里我采用了静态内部类的方式,如果项目中没有依赖Nacos,那么NacosConfiguration就不会生效,也就不会去初始化NacosConfigUpdateListener。

  
  1. @Configuration
  2. @ConditionalOnClass(value = com.alibaba.nacos.api.config.ConfigService.class)
  3. protected static class NacosConfiguration {
  4. @Bean
  5. public NacosConfigUpdateListener nacosConfigUpdateListener() {
  6. return new NacosConfigUpdateListener();
  7. }
  8. }
  9. @Configuration
  10. @ConditionalOnClass(value = com.ctrip.framework.apollo.ConfigService.class)
  11. protected static class ApolloConfiguration {
  12. @Bean
  13. public ApolloConfigUpdateListener apolloConfigUpdateListener() {
  14. return new ApolloConfigUpdateListener();
  15. }
  16. }

这个地方我顺便提一个点,就是为什么我们平时要多去看看开源框架的源码。因为像这种适配多个框架的逻辑比较常见,那么一些开源框架中肯定也有类似的逻辑。如果你之前有看过其他的框架是怎么实现的,那么这里你就会直接采取那种方式。

比如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的客户端,重新拉取整个配置文件的内容,然后手动刷新配置类。

  
  1. config.addChangeListener(changeEvent -> {
  2. ConfigFileFormat configFileFormat = ConfigFileFormat.Properties;
  3. String getConfigNamespace = finalApolloNamespace;
  4. if (finalApolloNamespace.contains(ConfigFileFormat.YAML.getValue())) {
  5. configFileFormat = ConfigFileFormat.YAML;
  6. // 去除.yaml后缀,getConfigFile时候会根据类型自动追加
  7. getConfigNamespace = getConfigNamespace.replaceAll("." + ConfigFileFormat.YAML.getValue(), "");
  8. }
  9. ConfigFile configFile = ConfigService.getConfigFile(getConfigNamespace, configFileFormat);
  10. String content = configFile.getContent();
  11. if (finalApolloNamespace.contains(ConfigFileFormat.YAML.getValue())) {
  12. poolProperties.refreshYaml(content);
  13. } else {
  14. poolProperties.refresh(content);
  15. }
  16. dynamicThreadPoolManager.refreshThreadPoolExecutor(false);
  17. log.info("线程池配置有变化,刷新完成");
  18. });

刷新逻辑:

  
  1. public void refresh(String content) {
  2. Properties properties = new Properties();
  3. try {
  4. properties.load(new ByteArrayInputStream(content.getBytes()));
  5. } catch (IOException e) {
  6. log.error("转换Properties异常", e);
  7. }
  8. doRefresh(properties);
  9. }
  10. public void refreshYaml(String content) {
  11. YamlPropertiesFactoryBean bean = new YamlPropertiesFactoryBean();
  12. bean.setResources(new ByteArrayResource(content.getBytes()));
  13. Properties properties = bean.getObject();
  14. doRefresh(properties);
  15. }
  16. private void doRefresh(Properties properties) {
  17. Map<String, String> dataMap = new HashMap<String, String>((Map) properties);
  18. ConfigurationPropertySource sources = new MapConfigurationPropertySource(dataMap);
  19. Binder binder = new Binder(sources);
  20. binder.bind("kitty.threadpools", Bindable.ofInstance(this)).get();
  21. }

目前只支持Properties和Yaml文件配置格式。

感兴趣的Star下呗:https://github.com/yinjihuan/kitty

关于作者*:尹吉欢,简单的技术爱好者,《Spring Cloud微服务-全栈技术与案例解析》, 《Spring Cloud微服务 入门 实战与进阶》作者, 公众号 猿天地 发起人。个人微信 jihuan900 ,欢迎勾搭。

我整理了一份很全的学习资料,感兴趣的可以微信搜索 「猿天地」,回复关键字 「学习资料」获取我整理好了的Spring Cloud,Spring Cloud Alibaba,Sharding-JDBC分库分表,任务调度框架XXL-JOB,MongoDB,爬虫等相关资料。

版权声明
本文为[尹吉欢]所创,转载请带上原文链接,感谢
http://cxytiandi.com/blog/detail/36490