为什么 server.ports 属性源会同时出现在 application 和 bootstrap 上下文中
server.port 属性是在 application.yml 文件中配置的,按理说应该只出现在 application context 中,但实际情况是 application 和 bootstrap 上下文中同时存在。
这是为什么呢?
事件发布的传递性
事件发布时,如果存在父上下文,那么会将事件传递给父上下文。
org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object, org.springframework.core.ResolvableType)
/**
* Publish the given event to all listeners.
* @param event the event to publish (may be an {@link ApplicationEvent}
* or a payload object to be turned into a {@link PayloadApplicationEvent})
* @param eventType the resolved event type, if known
* @since 4.2
*/
protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
Assert.notNull(event, "Event must not be null");
// Decorate event as an ApplicationEvent if necessary
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent) {
applicationEvent = (ApplicationEvent) event;
}
else {
applicationEvent = new PayloadApplicationEvent<>(this, event);
if (eventType == null) {
eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
}
}
// Multicast right now if possible - or lazily once the multicaster is initialized
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
}
else {
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}
// Publish event via parent context as well...
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext) {
((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
}
else {
this.parent.publishEvent(event);
}
}
}
ServerPortInfoApplicationContextInitializer
这是一个 ApplicationContextInitializer,配置在 spring-boot 包的 META-INF/spring.factories中,因此 application 和 bootstrap 上下文在容器初始化时都会加载。
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.context.embedded.ServerPortInfoApplicationContextInitializer
而ServerPortInfoApplicationContextInitializer的作用便是注册了事件 EmbeddedServletContainerInitializedEvent 的监听。
org.springframework.boot.context.embedded.ServerPortInfoApplicationContextInitializer#initialize
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.addApplicationListener(
new ApplicationListener<EmbeddedServletContainerInitializedEvent>() {
@Override
public void onApplicationEvent(
EmbeddedServletContainerInitializedEvent event) {
ServerPortInfoApplicationContextInitializer.this
.onApplicationEvent(event);
}
});
}
EmbeddedServletContainerInitializedEvent 事件触发
在容器初始化完成后,启动Embedded Web Application, Application 启动完毕后触发 EmbeddedServletContainerInitializedEvent 事件。
org.springframework.boot.context.embedded.EmbeddedWebApplicationContext#finishRefresh
@Override
protected void finishRefresh() {
super.finishRefresh();
EmbeddedServletContainer localContainer = startEmbeddedServletContainer();
if (localContainer != null) {
publishEvent(
new EmbeddedServletContainerInitializedEvent(this, localContainer));
}
}
EmbeddedServletContainerInitializedEvent 事件监听
public class ServerPortInfoApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
// 省略初始化内容
}
protected void onApplicationEvent(EmbeddedServletContainerInitializedEvent event) {
String propertyName = getPropertyName(event.getApplicationContext());
setPortProperty(event.getApplicationContext(), propertyName,
event.getEmbeddedServletContainer().getPort());
}
protected String getPropertyName(EmbeddedWebApplicationContext context) {
String name = context.getNamespace();
if (StringUtils.isEmpty(name)) {
name = "server";
}
return "local." + name + ".port";
}
// 该方法是递归方法,会在所有的层级的 context 上都添加 server.ports 属性源。
private void setPortProperty(ApplicationContext context, String propertyName,
int port) {
if (context instanceof ConfigurableApplicationContext) {
setPortProperty(((ConfigurableApplicationContext) context).getEnvironment(),
propertyName, port);
}
// 如果容器存在父容器,进行递归调用
if (context.getParent() != null) {
setPortProperty(context.getParent(), propertyName, port);
}
}
@SuppressWarnings("unchecked")
private void setPortProperty(ConfigurableEnvironment environment, String propertyName,
int port) {
MutablePropertySources sources = environment.getPropertySources();
PropertySource<?> source = sources.get("server.ports");
if (source == null) {
source = new MapPropertySource("server.ports", new HashMap<String, Object>());
sources.addFirst(source);
}
((Map<String, Object>) source.getSource()).put(propertyName, port);
}
}
总结
通过对源码的解析不难看出, 即使不考虑 spring 事件的传递性, EmbeddedServletContainerInitializedEvent事件触发时,其监听器也会在 context 的所有层级添加 server.ports 属性源。
如果考虑 spring 事件的传递性, 相当于 事件监听器 被多次执行,不过好在 server.ports 属性源只有在获取不到时才会被创建。:)