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


免責聲明!

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



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