深入Spring Boot:Spring Context的繼承關係和影響

前言

對於一個簡單的Spring boot應用,它的spring context是隻會有一個。

AnnotationConfigEmbeddedWebApplicationContext是spring boot裏自己實現的一個context,主要功能是啓動embedded servlet container,比如tomcat/jetty。

這個和傳統的war包應用不一樣,傳統的war包應用有兩個spring context。參考:http://hengyunabc.github.io/something-about-spring-mvc-webapplicationcontext/

但是對於一個複雜點的spring boot應用,它的spring context可能會是多個,下面分析下各種情況。

Demo

這個Demo展示不同情況下的spring boot context的繼承情況。

https://github.com/hengyunabc/spring-boot-inside/tree/master/demo-classloader-context

配置spring boot actuator/endpoints獨立端口時

spring boot actuator默認情況下和應用共用一個tomcat,這樣子的話就會直接把應用的endpoints暴露出去,帶來很大的安全隱患。

儘管 Spring boot後面默認把這個關掉,需要配置management.security.enabled=false纔可以訪問,但是這個還是太危險了。

所以通常都建議把endpoints開在另外一個獨立的端口上,比如 management.port=8081

可以增加-Dspring.cloud.bootstrap.enabled=false,來禁止spring cloud,然後啓動Demo。比如

mvn spring-boot:run -Dspring.cloud.bootstrap.enabled=false

然後打開 http://localhost:8080/ 可以看到應用的spring context繼承結構。

打開 http://localhost:8081/contexttree 可以看到Management Spring Contex的繼承結構。

  • 可以看到當配置management獨立端口時,management context的parent是應用的spring context
  • 相關的實現代碼在 org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration

在sprig cloud環境下spring context的情況

在有spring cloud時(通常是引入 spring-cloud-starter),因爲spring cloud有自己的一套配置初始化機制,所以它實際上是自己啓動了一個Spring context,並把自己置爲應用的context的parent。

spring cloud context的啓動代碼在org.springframework.cloud.bootstrap.BootstrapApplicationListener裏。

spring cloud context實際上是一個特殊的spring boot context,它只掃描BootstrapConfiguration

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// Use names and ensure unique to protect against duplicates
List<String> names = SpringFactoriesLoader
    .loadFactoryNames(BootstrapConfiguration.class, classLoader);
for (String name : StringUtils.commaDelimitedListToStringArray(
    environment.getProperty("spring.cloud.bootstrap.sources", ""))) {
  names.add(name);
}
// TODO: is it possible or sensible to share a ResourceLoader?
SpringApplicationBuilder builder = new SpringApplicationBuilder()
    .profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF)
    .environment(bootstrapEnvironment)
    .properties("spring.application.name:" + configName)
    .registerShutdownHook(false).logStartupInfo(false).web(false);
List<Class<?>> sources = new ArrayList<>();

最終會把這個ParentContextApplicationContextInitializer加到應用的spring context裏,來把自己設置爲應用的context的parent。

public class ParentContextApplicationContextInitializer implements
		ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
	private int order = Ordered.HIGHEST_PRECEDENCE;
	private final ApplicationContext parent;
	@Override
	public void initialize(ConfigurableApplicationContext applicationContext) {
		if (applicationContext != this.parent) {
			applicationContext.setParent(this.parent);
			applicationContext.addApplicationListener(EventPublisher.INSTANCE);
		}
	}

和上面一樣,直接啓動demo,不要配置-Dspring.cloud.bootstrap.enabled=false,然後訪問對應的url,就可以看到spring context的繼承情況。

如何在應用代碼裏獲取到 Management Spring Context

如果應用代碼想獲取到Management Spring Context,可以通過這個bean:org.springframework.boot.actuate.autoconfigure.ManagementContextResolver

spring boot在創建Management Spring Context時,就會保存到ManagementContextResolver裏。

@Configuration
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@ConditionalOnWebApplication
@AutoConfigureAfter({ PropertyPlaceholderAutoConfiguration.class,
		EmbeddedServletContainerAutoConfiguration.class, WebMvcAutoConfiguration.class,
		ManagementServerPropertiesAutoConfiguration.class,
		RepositoryRestMvcAutoConfiguration.class, HypermediaAutoConfiguration.class,
		HttpMessageConvertersAutoConfiguration.class })
public class EndpointWebMvcAutoConfiguration
		implements ApplicationContextAware, BeanFactoryAware, SmartInitializingSingleton {
      @Bean
    	public ManagementContextResolver managementContextResolver() {
    		return new ManagementContextResolver(this.applicationContext);
    	}

    	@Bean
    	public ManagementServletContext managementServletContext(
    			final ManagementServerProperties properties) {
    		return new ManagementServletContext() {

    			@Override
    			public String getContextPath() {
    				return properties.getContextPath();
    			}

    		};
    	}

如何在Endpoints代碼裏獲取應用的Spring context

spring boot本身沒有提供方法,應用可以自己寫一個@Configuration,保存應用的Spring context,然後在endpoints代碼裏再取出來。

ApplicationContext.setParent(ApplicationContext) 到底發生了什麼

從spring的代碼就可以看出來,主要是把parent的environment裏的propertySources加到child裏。這也就是spring cloud config可以生效的原因。

// org.springframework.context.support.AbstractApplicationContext.setParent(ApplicationContext)
/**
 * Set the parent of this application context.
 * <p>The parent {@linkplain ApplicationContext#getEnvironment() environment} is
 * {@linkplain ConfigurableEnvironment#merge(ConfigurableEnvironment) merged} with
 * this (child) application context environment if the parent is non-{@code null} and
 * its environment is an instance of {@link ConfigurableEnvironment}.
 * @see ConfigurableEnvironment#merge(ConfigurableEnvironment)
 */
@Override
public void setParent(ApplicationContext parent) {
  this.parent = parent;
  if (parent != null) {
    Environment parentEnvironment = parent.getEnvironment();
    if (parentEnvironment instanceof ConfigurableEnvironment) {
      getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
    }
  }
}
// org.springframework.core.env.AbstractEnvironment.merge(ConfigurableEnvironment)

@Override
public void merge(ConfigurableEnvironment parent) {
  for (PropertySource<?> ps : parent.getPropertySources()) {
    if (!this.propertySources.contains(ps.getName())) {
      this.propertySources.addLast(ps);
    }
  }
  String[] parentActiveProfiles = parent.getActiveProfiles();
  if (!ObjectUtils.isEmpty(parentActiveProfiles)) {
    synchronized (this.activeProfiles) {
      for (String profile : parentActiveProfiles) {
        this.activeProfiles.add(profile);
      }
    }
  }
  String[] parentDefaultProfiles = parent.getDefaultProfiles();
  if (!ObjectUtils.isEmpty(parentDefaultProfiles)) {
    synchronized (this.defaultProfiles) {
      this.defaultProfiles.remove(RESERVED_DEFAULT_PROFILE_NAME);
      for (String profile : parentDefaultProfiles) {
        this.defaultProfiles.add(profile);
      }
    }
  }
}

怎樣在Spring Event里正確判斷事件來源

默認情況下,Spring Child Context會收到Parent Context的Event。如果Bean依賴某個Event來做初始化,那麼就要判斷好Event是否Bean所在的Context發出的,否則有可能提前或者多次初始化。

正確的做法是實現ApplicationContextAware接口,先把context保存起來,在Event裏判斷相等時才處理。

public class MyBean implements ApplicationListener<ContextRefreshedEvent>, ApplicationContextAware {
	private ApplicationContext context;
	@Override
	public void onApplicationEvent(ContextRefreshedEvent event) {
		if (event.getApplicationContext().equals(context)) {
			// do something
		}
	}
	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.context = applicationContext;
	}
}

總結

  • 當配置management.port 爲獨立端口時,Management Spring Context也會是獨立的context,它的parent是應用的spring context
  • 當啓動spring cloud時,spring cloud自己會創建出一個spring context,並置爲應用的context的parent
  • ApplicationContext.setParent(ApplicationContext) 主要是把parent的environment裏的propertySources加到child裏
  • 正確處理Spring Event,判斷屬於自己的Context和Event
  • 理解的spring boot context的繼承關係,能避免一些微妙的spring bean注入的問題,還有不當的spring context的問題

公衆號

橫雲斷嶺的專欄

橫雲斷嶺的專欄

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章