Kitty中的動態線程池支持Nacos,Apollo多配置中心了


目錄

  • 回顧昨日
  • 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方式

加入依賴:

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

然后在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,比如下面:

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

那么集成的步驟跟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,依賴如下:

  <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

因為配置類是用的@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,這不就是跟我們這個一樣的邏輯么。

我們看下源碼就知道了,如下圖:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM