spring-cloud-context源码解读
- 1. The Bootstrap Application Context
- 2. Application Context Hierarchies
- 3. Changing the Location of Bootstrap Properties
- 4. Overriding the Values of Remote Properties
- 5. Customizing the Bootstrap Configuration
- 6. Customizing the Bootstrap Property Sources
- 7. Logging Configuration
- 8. Environment Changes
- 9. Refresh Scope
- 10. Encryption and Decryption
- 11. Endpoints
- References
在对spring-cloud-commons reference 中 spring-cloud-context 相关的内容进行翻译的基础上,为了便于理解,加入了源码解读。水平有限,欢迎指正。
1. The Bootstrap Application Context
Spring Cloud 应用会创建一个 bootstrap
上下文, 它是主应用的 父 上下文。 它的职责是 从外部资源加载配置属性以及解码本地的配置文件。 这两个上下文 共享一个 Environment,对任何Spring应用而言, Environment 是所有外部属性的源头。默认情况下, bootstrap 属性(并非是 bootstrap.properties 文件, 而是在bootstrap阶段加载的属性)具有较高的优先级, 因此它们不会被本地配置所覆盖
。
与 main
应用上下文不同, bootstrap 上下文使用不同的规约定位外部配置 。 取代 application.yml (或 .properties),你可以使用 bootstrap.yml, 这保持了 bootstrap 和 main 上下文 外部配置 的完美隔离。 下面是一个例子:
bootstrap.yml
spring:
application:
name: foo
cloud:
config:
uri: ${SPRING_CONFIG_URI:http://localhost:8888}
如果你的应用需要从服务器获取一些 特定于应用 的配置, 设定 spring.application.name (在 bootstrap.yml 或 application.yml 中) 会是一个不错的主意。如果要使用属性 spring.application.name 的值作为 应用上下文的ID, 那么你必须将它设置在 bootstrap.[properteis | yml]。
如果要完全禁用 bootstrap 处理过程, 可以设置环境变量 spring.cloud.bootstrap.enabled=false(例如,在 system properties 里)。
Understanding source code
默认配置下,Spring Cloud 应用启动后存在两个 ApplicationContext 。一个ID为 application,另一个为 bootstrap,bootstrap 为 application 的父容器
。bootstrap 容器主要负责从外部资源加载配置属性以及解码本地外部配置文件。
application 容器和 bootstrap 容器使用不同的配置方式。 application 容器通过 application.yml 配置,而 bootstrap 通过 bootstrap.yml 配置
。这么看来其实他们的配置方式还是挺一致的,即 {ContextID}.yml 。
那么 bootstrap 处理过程是怎样的呢?
step 1. 在 application 容器的初始化过程中会准备 环境变量 Environment。待 Environment 准备完毕后,会发送 ApplicationEnvironmentPreparedEvent
事件。
step 2. 在ApplicationEnvironmentPreparedEvent的事件监听器中,有一个监听器为 BootstrapApplicationListener
, 创建 bootstrap 容器的任务就是它来完成的。bootstrap context 的创建过程与 application context 并无二致,只是配置参数不同而已。
step 3. 完成 bootstrap 容器创建后,与 application 对应的 SpringApplication 中注册了 AncestorInitializer
,它的作用主要是设置 application 的父容器为 bootstrap
。此时并未设置。
step 4. application 容器的初始化阶段,调用 AncestorInitializer ,设置 application 的 parent 为 bootstrap
。
其核心代码可参考:
org.springframework.cloud.bootstrap.BootstrapApplicationListener#bootstrapServiceContext
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:}");
Map<String, Object> bootstrapMap = new HashMap<>();
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
bootstrapMap.put("spring.main.web-application-type", "none");
if (StringUtils.hasText(configLocation)) {
bootstrapMap.put("spring.config.location", configLocation);
}
bootstrapProperties.addFirst(
new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
for (PropertySource<?> source : environment.getPropertySources()) {
if (source instanceof StubPropertySource) {
continue;
}
bootstrapProperties.addLast(source);
}
// TODO: is it possible or sensible to share a ResourceLoader?
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) {
// 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.
builder.main(application.getMainApplicationClass());
}
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.
builderApplication
.setListeners(filterListeners(builderApplication.getListeners()));
}
builder.sources(BootstrapImportSelectorConfiguration.class);
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.
context.setId("bootstrap");
// Make the bootstrap context a parent of the app context
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)
bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
return context;
}
2. Application Context Hierarchies
如果你的应用是通过 SpringApplication 或 SpringApplicationBuilder 创建的, 那么 Bootstrap 容器将会添加为该容器的父容器。 子容器继承父容器的 属性源 和 profiles 是Spring的一个特性
, 因此 application 容器会包含附加属性源。 这些附加属性源包括:
bootstrap
: 如果在 Bootstrap 容器中发现 PropertySourceLocators 并且包含非空属性的话,会出现一个高优先级的可选CompositePropertySource 属性源。一个例子是从 Spring Cloud Config Server 获取的属性。Customizing the Bootstrap Property Sources
一节说明了如何个性化这个属性源的内容。applicationConfig [classpath:bootstrap.yml]
(如果Spring profiles功能激活的话,还包含 profile 相关的文件):如果你有一个 bootstrap.yml (或 .properties), 文件配置的属性将用于配置 Bootstrap 上下文。 在为子容器设置父容器时,这些属性源会添加到子容器中。这些属性源比 application.yml(或 .properties) 以及 任其他添加到子容器的属性源 的优先级要低
。Changing the Location of Bootstrap Properties
一节说明了如何个性化这些属性源的内容。
Related source:
org.springframework.cloud.bootstrap.BootstrapApplicationListener#mergeDefaultProperties
3. Changing the Location of Bootstrap Properties
bootstrap.yml 文件的路径可以通过以下两个参数指定,你可以在系统属性中指定:
- spring.cloud.bootstrap.name :bootstrap文件的名字,默认为bootstrap。
- spring.cloud.bootstrap.location :bootstrap文件的路径,默认为空。
4. Overriding the Values of Remote Properties
通过 bootstrap context 添加到应用的属性源通常都是*”remote“(如通过Spring Cloud配置服务器)。默认情况下属性不会被本地配置*覆盖。如果你希望应用通过系统属性或配置文件的方式覆盖这些远程属性,远程属性源必须通过设置 spring.cloud.config.allowOverride=true 进行授权(本地配置不生效)。一旦设置了该属性,还可以通过两个更详细的配置来控制远程属性与系统属性和本地配置文件的定位。
- spring.cloud.config.overrideNone=true: 从本地属性源覆盖
- spring.cloud.config.overrideSystemProperties=false:仅系统属性、命令行参数和环境变量(非本地配置文件)可以覆盖远程配置
Understanding source code
remote可以简单理解为所有类型为 PropertySourceLocator 的Bean。
本地配置可以理解为本地配置文件。
相关代码:
org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration#initialize
org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration#insertPropertySources
5. Customizing the Bootstrap Configuration
可以通过设置 bootstrap context 来做任何你想做的事情, 你需要在/META-INF/spring.factories文件中添加key为org.springframework.cloud.bootstrap.BootstrapConfiguration,值为逗号分隔的、Spring @Configuration注解的类即可。这些类将用来创建上下文。你希望在 application context 中自动装配的任何bean都可以在这里创建。并且该上下文中还有一个ApplicationContextInitializer类型的@Beans特殊规范。如果想要控制启动顺序(默认顺序为”last“),还可以使用@Order注解。
注意:在添加个性化BootstrapConfiguration时,需要注意添加的类不能被@ComponentScan扫描到,否则他们就会误添加到 application context 中,而 application context 可能根本不需要这些配置。为你的BootstrapConfiguration配置类指定单独的 package ,这个 package 既不在@ComponentScan配置的扫描包内,也不在 @SpringBootApplication 注解的类所在的包内。
bootstrap的处理流程会在 所有 initializer 注入到 SpringApplication 实例时结束(无论是作为独立的应用还是发布为应用服务器,这都是Spring Boot 启动的正常流程)。首先,bootstrap context 会根据 spring.factories 发现的类被创建。然后,所有ApplicationContextInitializer类型的@Bean 会 在应用启动前 被添加到 SpringApplication。
Understanding source code
在创建bootstrap过程中有句代码非常重要,它的作用是将 key org.springframework.cloud.bootstrap.BootstrapConfiguration 的所有value,加载并注册为 bean ,这样在 bootstrap 启动时才会完成你想要做的事情。
org.springframework.cloud.bootstrap.BootstrapApplicationListener#bootstrapServiceContext
builder.sources(BootstrapImportSelectorConfiguration.class);
6. Customizing the Bootstrap Property Sources
在bootstrap处理过程中 添加的默认外部配置属性源 是 Spring Cloud 配置服务器,但是你可以通过 添加 PropertySourceLocator 类型的 bean 到 bootstrap context 来添加额外的数据源(通过spring.factories配置文件)。举例来说,你可以从一个服务器或者数据库来添加额外属性。
作为示例,考虑下面的个性化定位器:
@Configuration
public class CustomPropertySourceLocator implements PropertySourceLocator {
@Override
public PropertySource<?> locate(Environment environment) {
return new MapPropertySource("customProperty",
Collections.<String, Object>singletonMap("property.from.sample.custom.source", "worked as intended"));
}
}
入参是为即将创建的 ApplicationContext 准备的 Environment ,我们新添加的 附加属性源 也会添加到 该 Environment 。 该 Environment 已经包含 Spring Boot 提供的属性源, 因此你可以使用这些数据源 来定位一个 特定于 Environment 的属性源(如,Spring Cloud Config Sever 属性源定位器,默认就是通过设置 spring.application.name 的值来实现的 )。
如果你创建了一个包含这个类的jar包, 并且在 META-INF/spring.factories 文件中包含如下配置, 名为 customProperty 的属性源 会出现在 classpath 中包含该 jar 包的任何一个应用中。
org.springframework.cloud.bootstrap.BootstrapConfiguration=sample.custom.CustomPropertySourceLocator
Understanding source code
org.springframework.cloud.bootstrap.BootstrapApplicationListener#bootstrapServiceContext 方法中有如下语句,该语句将 application context ID 强行设置为 bootstrap , 开始的默认值为 application。
// 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.
context.setId("bootstrap");
7. Logging Configuration
如果你像使用 Spring Boot 进行日志设置, 并且希望将配置应用于所有时间, 则应将此配置放在 bootstrap.[yml | properties]。
请注意, 为了使Spring Cloud正确初始化日志配置,您不能使用自定义前缀。 例如,初始化日志记录系统时,Spring Cloud将无法识别配置custom.loggin.logpath。
8. Environment Changes
应用 监听 EnvironmentChangeEvent 事件 并且 以几种标准方式 对变更做出反应 (用户可以通过常规方式 将其他的 ApplicationListeners 作为 @Beans 添加)。 监听 到 EnvironmentChangeEvent 事件时, 它包含发生变更 的键值的列表, 应用程序可以使用它们来:
- 重新绑定上下文中任何 @ConfigurationProperties bean
- 为 logging.level.* 中的任何属性 设置日志级别
请注意,默认情况下, Config Client 不轮询 Environment 中的变更。 通常,我们不建议您使用这种方法来检测变更(尽管您可以使用@Scheduled 注解对其进行设置)。如果您的客户端应用是横向扩展的,最好将 EnvironmentChangeEvent 事件广播给所有实例, 而不是让他们轮询获取变更。(例如,使用 Spring Cloud Bus)。
EnvironmentChangeEvent 时间覆盖了一大类刷新用例,你可以改变 Environment 并发布 EnvironmentChangeEvent 时间。请注意,这些API是公共的,并且是核心Spring的一部分。 您可以通过访问 /configprops 端点(正常的 Spring Boot Actuator 功能)来验证更改是否绑定到 @ConfigurationProperties Bean。 例如,一个数据源可以在运行时改变其 maxPoolSize (Spring Boot 创建的默认数据源是一个 @ConfigurationProperties Bean) 并 动态的增加容量。 重新绑定 @ConfigurationProperties 并不涵盖另一大类用例,在这些用例中,您需要对刷新进行更多控制并且对整个 ApplicationContext 进行原子变更。 为了解决这个问题,我们提供了 @RefreshScope。
Understanding source code
ConfigurationPropertiesRebinderAutoConfiguration
ConfigurationPropertiesBeans:
发现并存储所有@ConfigurationProperties注解的Bean
ConfigurationPropertiesRebinder:
当EnvironmentChangeEvent发生时,重新绑定所有的@ConfigurationProperties Bean
LifecycleMvcEndpointAutoConfiguration
EnvironmentManager:
本地配置属性管理,可以通过org.springframework.cloud.context.environment.EnvironmentManager#setProperty添加或修改属性,修改后触发EnvironmentChangeEvent事件。
9. Refresh Scope
当发生配置变更时,标记为@RefreshScope的Spring @Bean 会被特殊处理。 此功能解决了状态Bean的问题,他们仅在初始化时进行配置注入。 例如, 如果通过Environment改变数据库URL时, DataSource有打开的连接,您可能希望连接持有者能够继续完成他们的工作。然后,下次从连接池获取连接时,它将获取一个新的URL。
有时,甚至可能强制将@RefreshScope注解应用到只能初始化一次的某些Bean。如果bean是不可变的, 则必须用 @RefreshScope 注解 bean 或者 在属性 spring.cloud.refresh.extra-refreshable 下指定类名。
警告:如果您自己创建了一个DataSource,并且实现是 HikariDataSource, 返回最特定的类型,这种情况下是 HikariDataSource。 否则您需要设置 spring.cloud.refresh.extra-refreshable=javax.sql.DataSource 。
刷新作用域的beans是惰性代理,他们在使用时(即方法调用时)进行初始化,并且作用域表现为初始值的缓存。 要强制 bean 在下一个方法调用上重新初始化, 必须使它的缓存条目无效。
RefreshScope是上下文中的一个bean,并且有公共的 refreshAll() 方法以通过清除目标缓存 来刷新作用域内的所有bean。 /refresh 端点公开了该功能(通过HTTP或JMX)。要通过名称刷新单个 bean, 还有一个 refresh(String) 方法。
要公开 /refresh 端点,您需要在应用中添加以下配置:
management:
endpoints:
web:
exposure:
include: refresh
请注意:
@RefreshScope(在技术上)工作在@Configuration类上,但它可能导致令人惊讶的行为。 例如, 这并不意味着该类中定义所有@Bean本身都位于@RefreshScope 中。 具体来说, 除非组件本身在@RefreshScope中,否则任何依赖于这些bean的组件都不会在 刷新初始化时 被更新。在这种情况下,他们将在 刷新 时被重建并且他们的依赖将被重新注入。在这点上,他们是基于 被刷新的@Configuration 进行重新初始化。
上面的这段话不好理解,获取从RefreshScope的JavaDoc中可以得到更明确的解释。
org.springframework.cloud.context.config.annotation.RefreshScope JavaDoc
Convenience annotation to put a @Bean definition in refresh scope. Beans annotated this way can be refreshed at runtime and any
components that are using them will get a new instance on the next method call, fully initialized and injected with all dependencies.
Understanding source code
refresh endpoint
refresh 动作最终会调用 org.springframework.cloud.context.refresh.ContextRefresher#refresh
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment();
this.scope.refreshAll();
return keys;
}
该方法也可以通过发布 RefreshEvent 事件触发。
10. Encryption and Decryption
Spring Cloud有一个用于本地解密属性的 Environment 预处理器。它遵循与 Config Server 相同的规则,并通过 encrypt.* 具有相同的外部配置。因此,你可以使用 {cipher}* 形式的加密值,并且只要存在有效密钥,就可以在 main 应用程序上下文获取 Environment 之前对他们进行解密。 要在应用中使用加密功能,你需要在类路径中包含 Spring Security RSA (Maven 坐标:“org.springframework.security:spring-security-rsa”),并且还需要在您的JVM中包含完整的 JCE 扩展。
如果由于 “Illegal key size” 导致异常并且使用的Sun的JDK。你需要安装Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files。
将文件压缩到您所使用的 JRE/JDK x64/x86 版本的 JDK/jre/lib/security 文件夹。
11. Endpoints
对于 Spring Boot Actuator 应用程序, 可以使用一些其他的管理端点。 你可以使用
- 发送post请求到 /actuator/env 以更新 Environment 并 重新绑定 @ConfigurationProperties 和 日志级别
- 访问 /actuator/refresh 以 重新加载 bootstrap 上下文 并刷新 @RefreshScope bean
- 访问 /actuator/restart 以 关闭 ApplicationContext 并 重新启动它。 (默认禁用)
- 访问 /actuator/pause 和 /actuator/resume 以 调用 ApplicationContex 的生命周期方法。(stop() 和 start())
请注意:如果禁用 /actuator/restart 端点,那么 /actuator/pause 和 /actuator/resume 端点也将被禁用, 因为他们只是 /actuator/restart 的特例。
References
spring cloud系列一:Cloud Native Applications
1. Spring Cloud Context: Application Context Services
spring-cloud-commons reference
Spring Cloud Config 动态刷新实现机制