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

目录java

  • 回顾昨日
  • nacos 集成
  • Spring Cloud Alibaba 方式
  • Nacos Spring Boot 方式
  • Apollo 集成
  • 自研配置中心对接
  • 无配置中心对接
  • 实现源码分析
  • 兼容 Apollo 和 Nacos NoClassDefFoundError
  • Apollo 自动刷新问题
    回顾昨日
    上篇文章 《一时技痒,撸了个动态线程池,源码放 Github 了》发出后不少读者私下问我这个能不能用到工做中,用确定是能够用的,自己来讲是对线程池的扩展,而后对接了配置中心和监控。

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

还没发布到 Maven 中央仓库(后续会作),能够本身编译打包发布到私有仓库(临时方案)
耦合了 Nacos,若是你项目中没有用 Nacos 或者用的其余的配置中心怎么办?(本文内容)
只能替换业务线程池,像一些框架中的线程池没法替换(构思中)
本文的重点就是介绍如何对接 Nacos 和 Apollo,由于一开始就支持了 Nacos,可是支持的方式是依赖了 Spring Cloud Alibaba ,若是是没有用 Spring Cloud Alibaba 如何支持,也是须要扩展的。程序员

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

Spring Cloud Alibaba 方式
加入依赖:spring

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

而后在 Nacos 中增长线程池的配置,好比:bootstrap

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。api

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,好比下面:微信

<dependency>
  <groupId>com.alibaba.boot</groupId>
  <artifactId>nacos-config-spring-boot-starter</artifactId>
</dependency>

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

@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,依赖以下:

<dependency>
      <groupId>com.ctrip.framework.apollo</groupId>
      <artifactId>apollo-client</artifactId>
      <version>1.4.0</version>
  </dependency>

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

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

而后配置线程池配置的 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)。

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-nacos-config</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>com.ctrip.framework.apollo</groupId>
    <artifactId>apollo-client</artifactId>
    <version>1.4.0</version>
    <optional>true</optional>
</dependency>

而后内部将监听配置动态调整线程池参数的逻辑分开,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,这不就是跟咱们这个同样的逻辑么。

咱们看下源码就知道了,以下图:

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

Apollo 自动刷新问题
在实现的过程当中还遇到一个问题也跟你们分享下,就是 Apollo 中@ConfigurationProperties 配置类,在配置信息变动后不会自动刷新,须要配合 RefreshScope 或者 EnvironmentChangeEvent 来实现。

下图是 Apollo 文档的原话:
Kitty中的动态线程池支持Nacos,Apollo多配置中心了
Kitty中的动态线程池支持Nacos,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<String, String> dataMap = new HashMap<String, String>((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分布式监控
笑话:大厂都在用的任务调度框架我能不知道吗???
为何参与开源项目的程序员找工做时特别抢手?

后台回复 学习资料 领取学习视频

Kitty中的动态线程池支持Nacos,Apollo多配置中心了
若有收获,点个在看,诚挚感谢

尹吉欢我不差钱啊

相关文章
相关标签/搜索