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多配置中心了
若有收穫,點個在看,誠摯感謝

尹吉歡我不差錢啊

相關文章
相關標籤/搜索