【SpringCloud】Spring-Cloud-Common解析

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

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<>();
// 主要设置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_与容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 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

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() {
}

// 环境准备好时,初始化 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 配置信息给加载进去,而这个类:

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) // 导入了一个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 就有:

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 套件的开发者遵守这套规范的话,那我们是可以在不同的 套件 之间来回切换的,比方说目前市面上存在 SpringCloudNetflixSpringCloudAlibaba,而我们在使用其中一套的时候,如果注解用的是 spring-cloud-commons 的规范注解,则切换套件的任务将会变得十分简单。 下一节聊一聊 eureka-server 了。