Clarify role of spring.application.name and context hierarchies

Also allow override of bootstrap properties location using
spring.cloud.config.*.

Fixes gh-22
This commit is contained in:
Dave Syer
2014-10-29 14:59:48 +00:00
parent d564e8dda0
commit ec0108f867
11 changed files with 189 additions and 36 deletions

View File

@@ -1,12 +1,11 @@
require 'asciidoctor'
require 'erb'
require './src/main/ruby/readme.rb'
options = {:mkdirs => true, :safe => :unsafe, :attributes => 'linkcss'}
guard 'shell' do
watch(/^src\/[A-Za-z].*\.adoc$/) {|m|
SpringCloud::Build.render_file('src/main/asciidoc/README.adoc', :to_file => './README.adoc')
Asciidoctor.render_file('src/main/asciidoc/spring-cloud-config.adoc', options.merge(:to_dir => 'target/generated-docs'))
watch(/^docs\/[A-Za-z].*\.adoc$/) {|m|
Asciidoctor.load_file('docs/src/main/asciidoc/README.adoc', :to_file => './README.adoc', safe: :safe, parse: false)
Asciidoctor.render_file('docs/src/main/asciidoc/spring-cloud-config.adoc', options.merge(:to_dir => 'target/generated-docs'))
}
end

View File

@@ -25,13 +25,19 @@ via a JSON endpoint. The service has resources in the form:
----
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
----
where the "application" is injected as the "spring.config.name" in the
`SpringApplication` (i.e. what is normally "application" in a regular
Spring Boot app), "profile" is an active profile (or comma-separated
list of properties), and "label" is an optional git label (defaults to
"master").
"master".) The YAML and properties forms are coalesced into a single
map, even if the origin of the values (reflected in the
"propertySources" of the "standard" form) has multiple sources.
=== Client Side Usage

View File

@@ -253,6 +253,21 @@ For a Spring Boot Actuator application there are some additional management endp
* `/restart` for closing the `ApplicationContext` and restarting it (disabled by default)
* `/pause` and `/resume` for calling the `Lifecycle` methods (`stop()` and `start()` on the `ApplicationContext`)
=== Locating Remote Configuration Resources
The Config Service serves property sources from `/{name}/{env}/{label}`, where the default bindings are
* "name" = `${spring.application.name}`
* "env" = `${spring.profiles.active}` (actually `Environment.getActiveProfiles()`)
* "label" = "master"
All of them can be overridden by setting `spring.cloud.config.\*`
(where `*` is "name", "env" or "label"). The "label" is useful for
rolling back to previous versions of configuration; with the default
Config Server implementation it can be a git label, branch name or
commit id.
=== The Bootstrap Application Context
The Config Client operates by creating a "bootstrap" application
@@ -280,14 +295,75 @@ spring:
uri: ${SPRING_CONFIG_URI:http://localhost:8888}
----
It is a good idea to set the `spring.application.name` in
`bootstrap.yml` if your application needs any application-specific
configuration from the server.
It is a good idea to set the `spring.application.name` (in
`bootstrap.yml` or `application.yml`) if your application needs any
application-specific configuration from the server.
You can disable the bootstrap process completely by setting
`spring.cloud.bootstrap.enabled=false` (e.g. in System properties).
=== Customizing the Bootstrap
=== Application Context Hierarchies
If you build an application context from `SpringApplication` or
`SpringApplicationBuilder`, then the Bootstrap context is added as a
parent to that context. It is a feature of Spring that child contexts
inherit property sources and profiles from their parent, so the "main"
application context will contain additional property sources, compared
to building the same context without Spring Cloud Config. The
additional property sources are:
* "bootstrap": an optional `CompositePropertySource` appears with high
priority if any `PropertySourceLocators` are found in the Bootstrap
context, and they have non-empty properties. An example would be
properties from the Spring Cloud Config Server. See
link:#customizing-bootstrap-property-sources[below] for instructions
on how to customize the contents of this property source.
* "applicationConfig: [classpath:bootstrap.yml]" (and friends if
Spring profiles are active). If you have a `bootstrap.yml` (or
properties) then those properties are used to configure the Bootstrap
context, and then they get added to the child context when its parent
is set. They have lower precedence than the `application.yml` (or
properties) and any other property sources that are added to the child
as a normal part of the process of creating a Spring Boot
application. See link:#customizing-bootstrap-properties[below] for
instructions on how to customize the contents of these property
sources.
Because of the ordering rules of property sources the "bootstrap"
entries take precedence, but note that these do not contain any data
from `bootstrap.yml`, which has very low precedence, but can be used
to set defaults.
You can extend the context hierarchy by simply setting the parent
context of any `ApplicationContext` you create, e.g. using its own
interface, or with the `SpringApplicationBuilder` convenience methods
(`parent()`, `child()` and `sibling()`). Note that the
`SpringApplicationBuilder` allows you to share an `Environment`
amongst the whole hierarchy, but that is not the default. Thus,
normally you expect to see differences between different levels in the
hierarchy, and sibling contexts in particular do not need to have the
same profiles or property sources, even though they will share common
things with their parent. Every context in the hierarchy will have its
own "bootstrap" property source (possibly empty) to avoid promoting
values inadvertently from parents down to their descendants.
[[customizing-bootstrap-properties]]
=== Changing the Location of Bootstrap Properties
The `bootstrap.yml` (or `.properties) location can be specified using
`spring.cloud.bootstrap.name` (default "bootstrap") or
`spring.cloud.bootstrap.location` (default empty), e.g. in System
properties. Those properties behave like the `spring.config.*`
variants with the same name, in fact they are used to set up the
bootstrap `ApplicationContext` by setting those properties in its
`Environment`. If there is an active profile (from
`spring.profiles.active` or through the `Environment` API in the
context you are building) then properties in that profile will be
loaded as well, just like in a regular Spring Boot app, e.g. from
`bootstrap-development.properties` for a "development" profile.
=== Customizing the Bootstrap Configuration
The bootstrap context can be trained to do anything you like by adding
entries to `/META-INF/spring.factories` under the key
@@ -306,7 +382,8 @@ classes found in `spring.factories` and then all `@Beans` of type
`ApplicationContextInitializer` are added to the main
`SpringApplication` before it is started.
=== Customizing the Property Sources
[[customizing-bootstrap-property-sources]]
=== Customizing the Bootstrap Property Sources
The default property source for external configuration added by the
bootstrap process is the Config Server, but you can add additional

View File

@@ -37,7 +37,7 @@ import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor;
import org.springframework.cloud.bootstrap.config.ConfigServiceBootstrapConfiguration;
import org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration;
import org.springframework.cloud.config.client.RefreshEndpoint;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.context.environment.EnvironmentManager;
@@ -179,7 +179,7 @@ public class RefreshAutoConfiguration {
@Configuration
@ConditionalOnExpression("${endpoints.refresh.enabled:true}")
@ConditionalOnBean(ConfigServiceBootstrapConfiguration.class)
@ConditionalOnBean(PropertySourceBootstrapConfiguration.class)
protected static class RefreshEndpointConfiguration {
@Bean

View File

@@ -18,7 +18,9 @@ package org.springframework.cloud.bootstrap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.boot.SpringApplication;
@@ -38,6 +40,7 @@ import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* A listener that prepares a SpringApplication (e.g. populating its Environment) by
@@ -53,6 +56,8 @@ import org.springframework.util.ClassUtils;
public class BootstrapApplicationListener implements
ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {
public static final String BOOTSTRAP_PROPERTY_SOURCE_NAME = "bootstrap";
public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 5;
private int order = DEFAULT_ORDER;
@@ -84,27 +89,37 @@ public class BootstrapApplicationListener implements
for (PropertySource<?> source : bootstrapProperties) {
bootstrapProperties.remove(source.getName());
}
String configName = environment
.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
String configLocation = environment
.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
Map<String, Object> bootstrapMap = new HashMap<String, Object>();
bootstrapMap.put("spring.config.name", configName);
if (StringUtils.hasText(configLocation)) {
bootstrapMap.put("spring.config.location", configName);
}
bootstrapMap.put("spring.application.name", configName);
bootstrapProperties.addFirst(new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
bootstrapProperties.addFirst(new MapPropertySource("bootstrapInProgress",
Collections.<String, Object> emptyMap()));
for (PropertySource<?> source : environment.getPropertySources()) {
bootstrapProperties.addLast(source);
}
bootstrapProperties.addFirst(new MapPropertySource("bootstrap", Collections
.<String, Object> singletonMap("spring.config.name", "bootstrap")));
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// Use names and ensure unique to protect against duplicates
List<String> names = SpringFactoriesLoader.loadFactoryNames(
BootstrapConfiguration.class, classLoader);
// TODO: is it possible or sensible to share a ResourceLoader?
SpringApplicationBuilder builder = new SpringApplicationBuilder()
.showBanner(false).environment(bootstrapEnvironment).web(false)
.properties("spring.application.name:bootstrap");
.profiles(environment.getActiveProfiles()).showBanner(false)
.environment(bootstrapEnvironment).web(false);
List<Class<?>> sources = new ArrayList<Class<?>>();
for (String name : names) {
Class<?> cls = ClassUtils.resolveClassName(name, null);
try {
cls.getDeclaredAnnotations();
} catch (Exception e) {
}
catch (Exception e) {
continue;
}
if (cls != null) {
@@ -177,8 +192,10 @@ public class BootstrapApplicationListener implements
@Override
public void initialize(ConfigurableApplicationContext context) {
preemptMerge(context.getEnvironment().getPropertySources(), parent
.getEnvironment().getPropertySources().get("bootstrap"));
preemptMerge(
context.getEnvironment().getPropertySources(),
new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, Collections
.<String, Object> emptyMap()));
while (context.getParent() != null && context.getParent() != context) {
context = (ConfigurableApplicationContext) context.getParent();
}

View File

@@ -33,6 +33,7 @@ import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.cloud.bootstrap.BootstrapApplicationListener;
import org.springframework.cloud.config.client.ConfigServicePropertySourceLocator;
import org.springframework.cloud.config.client.PropertySourceLocator;
import org.springframework.util.StringUtils;
@@ -43,11 +44,13 @@ import org.springframework.util.StringUtils;
*/
@Configuration
@EnableConfigurationProperties
public class ConfigServiceBootstrapConfiguration implements
public class PropertySourceBootstrapConfiguration implements
ApplicationContextInitializer<ConfigurableApplicationContext> {
private static final String BOOTSTRAP_PROPERTY_SOURCE_NAME = BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME;
private static Log logger = LogFactory
.getLog(ConfigServiceBootstrapConfiguration.class);
.getLog(PropertySourceBootstrapConfiguration.class);
@Autowired(required = false)
private List<PropertySourceLocator> propertySourceLocators = new ArrayList<PropertySourceLocator>();
@@ -60,7 +63,8 @@ public class ConfigServiceBootstrapConfiguration implements
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
CompositePropertySource composite = new CompositePropertySource("bootstrap");
CompositePropertySource composite = new CompositePropertySource(
BOOTSTRAP_PROPERTY_SOURCE_NAME);
AnnotationAwareOrderComparator.sort(propertySourceLocators);
boolean empty = true;
for (PropertySourceLocator locator : propertySourceLocators) {
@@ -81,8 +85,8 @@ public class ConfigServiceBootstrapConfiguration implements
if (!empty) {
MutablePropertySources propertySources = applicationContext.getEnvironment()
.getPropertySources();
if (propertySources.contains("bootstrap")) {
propertySources.replace("bootstrap", composite);
if (propertySources.contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
propertySources.replace(BOOTSTRAP_PROPERTY_SOURCE_NAME, composite);
}
else {
propertySources.addFirst(composite);
@@ -95,7 +99,7 @@ public class ConfigServiceBootstrapConfiguration implements
ConfigurableEnvironment environment) {
ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator();
String[] profiles = environment.getActiveProfiles();
if (profiles.length==0) {
if (profiles.length == 0) {
profiles = environment.getDefaultProfiles();
}
locator.setEnv(StringUtils.arrayToCommaDelimitedString(profiles));

View File

@@ -9,6 +9,6 @@ org.springframework.cloud.context.restart.RestartListener
# Bootstrap components
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.bootstrap.config.ConfigServiceBootstrapConfiguration,\
org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration,\
org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration,\
org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration

View File

@@ -17,6 +17,7 @@
package org.springframework.cloud.bootstrap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
@@ -52,16 +53,34 @@ public class BootstrapConfigurationTests {
@Test
public void picksUpAdditionalPropertySource() {
context = new SpringApplicationBuilder().web(false).sources(
BareConfiguration.class).run();
context = new SpringApplicationBuilder().web(false)
.sources(BareConfiguration.class).run();
assertEquals("bar", context.getEnvironment().getProperty("bootstrap.foo"));
assertTrue(context.getEnvironment().getPropertySources().contains("bootstrap"));
}
@Test
public void environmentEnrichedOnce() {
context = new SpringApplicationBuilder().sources(BareConfiguration.class).environment(
new StandardEnvironment()).child(BareConfiguration.class).web(false).run();
public void applicationNameIsNotBootstrap() {
context = new SpringApplicationBuilder().web(false)
.properties("spring.cloud.bootstrap.name:other")
.sources(BareConfiguration.class).run();
assertEquals("main",
context.getEnvironment().getProperty("spring.application.name"));
assertEquals(
"other",
context.getParent().getEnvironment()
.getProperty("spring.application.name"));
// The bootstrap context has a different "bootstrap" property source
assertNotSame(context.getEnvironment().getPropertySources().get("bootstrap"),
((ConfigurableEnvironment) context.getParent().getEnvironment())
.getPropertySources().get("bootstrap"));
}
@Test
public void environmentEnrichedOnceWhenShared() {
context = new SpringApplicationBuilder().sources(BareConfiguration.class)
.environment(new StandardEnvironment()).child(BareConfiguration.class)
.web(false).run();
assertEquals("bar", context.getEnvironment().getProperty("bootstrap.foo"));
assertEquals(context.getEnvironment(), context.getParent().getEnvironment());
MutablePropertySources sources = context.getEnvironment().getPropertySources();
@@ -72,13 +91,40 @@ public class BootstrapConfigurationTests {
@Test
public void environmentEnrichedInParent() {
context = new SpringApplicationBuilder().sources(BareConfiguration.class).child(
BareConfiguration.class).web(false).run();
context = new SpringApplicationBuilder().sources(BareConfiguration.class)
.child(BareConfiguration.class).web(false).run();
assertEquals("bar", context.getEnvironment().getProperty("bootstrap.foo"));
assertNotSame(context.getEnvironment(), context.getParent().getEnvironment());
assertTrue(context.getEnvironment().getPropertySources().contains("bootstrap"));
assertTrue(((ConfigurableEnvironment) context.getParent().getEnvironment()).getPropertySources().contains(
"bootstrap"));
assertTrue(((ConfigurableEnvironment) context.getParent().getEnvironment())
.getPropertySources().contains("bootstrap"));
}
@Test
public void differentProfileInChild() {
// Profiles are always merged with the child
ConfigurableApplicationContext parent = new SpringApplicationBuilder()
.sources(BareConfiguration.class).profiles("parent").web(false).run();
context = new SpringApplicationBuilder(BareConfiguration.class).profiles("child")
.parent(parent).web(false).run();
assertNotSame(context.getEnvironment(), context.getParent().getEnvironment());
// The ApplicationContext merges profiles (profiles and property sources), see
// AbstractEnvironment.merge()
assertTrue(this.context.getEnvironment().acceptsProfiles("child", "parent"));
// But the parent is not a child
assertFalse(this.context.getParent().getEnvironment().acceptsProfiles("child"));
assertTrue(this.context.getParent().getEnvironment().acceptsProfiles("parent"));
assertTrue(((ConfigurableEnvironment) context.getParent().getEnvironment())
.getPropertySources().contains("bootstrap"));
assertEquals("bar", context.getEnvironment().getProperty("bootstrap.foo"));
// The "bootstrap" property source is not shared now, but it has the same
// properties in it because they are pulled from the PropertySourceConfiguration
// below
assertEquals("bar",
context.getParent().getEnvironment().getProperty("bootstrap.foo"));
// The parent property source is there in the child because they are both in the
// "parent" profile (by virtue of the merge in AbstractEnvironment)
assertEquals("parent", context.getEnvironment().getProperty("info.name"));
}
@Configuration
@@ -86,6 +132,7 @@ public class BootstrapConfigurationTests {
}
@Configuration
// This is added to bootstrap context as a source in bootstrap.properties
protected static class PropertySourceConfiguration implements PropertySourceLocator {
@Override

View File

@@ -0,0 +1 @@
info.name: parent

View File

@@ -1 +1,2 @@
spring.main.sources: org.springframework.cloud.bootstrap.BootstrapConfigurationTests.PropertySourceConfiguration
spring.main.sources: org.springframework.cloud.bootstrap.BootstrapConfigurationTests.PropertySourceConfiguration
info.name: child

View File

@@ -0,0 +1 @@
spring.application.name: main