封面图取自公众号:十个亿
本文来自“阿里巴巴中间件”投稿,作者:肖京,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
的作用非常关键,我们在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 中实现多注册中心聚合订阅,欢迎关注。
推荐阅读: