【SpringCloud】Spring-Cloud-Common解析

作者: Weidan 分类: Spring源码 发布时间: 2020-05-10

SpringCloud开篇

那么最近是想要来阅读一下 SpringCloud 的文章的,于是乎逗了几个圈子,又是去 GitHub原生 Eureka 的源码,又是去 Springspring-cloud-netflix 的源码,最后也是基本锁定只要从 spring-cloud-netflix 进去了解 SpringCloud 组件即可,毕竟 原生Eureka 我没用过…

spring-cloud-context项目

那么为啥是从 SpringCloudContext 开始咧,是因为这样的,如果我们某个组织想要开发 SpringCloud 套件的话,就需要使用到 Spring 官方提供的 spring-cloud-commons 项目,这个项目是一个 套件工具包,提供的是官方已经写好的一些注解和工具包,比方说 @EnableDiscoveryClient @LoadBalanced 这些我们常用的 SpringCloud 注解,并且提供了一些少量的支持,其中最重要的莫过于 SpringCloud 的上下文。

那我们之前读过 Spring 的源码的时候了解到,无论是 context 还是 application,都是支持父级容器的,而从容器中取出 Bean实例 的时候,也是 双亲加载机制,如果父级容器有了,那么子级容器是不会重新去加载的,这样我们在设计我们的业务项目的时候,就可以把一些基础架构的 Bean实例 丢到父级容器,并且子级容器只需要加载业务相关的类就可以了,当需要对第三方服务(如:MySQL Redis 都可以称为第三方服务)进行访问的时候,让我们的子级业务容器去父级容器取出来进行使用。有什么好处呢,我感觉就是专业的容器,做专业事情,我们编码弄出那么多设计,不外乎就是为了让程序的 拓展性会更好,当我们的基础服务发生改变的时候,那就可以将父级容器换一个,而不需要去动我们的业务容器。

带着这个想法来了解一下 spring-cloud-context 项目,spring-cloud-context 项目提供了一个容器,名为 bootstrap,那使用过的 spring-cloud 组件搭建过项目的同学肯定想到了我们常见的 bootstrap.yml,没错,这个配置文件就是配置 bootstrap 容器的,当我们在我们的项目中加入类似于 eureka zuul 或者 eureka-client 的时候,我们的项目容器就发生了翻天覆地的变化。

SpringApplication 会先根据 SPI 协议加载 BootstrapApplicationListener 类,并且在初始化 ConfigurableApplicationContext 之前,先执行这个上面的 ApplicationListener 的回调方法,把 Bootstrap容器 给初始化出来,并且设置为当前容器的 parent (典型的 我把你当朋友你居然要做我爸爸)。而如果有 SpringCloud 架构经验的同学肯定也明白一个事情,为啥我们在整合 spring-cloud-config-client 的时候,spring-cloud 的配置内容需要写在 bootstrap.yml 中,当然是因为 bootstrap.yml 是第一个被加载的,然后他获取到了配置以后,再初始化我们自己的容器,这时候我们自己的容器如果需要一些 远程配置 的时候,就可以先从 爸爸 那里去命中了。

而为啥要这样设计呢,说好听点,叫做我们可以随时替换符合 spring-cloud-context 规范的套件,说难听呢,就是为了让自己的市场份额,因为如果我们想替换微服务的提供商,那么符合 spring-cloud-context 规范的,都可以很简便的进行替换,而如果不符合我规范的,很抱歉,你需要自己重写很多东西。

Bootstrap容器的初始化

演示栗子

首先我们需要一个例子,那就用最简单的 EurekaServer 的例子来搭建吧:

首先,pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework.cloud.test</groupId>
    <artifactId>eureka-test</artifactId>
    <version>1.0</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
            <version>3.0.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.10.3</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.5</version>
        </dependency>

    </dependencies>
    <packaging>jar</packaging>
        <!-- 因为使用的是最新版的SpringCloud,还是快照版,很多包在央仓是找不到的,所以需要Spring的快照仓库来配合构建 -->
    <profiles>
        <profile>
            <id>spring</id>
            <repositories>
                <repository>
                    <id>spring-snapshots</id>
                    <name>Spring Snapshots</name>
                    <url>https://repo.spring.io/libs-snapshot-local</url>
                    <snapshots>
                        <enabled>true</enabled>
                    </snapshots>
                    <releases>
                        <enabled>false</enabled>
                    </releases>
                </repository>
                <repository>
                    <id>spring-milestones</id>
                    <name>Spring Milestones</name>
                    <url>https://repo.spring.io/libs-milestone-local</url>
                    <snapshots>
                        <enabled>false</enabled>
                    </snapshots>
                </repository>
                <repository>
                    <id>spring-releases</id>
                    <name>Spring Releases</name>
                    <url>https://repo.spring.io/release</url>
                    <snapshots>
                        <enabled>false</enabled>
                    </snapshots>
                </repository>
            </repositories>
            <pluginRepositories>
                <pluginRepository>
                    <id>spring-snapshots</id>
                    <name>Spring Snapshots</name>
                    <url>https://repo.spring.io/libs-snapshot-local</url>
                    <snapshots>
                        <enabled>true</enabled>
                    </snapshots>
                    <releases>
                        <enabled>false</enabled>
                    </releases>
                </pluginRepository>
                <pluginRepository>
                    <id>spring-milestones</id>
                    <name>Spring Milestones</name>
                    <url>https://repo.spring.io/libs-milestone-local</url>
                    <snapshots>
                        <enabled>false</enabled>
                    </snapshots>
                </pluginRepository>
                <pluginRepository>
                    <id>spring-releases</id>
                    <name>Spring Releases</name>
                    <url>https://repo.spring.io/libs-release-local</url>
                    <snapshots>
                        <enabled>false</enabled>
                    </snapshots>
                </pluginRepository>
            </pluginRepositories>
        </profile>
        <profile>
            <id>sonar</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.jacoco</groupId>
                        <artifactId>jacoco-maven-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>pre-unit-test</id>
                                <goals>
                                    <goal>prepare-agent</goal>
                                </goals>
                                <configuration>
                                    <propertyName>surefireArgLine</propertyName>
                                    <destFile>${project.build.directory}/jacoco.exec</destFile>
                                </configuration>
                            </execution>
                            <execution>
                                <id>post-unit-test</id>
                                <phase>test</phase>
                                <goals>
                                    <goal>report</goal>
                                </goals>
                                <configuration>
                                    <!-- Sets the path to the file which contains the execution data. -->
                                    <dataFile>${project.build.directory}/jacoco.exec</dataFile>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>
</project>

EurekaApplication.java

package eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.core.env.StandardEnvironment;

@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {

  public static void main(String[] args) {
    SpringApplication.run(EurekaApplication.class, args);
  }

  // 需要给个标准的环境才能运行
  @Bean
  public Environment environment() {
    return new StandardEnvironment();
  }

}

回顾下老朋友

看过我 SpringBoot 源码解析的小朋友们应该对下面这个代码会有点熟悉感:

public class SpringApplication {
  public ConfigurableApplicationContext run(String... args) {
    // 这是用来打印加载时间的工具类,暂时可以略过.
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    // 主要设置JVM虚拟机支持无设备情况下让awt可运行的属性
    configureHeadlessProperty();
    // 一. 加载所有SpringApplicationRunListeners并开始遍历所有Lintener启动监听回调函数
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
      // 二. 开始准备ConfigurableEnvironment环境
      // ===> 那么将要看的Bootstrap容器初始化就是在这里被执行的,先拿到所有的listener,然后调用onApplicationEvent方法
      // 在准备 ConfigurableApplicationContext,的时候,顺便准备一下Bootstrap
      ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
      configureIgnoreBeanInfo(environment);
      Banner printedBanner = printBanner(environment);
      // 三. 创建应用上下文
      context = createApplicationContext();
      exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                                                       new Class[] { ConfigurableApplicationContext.class }, context);
      // 四. 做一些准备工作
      prepareContext(context, environment, listeners, applicationArguments, printedBanner);
      // 五. 刷新上下文
      refreshContext(context);
      // 六. 刷新完成后做的一些清理、回调工作
      afterRefresh(context, applicationArguments);
      stopWatch.stop();
      if (this.logStartupInfo) {
        new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
      }
      // 七. 启动完成后,调用所有SpringApplicationRunListener的完成启动的回调函数
      listeners.started(context);
      // 八. 主要处理ApplicationRunner和CommandLineRunner的回调
      callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
      handleRunFailure(context, ex, exceptionReporters, listeners);
      throw new IllegalStateException(ex);
    }

    try {
      // 九. 运行时的SpringApplicationRunListener回调函数
      listeners.running(context);
    }
    catch (Throwable ex) {
      handleRunFailure(context, ex, exceptionReporters, null);
      throw new IllegalStateException(ex);
    }
    return context;
  }
}

那么为什么那么确定就是 Listener 被调用呢,证据确凿:spring-cloud-commons/spring-cloud-context/src/main/resources/META-INF/spring.factories

整合流程详见 Spring_Boot_与容器

# AutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration,\
org.springframework.cloud.autoconfigure.LifecycleMvcEndpointAutoConfiguration,\
org.springframework.cloud.autoconfigure.RefreshAutoConfiguration,\
org.springframework.cloud.autoconfigure.RefreshEndpointAutoConfiguration,\
org.springframework.cloud.autoconfigure.WritableEnvironmentEndpointAutoConfiguration
# Application Listeners,在这里注册了SpringBoot的ApplicationListener
org.springframework.context.ApplicationListener=\
org.springframework.cloud.bootstrap.BootstrapApplicationListener,\
org.springframework.cloud.bootstrap.LoggingSystemShutdownListener,\
org.springframework.cloud.context.restart.RestartListener
# Bootstrap components
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

当然我们需要重温一下 ApplicationListener 有什么生命周期函数以及观望一下 BootstrapApplicationListener

public interface SpringApplicationRunListener {

  // 项目开始时调用
  default void starting() {
  }

  // 环境准备好时,初始化 ConfigurableApplicationContext 的时候会调用到这里,
  default void environmentPrepared(ConfigurableEnvironment environment) {
  }

  // 上下文准备好时
  default void contextPrepared(ConfigurableApplicationContext context) {
  }

  // 上下文读取完成
  default void contextLoaded(ConfigurableApplicationContext context) {
  }

  // 启动完成
  default void started(ConfigurableApplicationContext context) {
  }

  // 项目运行时调用
  default void running(ConfigurableApplicationContext context) {
  }

  // 项目失败时
  default void failed(ConfigurableApplicationContext context, Throwable exception) {
  }

}

public class BootstrapApplicationListener
        implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {
  @Override
  public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
    ConfigurableEnvironment environment = event.getEnvironment();
    // spring.cloud.bootstrap.enabled配置容器的开关,默认是打开的
    if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class,
                                 true)) {
      return;
    }
    // Bootstrap的初始化同样会经过这里,那我们就不能让他递归创建,遇到Bootstrap直接跳过
    if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
      return;
    }
    ConfigurableApplicationContext context = null;
    // 默认bootstrap的配置名:bootstrap
    String configName = environment
      .resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
    // 如果我们初始化的时候已经存在父级容器了,则从父级容器中尝试命中BootstrapContext
    for (ApplicationContextInitializer<?> initializer : event.getSpringApplication()
         .getInitializers()) {
      if (initializer instanceof ParentContextApplicationContextInitializer) {
        context = findBootstrapContext(
          (ParentContextApplicationContextInitializer) initializer,
          configName);
      }
    }
    if (context == null) {
      // ======> 通过SpringApplicationBuilder来构建一个Context上下问
      context = bootstrapServiceContext(environment, event.getSpringApplication(),
                                        configName);
      // 添加一个主Context关闭的监听器,为了能够发生错误的时候同时关闭Bootstrap容器
      event.getSpringApplication()
        .addListeners(new CloseContextOnFailureApplicationListener(context));
    }

    apply(context, event.getSpringApplication(), environment);
  }

  private ConfigurableApplicationContext bootstrapServiceContext(
    ConfigurableEnvironment environment, final SpringApplication application,
    String configName) {
    // 创建一个标准环境配置,带有systemProperties和systemEnvironment的相关配置信息
    StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
    MutablePropertySources bootstrapProperties = bootstrapEnvironment
      .getPropertySources();
    // 开始整理Bootstrap所需要的配置,移除systemProperties和systemEnvironment
    for (PropertySource<?> source : bootstrapProperties) {
      bootstrapProperties.remove(source.getName());
    }
    String configLocation = environment
      .resolvePlaceholders("${spring.cloud.bootstrap.location:}");
    String configAdditionalLocation = environment
      .resolvePlaceholders("${spring.cloud.bootstrap.additional-location:}");
    Map<String, Object> bootstrapMap = new HashMap<>();
    bootstrapMap.put("spring.config.name", configName);
    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);
    }
    bootstrapProperties.addFirst(
      new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
    for (PropertySource<?> source : environment.getPropertySources()) {
      if (source instanceof StubPropertySource) {
        continue;
      }
      bootstrapProperties.addLast(source);
    }
    // 通过SpringApplicationBuilder构建一个船新的Context出来
    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);
    final SpringApplication builderApplication = builder.application();
    if (builderApplication.getMainApplicationClass() == null) {
      builder.main(application.getMainApplicationClass());
    }
    if (environment.getPropertySources().contains("refreshArgs")) {
      // 过滤掉在刷新环境的时候,会影响到全局状态的Listener,如 LoggingApplicationListener
      builderApplication
        .setListeners(filterListeners(builderApplication.getListeners()));
    }
    builder.sources(BootstrapImportSelectorConfiguration.class);
    // ====> 构建BootstrapContext,这时候要重复我们上面说到的Context的加载过程
    // 我们必须清楚这一步加载了什么BeanDefinition
    final ConfigurableApplicationContext context = builder.run();
    context.setId("bootstrap");
    // 然后将BootstrapContext设置为当前context的父级容器
    addAncestorInitializer(application, context);
    // 先移除掉Bootstrap的配置,后面会被加回去
    bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
    mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
    return context;
  }

}

到这里我们需要用一点言语来概括一下上下文初始化的过程:

  1. 我们所启动的 ApplicationContext,在准备加载的时候,调用了模块中定义的 ApplicationListener,也就是 spring-cloud-context 包中定义的 BootstrapApplicationListener
  2. BootstrapApplicationListener 开始根据 ApplicationContext 获取的一系列配置,使用 SpringApplicationBuilder 构建开始 BootstrapContext,并且通过 AncestorInitializer 配置好两个上下文的父子关系;
  3. SpringApplicationBuilder 调用 run() 函数,刷新容器中的 Bean实例
  4. 那接下来我们就需要重新进入 SpringBoot 容器的加载流程来瞅一瞅到底加载了什么

Bootstrap容器的加载

那之前在说 Spring 的时候有说过,Spring 加载 Bean 的时候是会根据一些规则,比如 @Configuration 或者 ImportSelector子类,用于查询需要导入的 配置Bean,整合进框架的方法莫过于 spring.factories 文件去定义。对应的生命周期子类将会在不同的时期被执行。

那么上面使用 SpringApplicationBuilder 构造 BootstrapContext 之前呢,通过 builder.sources(BootstrapImportSelectorConfiguration.class);Bootstrap 配置信息给加载进去,而这个类:

@Configuration(proxyBeanMethods = false)
@Import(BootstrapImportSelector.class) // 导入了一个BootstrapImportSelector的 ImportSelector 处理器
public class BootstrapImportSelectorConfiguration {}

// 导入Bootstrap配置类的选择器
public class BootstrapImportSelector implements EnvironmentAware, DeferredImportSelector {

    private Environment environment;

    private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory();

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        // Use names and ensure unique to protect against duplicates
        List<String> names = new ArrayList<>(SpringFactoriesLoader
                .loadFactoryNames(BootstrapConfiguration.class, classLoader));
        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;
    }
  //......
}

首先需要先来聊聊 ImportSelector,这个后处理器会在解析配置的时候被调用到,而调用他主要是用来加载我们整合框架的时候,需要使用到的一些特殊配置,那么看到上面的 BootstrapImportSelector,他支持将 BootstrapConfiguration 类(实际上使用了最简单的 注解 来表示一个类),所以 spring.factories 写了 org.springframework.cloud.bootstrap.BootstrapConfiguration=/xxx 的类即可被 SpringFactoriesLoader 读取到。

那这个类呢,就是将 spring.factories 定义的 BootstrapConfiguration 类加载到容器中,并且应用其中的设置。

那么目前加载的 Bean 就有:

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.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration

最后一个是 Eureka 加载的发现服务的客户端配置类。

那为啥我依赖个 @EnableEurekaServer 而给我注入的是一个 客户端 的配置呢,那是因为,Eureka 的设计中,Core 是用来存储实例的,而服务端和客户端存储的实例设计都是一样,那在 SpringCloud 项目中,服务端又可以相互注册,所以 EurekaServer 实际也是一个 EurekaClient

那么上面加载到容器中的类,那么很明显,在加载 Bootstrap 容器的时候,会被读取到上面那些类,上面那些类,有 ApplicationContextInitializer @Configuration 等等,应有尽有,主要都是用于在不同容器生命周期发光发亮的处理类。然后我们也说过,在 SpringBoot 使用的 AnnotationConfigApplicationContext 上下文中,所有的非惰性 Bean 在刷新的最后都会被进行一次 加载,所以上面配置中所有的 Bean 都会被初始化。

而这些 Bean,就是我们常用的 配置从config-server读取 配置解密 的关键,这些放在后面再来阅读。

那么现在总结一下,spring-cloud-context 有什么用呢,最主要的一点就是提供了一个 Bootstrap上下文SpringCloud 的组件们,就是在这个上下文中进行工作的,而我们的 业务上下文,依然存在于我们创建的上下文中,当需要用到一些比如调用第三方服务的工具类的时候,就会从 spring-cloud-context 中取出来使用,而 套件 则会利用这个上下文的工具类,来提供更加便利的使用。

spring-cloud-commons项目

这个包,提供了一系列的关于微服务的 类定义,如果 SpringCloud 套件的开发者遵守这套规范的话,那我们是可以在不同的 套件 之间来回切换的,比方说目前市面上存在 SpringCloudNetflixSpringCloudAlibaba,而我们在使用其中一套的时候,如果注解用的是 spring-cloud-commons 的规范注解,则切换套件的任务将会变得十分简单。

下一节聊一聊 eureka-server 了。