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

点击蓝色“程序猿DD”关注我哟

加个“星标”,不忘签到哦

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

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

引言

我们知道,使用 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.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>    </dependency></dependencies>
<dependencyManagement>    <dependencies>        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-dependencies</artifactId>            <version>Greenwich.SR1</version>            <type>pom</type>            <scope>import</scope>        </dependency>    </dependencies></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 方法,报错的信息如下所示。

***************************APPLICATION FAILED TO START***************************
Description:
Field autoServiceRegistration in org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration required a single bean, but 2 were found:    - nacosAutoServiceRegistration: defined by method 'nacosAutoServiceRegistration' in class path resource [org/springframework/cloud/alibaba/nacos/NacosDiscoveryAutoConfiguration.class]    - eurekaAutoServiceRegistration: defined by method 'eurekaAutoServiceRegistration' in class path resource [org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.class]

Action:
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 中排除可以注册成功了。

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

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

@Configuration@Import(AutoServiceRegistrationConfiguration.class)@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)public class AutoServiceRegistrationAutoConfiguration {
    @Autowired(required = false)    private AutoServiceRegistration autoServiceRegistration;
    @Autowired    private AutoServiceRegistrationProperties properties;
    @PostConstruct    protected void init() {        if (autoServiceRegistration == null && this.properties.isFailFast()) {            throw new IllegalStateException("Auto Service Registration has been requested, but there is no AutoServiceRegistration bean");        }    }}

重点关注这两个部分 @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,依旧没有找到。

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

@Order(Ordered.LOWEST_PRECEDENCE - 100)public class EnableDiscoveryClientImportSelector extends SpringFactoryImportSelector<EnableDiscoveryClient> {
    @Override    public String[] selectImports(AnnotationMetadata metadata) {        String[] imports = super.selectImports(metadata);
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(                metadata.getAnnotationAttributes(getAnnotationClass().getName(), true));
        boolean autoRegister = attributes.getBoolean("autoRegister");
        if (autoRegister) {            List<String> importsList = new ArrayList<>(Arrays.asList(imports));            importsList.add(                    "org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration");            imports = importsList.toArray(new String[0]);        }        else {            .........        }
        return imports;    }
    .........
}

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

public interface ImportSelector {
    /**     * Select and return the names of which class(es) should be imported based on     * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.     */    String[] selectImports(AnnotationMetadata importingClassMetadata);
}

从这段代码逻辑中可以看到,只要引入了 @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 来排除

重点讲一下第二种方法

public class RegistryExcludeFilter implements AutoConfigurationImportFilter {
    private static final Set<String> SHOULD_SKIP = new HashSet<>(        Arrays.asList("org.springframework.cloud.client.serviceregistry.ServiceRegistryAutoConfiguration",            "org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration"));
    @Override    public boolean[] match(String[] classNames, AutoConfigurationMetadata metadata) {        boolean[] matches = new boolean[classNames.length];
        for (int i = 0; i < classNames.length; i++) {            matches[i] = !SHOULD_SKIP.contains(classNames[i]);        }        return matches;    }}

然后将 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 中实现多注册中心聚合订阅,欢迎关注。

原文发布于微信公众号 - 程序猿DD(didispace)

原文发表时间:2019-04-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券