Let’s talk about the spring cloud project where there are multiple registry clients at the same time

Time:2021-9-22

preface

Some time ago, the business department had a business scenario. Their own micro service registry uses Eureka. They have some service interfaces to call the interfaces of brother departments. They set a service call scheme. The business department directly registers their services with the registry of brother departments, and then calls them through RPC. The registry of brother departments uses Nacos.

At the beginning, the R & D of the business department was directly introduced in 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>

Then the following error is reported during project startup

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]

The business department solves this problem by manually annotating Eureka’s client dependency if it is to include the microservices of brother departments during each release.

Later, the business department asked our department to introduce multiple registry clients into POM, and the project should start normally

requirement analysis

From project exception analysis

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]

It can be seen that the default service registration of spring cloud only supports a single service registry. Therefore, our solution is to either extend the source code of spring cloud to support multiple registries, or tell spring cloud to select a registry client we want when there are multiple registry clients

In this article, we choose a scheme that is relatively easy to implement. When there are multiple registry clients, we tell spring cloud which Registry we want to choose

Implementation scheme

At present, as long as the open source projects that are basically integrated with springboot, it can be said that most of them use automatic assembly, so our solution also starts from automatic assembly

Pre knowledge

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);
    }

These two code fragments, one is the code fragment of automatic assembly, and the other is to filter candidates which do not need automatic assembly

Scheme 1: use autoconfigurationimportfilter + custom ID

Principle of implementation:When the self-defined ID is Nacos, the automatic assembly of Eureka is excluded through autoconfigurationimportfilter; On the contrary, the automatic assembly of Nacos is excluded

1. Core implementation

public class RegistrationCenterAutoConfigurationImportFilter implements AutoConfigurationImportFilter, EnvironmentAware {

    private Environment environment;

    /**
     *Because springboot is automatically assembled, all configured classes of spring.factories will be loaded into the candidate collection by default,
     *Therefore, when we configure the code to enable Nacos, we need to exclude other registries, such as Eureka, from the candidate set first
     * @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. Configure spring.factories

org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
com.github.lybgeek.registration.autoconfigure.filter.RegistrationCenterAutoConfigurationImportFilter

Scheme 2: use application – ${to specify the registry ID} + spring.profiles.active

1. Disable other registry clients in the file of the registry you want to activate

For example, application-nacos.yml disables Eureka

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

#Disable Eureka client automatic registration
eureka:
  client:
    enabled: false

2. Activate the registry you want to register

spring:
  application:
    name: springboot-registration-client
  profiles:
    active: nacos

summary

The two schemes are recommended because the changes are minimal. Scheme 1 is applicable to registries that do not provide whether the registry switch needs to be activated. Secondly, if we want to exclude some open source automatic assembly components, we can also consider scheme 1

Demo link

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-registrationcenter-switch