上篇文章 《一時技癢,擼了個動態線程池,源碼放 Github 了》(mp.weixin.qq.com/s/JM9idgFPZ…)發出後不少讀者私下問我這個能不能用到工做中,用確定是能夠用的,自己來講是對線程池的擴展,而後對接了配置中心和監控。java
目前用的話主要存在下面幾個問題:git
本文的重點就是介紹如何對接 Nacos 和 Apollo,由於一開始就支持了 Nacos,可是支持的方式是依賴了 Spring Cloud Alibaba ,若是是沒有用 Spring Cloud Alibaba 如何支持,也是須要擴展的。github
Nacos 集成的話分兩種方式,一種是你的項目使用了 Spring Cloud Alibaba ,另外一種是隻用了 Spring Boot 方式的集成。web
加入依賴: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 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 的使用咱們都是用它的 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(); } 複製代碼} 複製代碼
經過@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 中@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,歡迎勾搭。
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-cloud
感興趣的能夠關注下個人微信公衆號 猿天地,更多技術文章第一時間閱讀。個人GitHub也有一些開源的代碼 github.com/yinjihuan