目錄

  • 回顧昨日

  • 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 方式

加入依賴:

<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 [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,這不就是跟我們這個一樣的邏輯麼。

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

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

相關推薦

後臺回覆 學習資料   領取學習視頻

如有收穫,點個在看,誠摯感謝

相關文章