SpringCloud开篇
那么最近是想要来阅读一下 SpringCloud
的文章的,于是乎逗了几个圈子,又是去 GitHub
下 原生 Eureka
的源码,又是去 Spring
下 spring-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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
| <?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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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
源码解析的小朋友们应该对下面这个代码会有点熟悉感:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| public class SpringApplication { public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context = null; Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); configureHeadlessProperty(); SpringApplicationRunListeners listeners = getRunListeners(args); listeners.starting(); try { ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); 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); } listeners.started(context); callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); }
try { 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_与容器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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
org.springframework.context.ApplicationListener=\ org.springframework.cloud.bootstrap.BootstrapApplicationListener,\ org.springframework.cloud.bootstrap.LoggingSystemShutdownListener,\ org.springframework.cloud.context.restart.RestartListener
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
| public interface SpringApplicationRunListener {
default void starting() { }
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(); if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class, true)) { return; } if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) { return; } ConfigurableApplicationContext context = null; String configName = environment .resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}"); for (ApplicationContextInitializer<?> initializer : event.getSpringApplication() .getInitializers()) { if (initializer instanceof ParentContextApplicationContextInitializer) { context = findBootstrapContext( (ParentContextApplicationContextInitializer) initializer, configName); } } if (context == null) { context = bootstrapServiceContext(environment, event.getSpringApplication(), configName); event.getSpringApplication() .addListeners(new CloseContextOnFailureApplicationListener(context)); }
apply(context, event.getSpringApplication(), environment); }
private ConfigurableApplicationContext bootstrapServiceContext( ConfigurableEnvironment environment, final SpringApplication application, String configName) { StandardEnvironment bootstrapEnvironment = new StandardEnvironment(); MutablePropertySources bootstrapProperties = bootstrapEnvironment .getPropertySources(); 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 builder = new SpringApplicationBuilder() .profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF) .environment(bootstrapEnvironment) .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")) { builderApplication .setListeners(filterListeners(builderApplication.getListeners())); } builder.sources(BootstrapImportSelectorConfiguration.class); final ConfigurableApplicationContext context = builder.run(); context.setId("bootstrap"); addAncestorInitializer(application, context); bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME); mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties); return context; }
}
|
到这里我们需要用一点言语来概括一下上下文初始化的过程:
- 我们所启动的
ApplicationContext
,在准备加载的时候,调用了模块中定义的 ApplicationListener
,也就是 spring-cloud-context
包中定义的 BootstrapApplicationListener
;
-
BootstrapApplicationListener
开始根据 ApplicationContext
获取的一系列配置,使用 SpringApplicationBuilder
构建开始 BootstrapContext
,并且通过 AncestorInitializer
配置好两个上下文的父子关系;
-
SpringApplicationBuilder
调用 run()
函数,刷新容器中的 Bean实例
。
- 那接下来我们就需要重新进入
SpringBoot
容器的加载流程来瞅一瞅到底加载了什么
Bootstrap容器的加载
那之前在说 Spring
的时候有说过,Spring
加载 Bean
的时候是会根据一些规则,比如 @Configuration
或者 ImportSelector子类
,用于查询需要导入的 配置Bean
,整合进框架的方法莫过于 spring.factories
文件去定义。对应的生命周期子类将会在不同的时期被执行。 那么上面使用 SpringApplicationBuilder
构造 BootstrapContext
之前呢,通过 builder.sources(BootstrapImportSelectorConfiguration.class);
把 Bootstrap
配置信息给加载进去,而这个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| @Configuration(proxyBeanMethods = false) @Import(BootstrapImportSelector.class) public class BootstrapImportSelectorConfiguration {}
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(); 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
就有:
1 2 3 4 5 6
| 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
套件的开发者遵守这套规范的话,那我们是可以在不同的 套件
之间来回切换的,比方说目前市面上存在 SpringCloudNetflix
和 SpringCloudAlibaba
,而我们在使用其中一套的时候,如果注解用的是 spring-cloud-commons
的规范注解,则切换套件的任务将会变得十分简单。 下一节聊一聊 eureka-server
了。