Spring Cloud 应用如何注册到多个注册中心_Java

封面图取自公众号:十个亿


本文来自“阿里巴巴中间件”投稿,作者:肖京,spring cloud alibaba成员, PMC


引言

我们知道,使用 Spring Cloud 开发微服务时,服务注册的使用方式非常简单,只需要引入服务注册的依赖即可。

  1. <dependencies>

  2.    <dependency>

  3.        <groupId>org.springframework.cloud</groupId>

  4.        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>

  5.        <version>0.9.0.RELEASE</version>            

  6.    </dependency>

  7.    <dependency>

  8.        <groupId>org.springframework.boot</groupId>

  9.        <artifactId>spring-boot-starter-web</artifactId>

  10.    </dependency>

  11. </dependencies>


  12. <dependencyManagement>

  13.    <dependencies>

  14.        <dependency>

  15.            <groupId>org.springframework.cloud</groupId>

  16.            <artifactId>spring-cloud-dependencies</artifactId>

  17.            <version>Greenwich.SR1</version>

  18.            <type>pom</type>

  19.            <scope>import</scope>

  20.        </dependency>

  21.    </dependencies>

  22. </dependencyManagement>

但是有些情况下,我们会有将一个 Spring Cloud 应用注册到多个服务注册中心的需求。

这时候如果简单地在依赖中添加两个服务注册组件的依赖,则应用在启动阶段就会报错,导致启动失败。

为什么不能多注册?

首先,我们在 Spring Cloud 应用中引入两个服务注册组件的依赖,重现一下启动失败的场景。

<dependencies>    <dependency>        <groupId>org.springframework.cloud</groupId>        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>        <version>0.9.0.RELEASE</version>    </dependency>    <dependency>        <groupId>org.springframework.cloud</groupId>        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>    </dependency>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>    </dependency></dependencies>

启动 main 方法,报错的信息如下所示。

  1. ***************************

  2. APPLICATION FAILED TO START

  3. ***************************


  4. Description:


  5. Field autoServiceRegistration in org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration required a single bean, but 2 were found:

  6.    - nacosAutoServiceRegistration: defined by method 'nacosAutoServiceRegistration' in class path resource [org/springframework/cloud/alibaba/nacos/NacosDiscoveryAutoConfiguration.class]

  7.    - eurekaAutoServiceRegistration: defined by method 'eurekaAutoServiceRegistration' in class path resource [org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.class]



  8. Action:


  9. Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

看日志可以发现启动失败的原因是因为 AutoServiceRegistrationAutoConfiguration 这个类需要自动注入一个类型为 AutoServiceRegistration 的 bean。但是在 Spring 容器中,发现了两个父类为 AutoServiceRegistration 的 bean,分别是 nacosAutoServiceRegistration 和 eurekaAutoServiceRegistration。这样就导致了自动注入时不知道应该选择使用哪个 bean,进而导致了应用启动失败。

提示的解决方案是将其中的一个 bean 标记为 @Primary,但是我们既无法修改 netflix-eureka-client 的源码,又无法修改 alibaba-nacos-discovery 的源码,而且我们还不能修改 AutoServiceRegistrationAutoConfiguration 所处于的 spring-cloud-commons 的源码。

没办法解决了吗?既然无法修改他们的源码,那我们现在换一个思路,我们将 AutoServiceRegistrationAutoConfiguration这个类从 autoconfigure 中排除。

使用如下方法,将其排除,在 application.properties 中添加如下配置,然后重新启动应用。

spring.autoconfigure.exclude=org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration

日志表明两边都注册成功了,登录控制台查看,也确实是如此。

2019-04-22 11:12:37.050  INFO 29189 --- [nfoReplicator-0] com.netflix.discovery.DiscoveryClient    : DiscoveryClient_OPENSOURCE-SERVICE-PROVIDER/192.168.0.2:opensource-service-provider:18082: registering service...2019-04-22 11:12:37.089  INFO 29189 --- [nfoReplicator-0] com.netflix.discovery.DiscoveryClient    : DiscoveryClient_OPENSOURCE-SERVICE-PROVIDER/192.168.0.2:opensource-service-provider:18082 - registration status: 2042019-04-22 11:12:37.109  INFO 29189 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 18082 (http) with context path ''2019-04-22 11:12:37.110  INFO 29189 --- [           main] .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 180822019-04-22 11:12:37.119  INFO 29189 --- [           main] o.s.c.a.n.registry.NacosServiceRegistry  : nacos registry, opensource-service-provider 192.168.0.2:18082 register finished2019-04-22 11:12:37.123  INFO 29189 --- [           main] c.a.demo.provider.ProviderApplication    : Started ProviderApplication in 4.352 seconds (JVM running for 4.928)

这样就解决了?

虽然直接 AutoServiceRegistrationAutoConfiguration这个类从 autoconfigure 中排除可以注册成功了。

但是这样做不会有什么副作用,或者影响其他功能吗?心里感觉没底,还是有点慌,对不对?

别慌,我们来看一下这个类的源码。

  1. @Configuration

  2. @Import(AutoServiceRegistrationConfiguration.class)

  3. @ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)

  4. public class AutoServiceRegistrationAutoConfiguration {


  5.    @Autowired(required = false)

  6.    private AutoServiceRegistration autoServiceRegistration;


  7.    @Autowired

  8.    private AutoServiceRegistrationProperties properties;


  9.    @PostConstruct

  10.    protected void init() {

  11.        if (autoServiceRegistration == null && this.properties.isFailFast()) {

  12.            throw new IllegalStateException("Auto Service Registration has been requested, but there is no AutoServiceRegistration bean");

  13.        }

  14.    }

  15. }

重点关注这两个部分 @Import(AutoServiceRegistrationConfiguration.class)init方法

init 方法

首先看 init方法。它的逻辑是做一个检查,如果 autoServiceRegistration 为空且 AutoServiceRegistrationProperties 的 failFast 属性为 true 的情况下,就直接抛出 IllegalStateException 异常。

没事,我们现在的问题就是因为 AutoServiceRegistration 太多了。而且 AutoServiceRegistrationProperties 中的 failFast 字段默认值是 false,除非你配置了为 true,否则这段逻辑本身也不会执行。

总结一下,从 init方法 来看,将 AutoServiceRegistrationAutoConfiguration 排除相当于使 AutoServiceRegistrationProperties 中的 failFast 字段失效。

如果你真的对这个配置有特别强的需求,那么你可以在手动排除后自行加上这块逻辑。但是在笔者看来完全没必要,无非就是在后面会更晚的阶段抛出另外一个异常而已。

@Import(AutoServiceRegistrationConfiguration.class)

然后我们再看看看 @Import(AutoServiceRegistrationConfiguration.class) 的逻辑。

@Configuration@EnableConfigurationProperties(AutoServiceRegistrationProperties.class)@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)public class AutoServiceRegistrationConfiguration {}

AutoServiceRegistrationConfiguration 这个类其实就只做了一件事,实例化一个 AutoServiceRegistrationProperties 的 bean。

AutoServiceRegistrationProperties 的作用非常关键,我们在NacosDiscoveryAutoConfigurationConsulAutoServiceRegistrationAutoConfiguration 以及 EurekaClientAutoConfiguration 这三个类的实现中都可以看到 ConditionalOnBean(AutoServiceRegistrationProperties.class) 这样的关键代码。可以说, ConditionalOnBean(AutoServiceRegistrationProperties.class) 是服务注册的开关。

那问题来了,为什么我们把他排除了之后,应用不仅启动成功了,还分别成功注册到两个注册中心了呢?

下载了 spring-cloud-common 的源码,对着 AutoServiceRegistrationProperties 点击右键,选择使用 Find Usages,在下方找一下 Usagein.classNewinstance creation,并没有找到其他实例化 AutoServiceRegistrationProperties 的使用。

那这个 bean 到底是在什么情况下实例化的呢?换个思路,既然这个 bean 只能通过 AutoServiceRegistrationConfiguration 这个类来实例化,那么我们找找 AutoServiceRegistrationConfiguration 还在那里被使用到了。继续对着 AutoServiceRegistrationConfiguration 点击右键,选择使用 Find Usages,依旧没有找到。

最后没办法,使用全文搜索试试,终于找到了如下代码片段,下面的引用只保留了关键的部分。

  1. @Order(Ordered.LOWEST_PRECEDENCE - 100)

  2. public class EnableDiscoveryClientImportSelector extends SpringFactoryImportSelector<EnableDiscoveryClient> {


  3.    @Override

  4.    public String[] selectImports(AnnotationMetadata metadata) {

  5.        String[] imports = super.selectImports(metadata);


  6.        AnnotationAttributes attributes = AnnotationAttributes.fromMap(

  7.                metadata.getAnnotationAttributes(getAnnotationClass().getName(), true));


  8.        boolean autoRegister = attributes.getBoolean("autoRegister");


  9.        if (autoRegister) {

  10.            List<String> importsList = new ArrayList<>(Arrays.asList(imports));

  11.            importsList.add(

  12.                    "org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration");

  13.            imports = importsList.toArray(new String[0]);

  14.        }

  15.        else {

  16.            .........

  17.        }


  18.        return imports;

  19.    }


  20.    .........


  21. }

我们在看看 ImportSelector 这个接口对于 selectImports(AnnotationMetadataimportingClassMetadata) 方法的注释。

  1. public interface ImportSelector {


  2.    /**

  3.     * Select and return the names of which class(es) should be imported based on

  4.     * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.

  5.     */

  6.    String[] selectImports(AnnotationMetadata importingClassMetadata);


  7. }

从这段代码逻辑中可以看到,只要引入了 @EnableDiscoveryClient,且没有显示地指定 autoRegister 为 false,那么就会引入 AutoServiceRegistrationConfiguration 这个 Configuration。

总结一下,从 @Import(AutoServiceRegistrationConfiguration.class) 这部分来看,将 AutoServiceRegistrationAutoConfiguration 排除后,则必须要存在@EnableDiscoveryClient 注解,且没有显示地指定 autoRegister 为 false,服务才能自动注册。

总结

通过刚才的分析,我们重述一下将 AutoServiceRegistrationAutoConfiguration 排除后的影响面。

  • AutoServiceRegistrationProperties 中的 failFast 字段失效

  • 必须要存在 @EnableDiscoveryClient 注解,且没有显示地指定 autoRegister 为 false,服务才能自动注册。

看到这里,我们应该定位到了问题的影响面。除非对于上述的两点有特殊的需求,在 spring.autoconfigure 中 exclude 掉 AutoServiceRegistrationAutoConfiguration,不会有其他副作用。

更进一步

1.刚才演示的是一个最基础的场景。一般来说,我们的 spring boot 应用都会使用 spring-boot-starter-actuator,当存在这个依赖时,即使执行了上文的操作,启动时还是报错。

这该怎么办?根据报错信息定位到是 ServiceRegistryAutoConfiguration 这个类,接着排除就可以,至于排除后会产生哪些影响,监控会少一个 Endpoint,这里就不具体分析了。

2.在配置文件中填写 spring.autoconfigure.exclude 中添加类比较麻烦,还有其他办法吗?

  • 在代码中排除,@SpringBootApplication(exclude=SecurityAutoConfiguration.class)

  • 通过 AutoConfigurationImportFilter 来排除

重点讲一下第二种方法

  1. public class RegistryExcludeFilter implements AutoConfigurationImportFilter {


  2.    private static final Set<String> SHOULD_SKIP = new HashSet<>(

  3.        Arrays.asList("org.springframework.cloud.client.serviceregistry.ServiceRegistryAutoConfiguration",

  4.            "org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration"));


  5.    @Override

  6.    public boolean[] match(String[] classNames, AutoConfigurationMetadata metadata) {

  7.        boolean[] matches = new boolean[classNames.length];


  8.        for (int i = 0; i < classNames.length; i++) {

  9.            matches[i] = !SHOULD_SKIP.contains(classNames[i]);

  10.        }

  11.        return matches;

  12.    }

  13. }

然后将 RegistryExcludeFilter 添加到 spring.factories 中

org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=xxx.xxx.RegistryExcludeFilter

看起来这样是麻烦了一些,多了一步,但是我们可以将这些修改放在一个 base 包中,业务开发时只需要引入这个 base 包即可。

3.使用场景

讲了这么多,照应一下开头,到底是什么场景会有需要注册到多个注册中心的需求呢?

我们目前看到的场景是迁移注册中心的时候会有这个需求。当应用需要进行迁移时,如何保证业务不中断是重中之重。而服务注册中心与服务调用强相关,可以说服务注册中心的平滑迁移是应用平滑迁移的基础。

也许你不想进行上述的那么多操作,而是想直接体验多注册的特性。 笔者已经基于上面说的第二种方法完成了一个 base 包,且同时支持 Spring Boot/Cloud 的各个版本,直接引入下面的依赖,用起来吧。

<dependency>       <groupId>com.alibaba.edas</groupId>       <artifactId>edas-sc-migration-starter</artifactId>       <version>1.0.1</version></dependency>


4.下集预告

下一篇,我们将讲述一下如何在 Ribbon 中实现多注册中心聚合订阅,欢迎关注。


推荐阅读