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