Loading...
墨滴

lyq

2021/12/19  阅读:33  主题:橙心

Spring Cloud 引导上下文源码分析

spring cloud 版本为Hoxton.SR9


前言

最近由于工作比较忙,一直没有腾出时间更新。今天周末,闲暇之余继续接着肝一篇。在《Spring Boot外部化配置源码分析》中,我们知道:在spring boot启动阶段的prepareEnvironment阶段会发布ApplicationEnvironmentPreparedEvent事件,会被ConfigFileApplicationListener监听并处理。在spring cloud环境中,同样有一个监听器来监听ApplicationEnvironmentPreparedEvent事件,这个类就是BootstrapApplicationListener。那么下面一起来看看,这个类是怎么加载引导上下文的。

源码分析

BootstrapApplicationListener

public class BootstrapApplicationListener
  implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered

开始之前先看看这个类实现了Ordered接口,那么必然会重写它的getOrder()方法:

public int getOrder() {
 return this.order;
}
public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 5;

private int order = DEFAULT_ORDER;

通过上面的代码,可以看到返回的最高优先级+5,而ConfigFileApplicationListener同样实现了Ordered接口,它的优先级为:最高优先级+10。所以在spring cloud环境下,优先加载的是BootstrapApplicationListener,搞清楚这一点很重要!!!

ok,接下来看看重写的onApplicationEvent方法:

@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
 // 获取prepareEnvironment中传入的Environment对象
 ConfigurableEnvironment environment = event.getEnvironment();
 // 判断环境中的spring.cloud.bootstrap.enabled属性是否等于true,如果不等于true,则退出,说明不需要加载引导上下文。
 if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class,
   true)) 
{
  return;
 }
 // don't listen to events in a bootstrap context
 // 如果环境变量中已经包含名为:bootstrap的属性,则退出,防止陷入死循环
 if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
  return;
 }
 ConfigurableApplicationContext context = null;
 // 是否配置spring.cloud.bootstrap.name属性,没有配置默认使用bootstrap
 String configName = environment
   .resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
 // 迭代spring上下文中的ApplicationContextInitializer实例bean
 for (ApplicationContextInitializer<?> initializer : event.getSpringApplication()
   .getInitializers()) {
  // 如果当前的initializer是ParentContextApplicationContextInitializer的实例
  if (initializer instanceof ParentContextApplicationContextInitializer) {
   // 查找引导上下文
   context = findBootstrapContext(
     (ParentContextApplicationContextInitializer) initializer,
     configName);
  }
 }
 // 如果上下文等于null
 if (context == null) {
  // 创建引导上下文
  context = bootstrapServiceContext(environment, event.getSpringApplication(),
    configName);
  event.getSpringApplication()
    .addListeners(new CloseContextOnFailureApplicationListener(context));
 }
 
 // 添加ApplicationContextInitializer并排序,主要是将EnvironmentDecryptApplicationInitializer类型的bean包装成DelegatingEnvironmentDecryptApplicationInitializer并设置order值,以及通过set集合去重
 apply(context, event.getSpringApplication(), environment);
}

详细见代码注释。如果spring.cloud.bootstrap.enabled=true(系统属性或者系统环境变量中配置),在第一次加载的时候context等于null,会进入bootstrapServiceContext方法:

private ConfigurableApplicationContext bootstrapServiceContext(
   ConfigurableEnvironment environment, final SpringApplication application,
   String configName)
 
{
 // 创建Environment
 StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
 // 获取可改变的属性源,默认有systemProperties和systemEnvironment
 MutablePropertySources bootstrapProperties = bootstrapEnvironment
   .getPropertySources();
 // 移除默认添加的2个配置源
 for (PropertySource<?> source : bootstrapProperties) {
  bootstrapProperties.remove(source.getName());
 }
 // 解析环境中是否配置了spring.cloud.bootstrap.location属性
 String configLocation = environment
   .resolvePlaceholders("${spring.cloud.bootstrap.location:}");
 // 解析环境中是否配置了spring.cloud.bootstrap.additional-location属性
 String configAdditionalLocation = environment
   .resolvePlaceholders("${spring.cloud.bootstrap.additional-location:}");
 // 创建map
 Map<String, Object> bootstrapMap = new HashMap<>();
 // 设置spring.config.name属性值为上一步传入的configName,默认:bootstrap
 bootstrapMap.put("spring.config.name", configName);
 // if an app (or test) uses spring.main.web-application-type=reactive, bootstrap
 // will fail
 // force the environment to use none, because if though it is set below in the
 // builder
 // the environment overrides it
 // 设置应用类型为非web类型
 bootstrapMap.put("spring.main.web-application-type""none");
 if (StringUtils.hasText(configLocation)) {
  bootstrapMap.put("spring.config.location", configLocation);
 }
 if (StringUtils.hasText(configAdditionalLocation)) {
  bootstrapMap.put("spring.config.additional-location",
    configAdditionalLocation);
 }
 // 向bootstrap属性源中添加名为bootstrap的属性
 bootstrapProperties.addFirst(
   new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
 // 迭代上一步传入的environment中的配置源
 for (PropertySource<?> source : environment.getPropertySources()) {
  // 如果属性源是StubPropertySource实例(servletConfigInitParams和servletContextInitParams),则继续下一次循环
  if (source instanceof StubPropertySource) {
   continue;
  }
  // 非StubPropertySource实例,添加到bootstraps属性源中,后添加的在最后
  bootstrapProperties.addLast(source);
 }
 // TODO: is it possible or sensible to share a ResourceLoader?
 // 通过SpringApplicationBuilder配置SpringApplication
 SpringApplicationBuilder builder = new SpringApplicationBuilder()
   .profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF)
   .environment(bootstrapEnvironment)
   // Don't use the default properties in this builder
   .registerShutdownHook(false).logStartupInfo(false)
   .web(WebApplicationType.NONE);
 // 通过SpringApplicationBuilder得到SpringApplication
 final SpringApplication builderApplication = builder.application();
 // 如果主应用类等于null
 if (builderApplication.getMainApplicationClass() == null) {
  // gh_425:
  // SpringApplication cannot deduce the MainApplicationClass here
  // if it is booted from SpringBootServletInitializer due to the
  // absense of the "main" method in stackTraces.
  // But luckily this method's second parameter "application" here
  // carries the real MainApplicationClass which has been explicitly
  // set by SpringBootServletInitializer itself already.
  // 则将当前主配置类(即main方法所在的类)设置到新创建的SpringApplication
  builder.main(application.getMainApplicationClass());
 }
 // 是否配置了refreshArgs属性
 if (environment.getPropertySources().contains("refreshArgs")) {
  // If we are doing a context refresh, really we only want to refresh the
  // Environment, and there are some toxic listeners (like the
  // LoggingApplicationListener) that affect global static state, so we need a
  // way to switch those off.
  // 过滤掉LoggingApplicationListener和LoggingSystemShutdownListener类型的监听器
  builderApplication
   .setListeners(filterListeners(builderApplication.getListeners()));
 }
 // 1.添加配置源(重要),一会介绍
 builder.sources(BootstrapImportSelectorConfiguration.class);
 // 启动非web类型的springboot应用,此时会再次进入到BootstrapApplicationListener,由于spring环境中已经有了名为bootstrap的配置,所以不会再次执行上面分析的代码。启动完成后,会继续执行下面的代码。
 final ConfigurableApplicationContext context = builder.run();
 // gh-214 using spring.application.name=bootstrap to set the context id via
 // `ContextIdApplicationContextInitializer` prevents apps from getting the actual
 // spring.application.name
 // during the bootstrap phase.
 // 设置上下文的id
 context.setId("bootstrap");
 // Make the bootstrap context a parent of the app context
 // 2.设置父子上下文
 addAncestorInitializer(application, context);
 // It only has properties in it now that we don't want in the parent so remove
 // it (and it will be added back later)
 // 移除名为bootstrap的属性
 bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
 // 合并父子上下文中相同的属性
 mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
 return context;
}

详细见代码注释。在上面的代码中,我们看到通过builder.sources(BootstrapImportSelectorConfiguration.class)这行代码,向父容器中添加了BootstrapImportSelectorConfiguration配置源。进入这个类看一下:

BootstrapImportSelectorConfiguration

@Configuration(proxyBeanMethods = false)
@Import(BootstrapImportSelector.class)
public class BootstrapImportSelectorConfiguration 
{

}

通过@Import注解导入了BootstrapImportSelector类:

public class BootstrapImportSelector implements EnvironmentAwareDeferredImportSelector

这个类实现了DeferredImportSelector类,这是一个推迟导入选择器(不理解的,请读者自行百度)。它重写了父类的selectImports方法:

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
 ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
 // Use names and ensure unique to protect against duplicates
 // 通过SPI机制,加载spring.factories中属性为BootstrapConfiguration的配置类
 List<String> names = new ArrayList<>(SpringFactoriesLoader
   .loadFactoryNames(BootstrapConfiguration.classclassLoader));
 names.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(
   this.environment.getProperty("spring.cloud.bootstrap.sources"""))));

 List<OrderedAnnotatedElement> elements = new ArrayList<>();
 for (String name : names) {
  try {
   elements.add(
     new OrderedAnnotatedElement(this.metadataReaderFactory, name));
  }
  catch (IOException e) {
   continue;
  }
 }
 AnnotationAwareOrderComparator.sort(elements);

 String[] classNames = elements.stream().map(e -> e.name).toArray(String[]::new);

 return classNames;
}

核心内容就是通过springboot的SPI工厂模式,加载/META-INF/spring.factories文件中,属性为org.springframework.cloud.bootstrap.BootstrapConfiguration的配置类:

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration,\
org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration,\
org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.cloud.util.random.CachedRandomPropertySourceAutoConfiguration

org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration:这个类主要解析加载外部化配置属性

org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration:主要配置文件中前缀为{cipher}的相关解密,熟悉spring-boot-starter-security在springcloud应用的朋友一定不陌生

org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration:主要是监听EnvironmentChangeEvent事件用于刷新@ConfigurationProperties标记的配置

org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration:主要解析配置文件中的${}占位符

关于这些类的作用,就不深入展开了。好了,回到bootstrapServiceContext方法中,在引导上下文启动后,通过addAncestorInitializer(application, context)方法设置父子上下文,进入这个方法看看:

private void addAncestorInitializer(SpringApplication application,
   ConfigurableApplicationContext context)
 
{
 boolean installed = false;
 // 迭代spring上下文中的ApplicationContextInitializer实例
 for (ApplicationContextInitializer<?> initializer : application
   .getInitializers()) {
  // 如果当前的initializer是AncestorInitializer的一个实例
  if (initializer instanceof AncestorInitializer) {
   installed = true;
   // New parent
   // 将引导上下文设置为springboot应用的父上下文
   ((AncestorInitializer) initializer).setParent(context);
  }
 }
 // 第一次因为容器中没有AncestorInitializer类型的initializer
 if (!installed) {
  // 向springboot的上下文中添加AncestorInitializer类型的initializer,并设置父上下文为bootstrap上下文
  application.addInitializers(new AncestorInitializer(context));
 }

}

至此,关于spring cloud引导上下文的主要代码已经分析完毕,剩下的比如合并父子上下文中同名属性相关代码,以及在apply方法中设置ApplicationContextInitializer并去重的相关代码,还请大家自行分析,相信很容易理解。

总结

在spring cloud环境中,通过BootstrapApplicationListener监听ApplicationEnvironmentPreparedEvent事件。配置并启动了一个非web类型的引导上下文,在启动的时候通过设置BootstrapImportSelectorConfiguration类导入并扫描classpath下/META-INF/spring.factoriesorg.springframework.cloud.bootstrap.BootstrapConfiguration为属性名的配置类,装配到引导上下文中,并作为springboot的父上下文,然后合并两个上下文中相同的属性配置以及通过set集合去除父子上下文中重复的ApplicationContextInitializer类型的bean。并且2个上下文共享一个Environment

欢迎关注我的公众号:程序员L札记

更多原创文章,请扫码关注我的微信公众号
更多原创文章,请扫码关注我的微信公众号

lyq

2021/12/19  阅读:33  主题:橙心

作者介绍

lyq