diff --git a/Guardfile b/Guardfile index 37c695d5..b716abba 100644 --- a/Guardfile +++ b/Guardfile @@ -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 diff --git a/docs/src/main/asciidoc/quickstart.adoc b/docs/src/main/asciidoc/quickstart.adoc index cf8dfdc1..5ce2ae19 100644 --- a/docs/src/main/asciidoc/quickstart.adoc +++ b/docs/src/main/asciidoc/quickstart.adoc @@ -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 diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index 62c39765..f205559b 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -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 diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java index 6852f92e..2b258130 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java @@ -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 diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/bootstrap/BootstrapApplicationListener.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/bootstrap/BootstrapApplicationListener.java index fbdf574e..4fc6894c 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/bootstrap/BootstrapApplicationListener.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/bootstrap/BootstrapApplicationListener.java @@ -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, 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 bootstrapMap = new HashMap(); + 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. emptyMap())); for (PropertySource source : environment.getPropertySources()) { bootstrapProperties.addLast(source); } - bootstrapProperties.addFirst(new MapPropertySource("bootstrap", Collections - . singletonMap("spring.config.name", "bootstrap"))); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); // Use names and ensure unique to protect against duplicates List 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> sources = new ArrayList>(); 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 + . emptyMap())); while (context.getParent() != null && context.getParent() != context) { context = (ConfigurableApplicationContext) context.getParent(); } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/bootstrap/config/ConfigServiceBootstrapConfiguration.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java similarity index 87% rename from spring-cloud-config-client/src/main/java/org/springframework/cloud/bootstrap/config/ConfigServiceBootstrapConfiguration.java rename to spring-cloud-config-client/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java index 1918fe7a..e24c4593 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/bootstrap/config/ConfigServiceBootstrapConfiguration.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java @@ -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 { + 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 propertySourceLocators = new ArrayList(); @@ -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)); diff --git a/spring-cloud-config-client/src/main/resources/META-INF/spring.factories b/spring-cloud-config-client/src/main/resources/META-INF/spring.factories index 1b96568b..252949b7 100644 --- a/spring-cloud-config-client/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-config-client/src/main/resources/META-INF/spring.factories @@ -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 \ No newline at end of file diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/bootstrap/BootstrapConfigurationTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/bootstrap/BootstrapConfigurationTests.java index f34cf5ab..691fac99 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/bootstrap/BootstrapConfigurationTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/bootstrap/BootstrapConfigurationTests.java @@ -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 diff --git a/spring-cloud-config-client/src/test/resources/bootstrap-parent.properties b/spring-cloud-config-client/src/test/resources/bootstrap-parent.properties new file mode 100644 index 00000000..0c36bbdd --- /dev/null +++ b/spring-cloud-config-client/src/test/resources/bootstrap-parent.properties @@ -0,0 +1 @@ +info.name: parent \ No newline at end of file diff --git a/spring-cloud-config-client/src/test/resources/bootstrap.properties b/spring-cloud-config-client/src/test/resources/bootstrap.properties index 6f6855c6..981dfb73 100644 --- a/spring-cloud-config-client/src/test/resources/bootstrap.properties +++ b/spring-cloud-config-client/src/test/resources/bootstrap.properties @@ -1 +1,2 @@ -spring.main.sources: org.springframework.cloud.bootstrap.BootstrapConfigurationTests.PropertySourceConfiguration \ No newline at end of file +spring.main.sources: org.springframework.cloud.bootstrap.BootstrapConfigurationTests.PropertySourceConfiguration +info.name: child \ No newline at end of file diff --git a/spring-cloud-config-client/src/test/resources/other.properties b/spring-cloud-config-client/src/test/resources/other.properties new file mode 100644 index 00000000..dc756282 --- /dev/null +++ b/spring-cloud-config-client/src/test/resources/other.properties @@ -0,0 +1 @@ +spring.application.name: main \ No newline at end of file