聊聊springcloud專案同時存在多個註冊中心客戶端採坑記
前言
前段時間業務部門有這麼一個業務場景,他們自己微服務註冊中心是用eureka,他們有一些服務介面要呼叫兄弟部門的介面,他們定了一個服務呼叫方案,業務部門直接把他們服務註冊到兄弟部門的註冊中心,然後走rpc呼叫,兄弟部門註冊中心是用nacos。
一開始業務部門研發直接在在pom.xml這麼引入
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
然後專案啟動報瞭如下錯
Field registration in org.springframework.cloud.client.serviceregistry.ServiceRegistryAutoConfiguration$ServiceRegistryEndpointConfiguration required a single bean, but 2 were found: - nacosRegistration: defined by method 'nacosRegistration' in class path resource [com/alibaba/cloud/nacos/NacosDiscoveryAutoConfiguration.class] - eurekaRegistration: defined in BeanDefinition defined in class path resource [org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration$RefreshableEurekaClientConfiguration.class]
業務部門是這麼解決的,每次發版時,如果是要納入兄弟部門的微服務,他們就先手動註釋掉eureka的客戶端依賴。
後來業務部門就向我們部門提了一個需求,pom引入多個註冊中心客戶端,專案也要能正常啟動
需求分析
從專案異常分析
Field registration in org.springframework.cloud.client.serviceregistry.ServiceRegistryAutoConfiguration$ServiceRegistryEndpointConfiguration required a single bean, but 2 were found: - nacosRegistration: defined by method 'nacosRegistration' in class path resource [com/alibaba/cloud/nacos/NacosDiscoveryAutoConfiguration.class] - eurekaRegistration: defined in BeanDefinition defined in class path resource [org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration$RefreshableEurekaClientConfiguration.class]
可以看出springcloud預設的服務註冊是隻支援單服務註冊中心。因此我們解決的方案要麼擴充套件springcloud原始碼,讓它支援多註冊中心,要麼就是告訴springcloud當存在多種註冊中心客戶端時,選擇一種我們想要的註冊中心客戶端
本文就選實現相對容易的方案,當存在多種註冊中心客戶端時,我們告訴springcloud,我們想選的註冊中心
實現方案
目前基本只要和springboot整合的開源專案,可以說大部分使用了自動裝配,因此我們的解決思路也是從自動裝配搞起
前置知識
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
private List<String> filter(List<String> configurations, AutoConfigurationMetadata autoConfigurationMetadata) {
long startTime = System.nanoTime();
String[] candidates = StringUtils.toStringArray(configurations);
boolean[] skip = new boolean[candidates.length];
boolean skipped = false;
for (AutoConfigurationImportFilter filter : getAutoConfigurationImportFilters()) {
invokeAwareMethods(filter);
boolean[] match = filter.match(candidates, autoConfigurationMetadata);
for (int i = 0; i < match.length; i++) {
if (!match[i]) {
skip[i] = true;
candidates[i] = null;
skipped = true;
}
}
}
if (!skipped) {
return configurations;
}
List<String> result = new ArrayList<>(candidates.length);
for (int i = 0; i < candidates.length; i++) {
if (!skip[i]) {
result.add(candidates[i]);
}
}
if (logger.isTraceEnabled()) {
int numberFiltered = configurations.size() - result.size();
logger.trace("Filtered " + numberFiltered + " auto configuration class in "
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) + " ms");
}
return new ArrayList<>(result);
}
這兩個程式碼片段,一個是自動裝配的程式碼片段,一個是過濾候選哪些不需要進行自動裝配
方案一:利用AutoConfigurationImportFilter + 自定義標識
實現的原理: 當自定的標識為nacos,通過AutoConfigurationImportFilter排除eureka的自動裝配;反之排除nacos的自動裝配
1、核心實現
public class RegistrationCenterAutoConfigurationImportFilter implements AutoConfigurationImportFilter, EnvironmentAware {
private Environment environment;
/**
* 因為springboot自動裝配,預設會把spring.factories的配置的類先全部載入到候選集合中,
* 因此當我們程式碼配置啟用nacos,則需把其他註冊中心,比如eureka先從候選集合排除
* @param autoConfigurationClasses
* @param autoConfigurationMetadata
* @return
*/
@Override
public boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) {
Set<String> activeProfiles = Arrays.stream(environment.getActiveProfiles()).collect(Collectors.toSet());
if(activeProfiles.contains(RegistrationCenterConstants.ACTIVE_PROFILE_NACOS)){
return excludeMissMatchRegistrationAutoConfiguration(autoConfigurationClasses, new String[]{RegistrationCenterConstants.EUREKA_AUTO_CONFIGURATION_CLASSES});
}else if (activeProfiles.contains(RegistrationCenterConstants.ACTIVE_PROFILE_EUREKA)){
return excludeMissMatchRegistrationAutoConfiguration(autoConfigurationClasses, new String[]{RegistrationCenterConstants.NACOS_DISCOVERY_AUTO_CONFIGURATION_CLASSES,RegistrationCenterConstants.NACOS_DISCOVERY_CLIENT_AUTO_CONFIGURATION_CLASSES,RegistrationCenterConstants.NACOS_DISCOVERY_ENDPOINT_AUTO_CONFIGURATION_CLASSES});
}
return new boolean[0];
}
private boolean[] excludeMissMatchRegistrationAutoConfiguration(String[] autoConfigurationClasses,String[] needExcludeRegistrationAutoConfigurationClasse) {
Map<String,Boolean> needExcludeRegistrationAutoConfigurationClzMap = initNeedExcludeRegistrationAutoConfigurationClzMap(needExcludeRegistrationAutoConfigurationClasse);
boolean[] match = new boolean[autoConfigurationClasses.length];
for (int i = 0; i < autoConfigurationClasses.length; i++) {
String autoConfigurationClz = autoConfigurationClasses[i];
match[i] = !needExcludeRegistrationAutoConfigurationClzMap.containsKey(autoConfigurationClz);
}
return match;
}
private Map<String,Boolean> initNeedExcludeRegistrationAutoConfigurationClzMap(String[] needExcludeRegistrationAutoConfigurationClasse){
Map<String,Boolean> needExcludeRegistrationAutoConfigurationClzMap = new HashMap<>();
for (String needExcludeRegistrationAutoConfigurationClz : needExcludeRegistrationAutoConfigurationClasse) {
needExcludeRegistrationAutoConfigurationClzMap.put(needExcludeRegistrationAutoConfigurationClz,false);
}
return needExcludeRegistrationAutoConfigurationClzMap;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}
2、配置spring.factories
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
com.github.lybgeek.registration.autoconfigure.filter.RegistrationCenterAutoConfigurationImportFilter
方案二:利用application-${指定註冊中心標識} + spring.profiles.active
1、在要啟用的註冊中心的檔案禁用其他註冊中心客戶端
比如appliication-nacos.yml禁用eureka
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
# 禁用eureka客戶端自動註冊
eureka:
client:
enabled: false
2、啟用想要註冊的註冊中心
spring:
application:
name: springboot-registration-client
profiles:
active: nacos
總結
這兩種方案個人是比較推薦方案二,因為改動最小。方案一比較適用於沒有提供是否需要啟用註冊中心開關的註冊中心。其次如果我們要排除某些開源自動裝配的元件,也可以考慮用方案一
demo連結
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-registrationcenter-switch