上篇文章 《一时技痒,撸了个动态线程池,源码放 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