Spring-Cloud中AbstractAutoServiceRegistration的“bug“

前话:zipkin-server与nacos结合时的坑

参考github解决方案

在解决Nacos的issue——对接zipkin时,遇到了一个奇怪的问题,当采用github的解决方案,即采用eureka-client时,zipkin-server能够自动注册到Eureka Server中,但是当采用nacos-discovery时,却怎么也无法实现zipkin-server服务自动注册到nacos-server中,通过断点调试以及参考Spring Cloud相关源码以及文档,终于发现了问题所在

Spring-Cloud认为服务自动注册的时机

SpringCloud本身认为服务的注册时机,应该是WebServerInitializedEvent事件发生后,进行服务的自动注册,因为在接收到此事件时,会下发bind(Event)操作,由start()函数内部调用register()实现服务的自动注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Override
@SuppressWarnings("deprecation")
public void onApplicationEvent(WebServerInitializedEvent event) {
bind(event);
}

@Deprecated
public void bind(WebServerInitializedEvent event) {
ApplicationContext context = event.getApplicationContext();
if (context instanceof ConfigurableWebServerApplicationContext) {
if ("management".equals(((ConfigurableWebServerApplicationContext) context)
.getServerNamespace())) {
return;
}
}
this.port.compareAndSet(0, event.getWebServer().getPort());
this.start();
}

public void start() {
if (!isEnabled()) {
if (logger.isDebugEnabled()) {
logger.debug("Discovery Lifecycle disabled. Not starting");
}
return;
}

// only initialize if nonSecurePort is greater than 0 and it isn't already running
// because of containerPortInitializer below
if (!this.running.get()) {
this.context.publishEvent(
new InstancePreRegisteredEvent(this, getRegistration()));
register();
if (shouldRegisterManagement()) {
registerManagement();
}
this.context.publishEvent(
new InstanceRegisteredEvent<>(this, getConfiguration()));
this.running.compareAndSet(false, true);
}

}

存在的问题

由上面的代码可知,SpringCloud本身对于服务注册的时机,是在发生WebServerInitializedEvent事件之后,才会去调用相应的register()方法实现服务注册,但是这里就可能存在一个小小的问题了。

在创建SpringBoot Web Application时,有一段代码比较关键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, "
+ "please specify an ApplicationContextClass",
ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

因此,这里的contextClass对应着三种不同的类型,分别是AnnotationConfigServletWebServerApplicationContextAnnotationConfigReactiveWebServerApplicationContext以及AnnotationConfigApplicationContext,而这三个中,只有前面两个Context会触发WebServerInitializedEvent事件的下发

AnnotationConfigServletWebServerApplicationContext

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void finishRefresh() {
super.finishRefresh();
WebServer webServer = startWebServer();
if (webServer != null) {
publishEvent(new ServletWebServerInitializedEvent(webServer, this));
}
}

public class ServletWebServerInitializedEvent extends WebServerInitializedEvent {
...
}

AnnotationConfigReactiveWebServerApplicationContext

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void finishRefresh() {
super.finishRefresh();
WebServer webServer = startReactiveWebServer();
if (webServer != null) {
publishEvent(new ReactiveWebServerInitializedEvent(webServer, this));
}
}

public class ReactiveWebServerInitializedEvent extends WebServerInitializedEvent {
...
}

AnnotationConfigApplicationContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected void finishRefresh() {
// Clear context-level resource caches (such as ASM metadata from scanning).
clearResourceCaches();

// Initialize lifecycle processor for this context.
initLifecycleProcessor();

// Propagate refresh to lifecycle processor first.
getLifecycleProcessor().onRefresh();

// Publish the final event.
publishEvent(new ContextRefreshedEvent(this));

// Participate in LiveBeansView MBean, if active.
LiveBeansView.registerApplicationContext(this);
}

public class ContextRefreshedEvent extends ApplicationContextEvent {
...
}

因此,如果设置了spring.main.web-application-type=none时,createApplicationContext代码中的switch分支就会执行contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);语句,此时获取到的ContextAnnotationConfigApplicationContext,而AnnotationConfigApplicationContext调用的finishRefresh方法所下发的事件ContextRefreshedEvent是没有继承WebServerInitializedEvent的,在这种情况下,SpringCloud就无法执行服务自动注册

进行调整

调整的方法很简单,由于SpringBoot采用事件机制,因此当整个SpringBoot Application程序初始化完毕,执行listeners.running(context)时,会触发ApplicationReadyEvent事件

SpringApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
...

try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}

EventPublishingRunListener

1
2
3
4
5
@Override
public void running(ConfigurableApplicationContext context) {
context.publishEvent(
new ApplicationReadyEvent(this.application, this.args, context));
}

因此,只需要改动如下代码即可

org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration

1
2
3
4
5
public abstract class AbstractAutoServiceRegistration<R extends Registration>
implements AutoServiceRegistration, ApplicationContextAware,
ApplicationListener<ApplicationReadyEvent> {
...
}

更改事件监听类型为ApplicationReadyEvent即可