https://mp.weixin.qq.com/s/HztGvHGi-gF88Kyz2PcE5A 封面图取自公众号:十个亿

本文来自“阿里巴巴中间件”投稿,作者:肖京,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
:
 
204
2019
-
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 
18082
2019
-
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
 finished
2019
-
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 的作用非常关键,我们在NacosDiscoveryAutoConfiguration、 ConsulAutoServiceRegistrationAutoConfiguration 以及 EurekaClientAutoConfiguration 这三个类的实现中都可以看到 ConditionalOnBean(AutoServiceRegistrationProperties.class) 这样的关键代码。可以说, ConditionalOnBean(AutoServiceRegistrationProperties.class) 是服务注册的开关。

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

下载了 spring-cloud-common 的源码,对着 AutoServiceRegistrationProperties 点击右键,选择使用 Find Usages,在下方找一下 Usagein.class 和 Newinstance 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 中实现多注册中心聚合订阅,欢迎关注。