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

前言

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

AnnotationConfigEmbeddedWebApplicationContext是spring boot裏本身實現的一個context,主要功能是啓動embedded servlet container,好比tomcat/jetty。java

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

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

Demo

這個Demo展現不一樣狀況下的spring boot context的繼承狀況。web

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

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

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

儘管 Spring boot後面默認把這個關掉,須要配置management.security.enabled=false才能夠訪問,可是這個仍是太危險了。api

因此一般都建議把endpoints開在另一個獨立的端口上,好比 management.port=8081spring-mvc

能夠增長-Dspring.cloud.bootstrap.enabled=false,來禁止spring cloud,而後啓動Demo。好比tomcat

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的問題

公衆號

橫雲斷嶺的專欄

橫雲斷嶺的專欄

相關文章
相關標籤/搜索