前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring Cloud 应用如何注册到多个注册中心

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

作者头像
程序猿DD
发布2019-05-14 17:37:11
4.3K0
发布2019-05-14 17:37:11
举报
文章被收录于专栏:程序猿DD程序猿DD

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

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

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

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

引言

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

代码语言:javascript
复制
<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 应用中引入两个服务注册组件的依赖,重现一下启动失败的场景。

代码语言:javascript
复制
<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 方法,报错的信息如下所示。

代码语言:javascript
复制
***************************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 中添加如下配置,然后重新启动应用。

代码语言:javascript
复制
spring.autoconfigure.exclude=org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration

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

代码语言:javascript
复制
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 中排除可以注册成功了。

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

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

代码语言:javascript
复制
@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) 的逻辑。

代码语言:javascript
复制
@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,依旧没有找到。

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

代码语言:javascript
复制
@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) 方法的注释。

代码语言:javascript
复制
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 来排除

重点讲一下第二种方法

代码语言:javascript
复制
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 中

代码语言:javascript
复制
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=xxx.xxx.RegistryExcludeFilter
代码语言:javascript
复制

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

3.使用场景

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

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

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

代码语言:javascript
复制
<dependency>       <groupId>com.alibaba.edas</groupId>       <artifactId>edas-sc-migration-starter</artifactId>       <version>1.0.1</version></dependency>

4.下集预告

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-04-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序猿DD 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 为什么不能多注册?
  • 这样就解决了?
    • init 方法
      • @Import(AutoServiceRegistrationConfiguration.class)
        • 总结
        • 更进一步
        相关产品与服务
        微服务引擎 TSE
        微服务引擎(Tencent Cloud Service Engine)提供开箱即用的云上全场景微服务解决方案。支持开源增强的云原生注册配置中心(Zookeeper、Nacos 和 Apollo),北极星网格(腾讯自研并开源的 PolarisMesh)、云原生 API 网关(Kong)以及微服务应用托管的弹性微服务平台。微服务引擎完全兼容开源版本的使用方式,在功能、可用性和可运维性等多个方面进行增强。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档