diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 80ceffe7c07..6c8a9ae5a53 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -249,10 +249,10 @@ them using `SpringApplication.setAddCommandLineProperties(false)`. `SpringApplication` will load properties from `application.properties` files in the following locations and add them to the Spring `Environment`: -. The current directory . A `/config` subdir of the current directory. -. The classpath root +. The current directory . A classpath `/config` package +. The classpath root The list is ordered by precedence (locations higher in the list override lower items). @@ -261,13 +261,29 @@ an alternative to '.properties'. If you don't like `application.properties` as the configuration file name you can switch to another by specifying a `spring.config.name` environment property. You can also refer -to an explicit location using the `spring.config.location` environment property. +to an explicit location using the `spring.config.location` environment property (comma- +separated list of directory locations, or file paths). [indent=0] ---- $ java -jar myproject.jar --spring.config.name=myproject ---- +or + +[indent=0] +---- + $ java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties +---- + +If `spring.config.location` contains directories (as opposed to files) +they should end in "/" (and will be appended with the names generated +from `spring.config.name` before being loaded). The default search +path `classpath:,classpath:/config,file:,file:config/` is always used, +irrespective of the value of `spring.config.location`. In that way you +can set up default values for your app in `application.properties` (or +whatever other basename you choose with `spring.config.name`) and +override it at runtime with a different file, keeping the defaults. [[boot-features-external-config-profile-specific-properties]] diff --git a/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java b/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java index 4f40702683c..96b39121b19 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java @@ -35,6 +35,7 @@ import org.springframework.boot.bind.PropertySourcesPropertyValues; import org.springframework.boot.bind.RelaxedDataBinder; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.boot.env.EnumerableCompositePropertySource; import org.springframework.boot.env.PropertySourcesLoader; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; @@ -98,8 +99,8 @@ public class ConfigFileApplicationListener implements private static final String CONFIG_LOCATION_PROPERTY = "spring.config.location"; - private static final String DEFAULT_SEARCH_LOCATIONS = "file:./config/,file:./," - + "classpath:/config/,classpath:/"; + // Note the order is from least to most specific (last one wins) + private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/"; private static final String DEFAULT_NAMES = "application"; @@ -197,8 +198,8 @@ public class ConfigFileApplicationListener implements * search location should be a directory path (ending in "/") and it will be prefixed * by the file names constructed from {@link #setSearchNames(String) search names} and * profiles (if any) plus file extensions supported by the properties loaders. - * Locations are considered in the order specified, with earlier items taking - * precedence. + * Locations are considered in the order specified, with later items taking precedence + * (like a map merge). */ public void setSearchLocations(String locations) { Assert.hasLength(locations, "Locations must not be empty"); @@ -288,15 +289,22 @@ public class ConfigFileApplicationListener implements } // The default profile for these purposes is represented as null. We add it - // last so that it is first out (active profiles will then override any - // settings in the defaults when the list is reversed later). + // last so that it is first out of the queue (active profiles will then + // override any settings in the defaults when the list is reversed later). this.profiles.add(null); while (!this.profiles.isEmpty()) { String profile = this.profiles.poll(); for (String location : getSearchLocations()) { - for (String name : getSearchNames()) { - load(location, name, profile); + if (!location.endsWith("/")) { + // location is a filename already, so don't search for more + // filenames + load(location, null, profile); + } + else { + for (String name : getSearchNames()) { + load(location, name, profile); + } } } } @@ -307,30 +315,39 @@ public class ConfigFileApplicationListener implements private void load(String location, String name, String profile) throws IOException { - // Try to load directly from the location - PropertySource locationPropertySource = load(location, profile); + String group = "profile=" + (profile == null ? "" : profile); - // If that fails, try a search - if (locationPropertySource == null) { + if (!StringUtils.hasText(name)) { + // Try to load directly from the location + loadIntoGroup(group, location, profile); + } + else { + // Search for a file with the given name for (String ext : this.propertiesLoader.getAllFileExtensions()) { if (profile != null) { // Try the profile specific file - load(location + name + "-" + profile + "." + ext, null); - load(location + name + "-" + profile + "." + ext, profile); + loadIntoGroup(group, location + name + "-" + profile + "." + ext, + null); + // Sometimes people put "spring.profiles: dev" in + // application-dev.yml (gh-340). Arguably we should try and error + // out on that, but we can be kind and load it anyway. + loadIntoGroup(group, location + name + "-" + profile + "." + ext, + profile); } - // Try the profile (if any) specific section of the normal file - load(location + name + "." + ext, profile); + // Also try the profile specific section (if any) of the normal file + loadIntoGroup(group, location + name + "." + ext, profile); } } } - private PropertySource load(String resourceLocation, String profile) - throws IOException { - Resource resource = this.resourceLoader.getResource(resourceLocation); + private PropertySource loadIntoGroup(String identifier, String location, + String profile) throws IOException { + Resource resource = this.resourceLoader.getResource(location); if (resource != null) { - String name = "applicationConfig: " + resource.getDescription(); + String name = "applicationConfig: [" + location + "]"; + String group = "applicationConfig: [" + identifier + "]"; PropertySource propertySource = this.propertiesLoader.load(resource, - name, profile); + group, name, profile); if (propertySource != null) { maybeActivateProfiles(propertySource .getProperty(ACTIVE_PROFILES_PROPERTY)); @@ -381,11 +398,9 @@ public class ConfigFileApplicationListener implements environment.setActiveProfiles(profiles.toArray(new String[profiles.size()])); } - public Set getSearchLocations() { + private Set getSearchLocations() { Set locations = new LinkedHashSet(); - locations.addAll(asResolvedSet( - ConfigFileApplicationListener.this.searchLocations, - DEFAULT_SEARCH_LOCATIONS)); + // User-configured settings take precedence, so we do them first if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) { for (String path : asResolvedSet( this.environment.getProperty(CONFIG_LOCATION_PROPERTY), null)) { @@ -398,10 +413,13 @@ public class ConfigFileApplicationListener implements locations.add(path); } } + locations.addAll(asResolvedSet( + ConfigFileApplicationListener.this.searchLocations, + DEFAULT_SEARCH_LOCATIONS)); return locations; } - public Set getSearchNames() { + private Set getSearchNames() { if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) { return asResolvedSet(this.environment.getProperty(CONFIG_NAME_PROPERTY), null); @@ -410,16 +428,10 @@ public class ConfigFileApplicationListener implements } private Set asResolvedSet(String value, String fallback) { - return asResolvedSet(value, fallback, true); - } - - private Set asResolvedSet(String value, String fallback, boolean reverse) { List list = Arrays.asList(StringUtils .commaDelimitedListToStringArray(value != null ? this.environment .resolvePlaceholders(value) : fallback)); - if (reverse) { - Collections.reverse(list); - } + Collections.reverse(list); return new LinkedHashSet(list); } @@ -428,7 +440,6 @@ public class ConfigFileApplicationListener implements for (PropertySource item : sources) { reorderedSources.add(item); } - Collections.reverse(reorderedSources); this.environment.getPropertySources().addLast( new ConfigurationPropertySources(reorderedSources)); } @@ -477,7 +488,15 @@ public class ConfigFileApplicationListener implements .remove(ConfigurationPropertySources.NAME); if (removed != null) { for (PropertySource propertySource : removed.sources) { - propertySources.addLast(propertySource); + if (propertySource instanceof EnumerableCompositePropertySource) { + EnumerableCompositePropertySource composite = (EnumerableCompositePropertySource) propertySource; + for (PropertySource nested : composite.getSource()) { + propertySources.addLast(nested); + } + } + else { + propertySources.addLast(propertySource); + } } } } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java index a7d9f2ea35c..44432d7e28a 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java @@ -338,10 +338,12 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc for (String location : locations) { Resource resource = this.resourceLoader.getResource(this.environment .resolvePlaceholders(location)); - for (String profile : this.environment.getActiveProfiles()) { - loader.load(resource, null, profile); + String[] profiles = this.environment.getActiveProfiles(); + for (int i = profiles.length; i-- > 0;) { + String profile = profiles[i]; + loader.load(resource, profile); } - loader.load(resource, null, null); + loader.load(resource); } return loader.getPropertySources(); } diff --git a/spring-boot/src/main/java/org/springframework/boot/env/EnumerableCompositePropertySource.java b/spring-boot/src/main/java/org/springframework/boot/env/EnumerableCompositePropertySource.java new file mode 100644 index 00000000000..9feaff94a6d --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/env/EnumerableCompositePropertySource.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.env; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; + +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; + +/** + * An mutable, enumerable, composite property source. New sources are added last (and + * hence resolved with lowest priority). + * + * @see PropertySource + * @see EnumerablePropertySource + * + * @author Dave Syer + */ +public class EnumerableCompositePropertySource extends + EnumerablePropertySource>> { + + private volatile String[] names; + + public EnumerableCompositePropertySource(String sourceName) { + super(sourceName, new LinkedHashSet>()); + } + + @Override + public Object getProperty(String name) { + for (PropertySource propertySource : getSource()) { + Object value = propertySource.getProperty(name); + if (value != null) { + return value; + } + } + return null; + } + + @Override + public String[] getPropertyNames() { + String[] result = this.names; + if (result == null) { + List names = new ArrayList(); + for (PropertySource source : new ArrayList>(getSource())) { + if (source instanceof EnumerablePropertySource) { + names.addAll(Arrays.asList(((EnumerablePropertySource) source) + .getPropertyNames())); + } + } + this.names = names.toArray(new String[0]); + result = this.names; + } + return result; + } + + public void add(PropertySource source) { + getSource().add(source); + this.names = null; + } +} diff --git a/spring-boot/src/main/java/org/springframework/boot/env/PropertySourcesLoader.java b/spring-boot/src/main/java/org/springframework/boot/env/PropertySourcesLoader.java index 25e9fd55e1b..4881ba0c581 100644 --- a/spring-boot/src/main/java/org/springframework/boot/env/PropertySourcesLoader.java +++ b/spring-boot/src/main/java/org/springframework/boot/env/PropertySourcesLoader.java @@ -64,6 +64,31 @@ public class PropertySourcesLoader { /** * Load the specified resource (if possible) and add it as the first source. * @param resource the source resource (may be {@code null}). + * @return the loaded property source or {@code null} + * @throws IOException + */ + public PropertySource load(Resource resource) throws IOException { + return this.load(resource, null); + } + + /** + * Load the profile-specific properties from the specified resource (if any) and add + * it as the first source. + * + * @param resource the source resource (may be {@code null}). + * @param profile a specific profile to load or {@code null} to load the default. + * @return the loaded property source or {@code null} + * @throws IOException + */ + public PropertySource load(Resource resource, String profile) throws IOException { + return this.load(resource, resource.getDescription(), profile); + } + + /** + * Load the profile-specific properties from the specified resource (if any), give the + * name provided and add it as the first source. + * + * @param resource the source resource (may be {@code null}). * @param name the root property name (may be {@code null}). * @param profile a specific profile to load or {@code null} to load the default. * @return the loaded property source or {@code null} @@ -71,13 +96,36 @@ public class PropertySourcesLoader { */ public PropertySource load(Resource resource, String name, String profile) throws IOException { + return this.load(resource, null, name, profile); + } + + /** + * Load the profile-specific properties from the specified resource (if any), give the + * name provided and add it to a group of property sources identified by the group + * name. Property sources are added to the end of a group, but new groups are added as + * the first in the chain being assembled. This means the normal sequence of calls is + * to first create the group for the default (null) profile, and then add specific + * groups afterwards (with the highest priority last). Property resolution from the + * resulting sources will consider all keys for a given group first and then move to + * the next group. + * + * @param resource the source resource (may be {@code null}). + * @param group an identifier for the group that this source belongs to + * @param name the root property name (may be {@code null}). + * @param profile a specific profile to load or {@code null} to load the default. + * @return the loaded property source or {@code null} + * @throws IOException + */ + public PropertySource load(Resource resource, String group, String name, + String profile) throws IOException { if (isFile(resource)) { - name = generatePropertySourceName(resource, name, profile); + String sourceName = generatePropertySourceName(name, profile); for (PropertySourceLoader loader : this.loaders) { if (canLoadFileExtension(loader, resource)) { - PropertySource source = loader.load(name, resource, profile); - addPropertySource(source); - return source; + PropertySource specific = loader.load(sourceName, resource, + profile); + addPropertySource(group, specific, profile); + return specific; } } } @@ -91,11 +139,7 @@ public class PropertySourcesLoader { .getFilename())); } - private String generatePropertySourceName(Resource resource, String name, - String profile) { - if (name == null) { - name = resource.getDescription(); - } + private String generatePropertySourceName(String name, String profile) { return (profile == null ? name : name + "#" + profile); } @@ -109,10 +153,37 @@ public class PropertySourcesLoader { return false; } - private void addPropertySource(PropertySource propertySource) { - if (propertySource != null) { - this.propertySources.addLast(propertySource); + private void addPropertySource(String basename, PropertySource source, + String profile) { + + if (source == null) { + return; } + + if (basename == null) { + this.propertySources.addLast(source); + return; + } + + EnumerableCompositePropertySource group = getGeneric(basename); + group.add(source); + if (this.propertySources.contains(group.getName())) { + this.propertySources.replace(group.getName(), group); + } + else { + this.propertySources.addFirst(group); + } + + } + + private EnumerableCompositePropertySource getGeneric(String name) { + PropertySource source = this.propertySources.get(name); + if (source instanceof EnumerableCompositePropertySource) { + return (EnumerableCompositePropertySource) source; + } + EnumerableCompositePropertySource composite = new EnumerableCompositePropertySource( + name); + return composite; } /** diff --git a/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileApplicationListenerTests.java b/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileApplicationListenerTests.java index 0e24edc573a..6f442bdd06e 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileApplicationListenerTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileApplicationListenerTests.java @@ -36,6 +36,7 @@ import org.junit.rules.ExpectedException; import org.springframework.boot.SpringApplication; import org.springframework.boot.context.config.ConfigFileApplicationListener.ConfigurationPropertySources; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; +import org.springframework.boot.env.EnumerableCompositePropertySource; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; @@ -91,8 +92,7 @@ public class ConfigFileApplicationListenerTests { @Test public void loadTwoPropertiesFile() throws Exception { EnvironmentTestUtils.addEnvironment(this.environment, "spring.config.location:" - + "classpath:testproperties.properties," - + "classpath:application.properties"); + + "classpath:application.properties,classpath:testproperties.properties"); this.initializer.onApplicationEvent(this.event); String property = this.environment.getProperty("my.property"); assertThat(property, equalTo("frompropertiesfile")); @@ -101,8 +101,7 @@ public class ConfigFileApplicationListenerTests { @Test public void loadTwoPropertiesFilesWithProfiles() throws Exception { EnvironmentTestUtils.addEnvironment(this.environment, "spring.config.location:" - + "classpath:enableprofile.properties," - + "classpath:enableother.properties"); + + "classpath:enableprofile.properties,classpath:enableother.properties"); this.initializer.onApplicationEvent(this.event); assertEquals("other", StringUtils.arrayToCommaDelimitedString(this.environment .getActiveProfiles())); @@ -120,7 +119,25 @@ public class ConfigFileApplicationListenerTests { StringUtils.arrayToCommaDelimitedString(this.environment .getActiveProfiles())); String property = this.environment.getProperty("my.property"); - assertThat(property, equalTo("fromtwopropertiesfile")); + // The value from the second file wins (no profile specific configuration is + // actually loaded) + assertThat(property, equalTo("frompropertiesfile")); + } + + @Test + public void loadTwoPropertiesFilesWithProfilesAndSwitchOneOffFromSpecificLocation() + throws Exception { + EnvironmentTestUtils.addEnvironment(this.environment, + "spring.config.name:enabletwoprofiles", + "spring.config.location:classpath:enableprofile.properties"); + this.initializer.onApplicationEvent(this.event); + assertEquals("myprofile", + StringUtils.arrayToCommaDelimitedString(this.environment + .getActiveProfiles())); + String property = this.environment.getProperty("my.property"); + // The value from the second file wins (no profile specific configuration is + // actually loaded) + assertThat(property, equalTo("frompropertiesfile")); } @Test @@ -158,8 +175,8 @@ public class ConfigFileApplicationListenerTests { @Test public void loadTwoOfThreePropertiesFile() throws Exception { EnvironmentTestUtils.addEnvironment(this.environment, "spring.config.location:" - + "classpath:testproperties.properties," + "classpath:application.properties," + + "classpath:testproperties.properties," + "classpath:nonexistent.properties"); this.initializer.onApplicationEvent(this.event); String property = this.environment.getProperty("my.property"); @@ -178,7 +195,8 @@ public class ConfigFileApplicationListenerTests { this.initializer.setSearchNames("moreproperties,testproperties"); this.initializer.onApplicationEvent(this.event); String property = this.environment.getProperty("my.property"); - assertThat(property, equalTo("frommorepropertiesfile")); + // The search order has highest precedence last (like merging a map) + assertThat(property, equalTo("frompropertiesfile")); } @Test @@ -273,6 +291,8 @@ public class ConfigFileApplicationListenerTests { public void yamlSetsProfiles() throws Exception { this.initializer.setSearchNames("testsetprofiles"); this.initializer.onApplicationEvent(this.event); + assertEquals("dev", StringUtils.arrayToCommaDelimitedString(this.environment + .getActiveProfiles())); String property = this.environment.getProperty("my.property"); assertThat(Arrays.asList(this.environment.getActiveProfiles()), contains("dev")); assertThat(property, equalTo("fromdevprofile")); @@ -283,13 +303,20 @@ public class ConfigFileApplicationListenerTests { assertEquals(2, sources.size()); List names = new ArrayList(); for (org.springframework.core.env.PropertySource source : sources) { - names.add(source.getName()); + if (source instanceof EnumerableCompositePropertySource) { + for (org.springframework.core.env.PropertySource nested : ((EnumerableCompositePropertySource) source) + .getSource()) { + names.add(nested.getName()); + } + } + else { + names.add(source.getName()); + } } assertThat( names, - contains( - "applicationConfig: class path resource [testsetprofiles.yml]#dev", - "applicationConfig: class path resource [testsetprofiles.yml]")); + contains("applicationConfig: [classpath:/testsetprofiles.yml]#dev", + "applicationConfig: [classpath:/testsetprofiles.yml]")); } @Test @@ -320,10 +347,10 @@ public class ConfigFileApplicationListenerTests { String property = this.environment.getProperty("my.property"); assertThat(property, equalTo("fromspecificlocation")); assertThat(this.environment, containsProperySource("applicationConfig: " - + "class path resource [specificlocation.properties]")); + + "[classpath:specificlocation.properties]")); // The default property source is still there assertThat(this.environment, containsProperySource("applicationConfig: " - + "class path resource [application.properties]")); + + "[classpath:/application.properties]")); assertThat(this.environment.getProperty("foo"), equalTo("bucket")); } @@ -333,8 +360,8 @@ public class ConfigFileApplicationListenerTests { EnvironmentTestUtils.addEnvironment(this.environment, "spring.config.location:" + location); this.initializer.onApplicationEvent(this.event); - assertThat(this.environment, containsProperySource("applicationConfig: " - + "URL [" + location + "]")); + assertThat(this.environment, containsProperySource("applicationConfig: [" + + location + "]")); } @Test @@ -343,8 +370,8 @@ public class ConfigFileApplicationListenerTests { EnvironmentTestUtils.addEnvironment(this.environment, "spring.config.location:" + location); this.initializer.onApplicationEvent(this.event); - assertThat(this.environment, containsProperySource("applicationConfig: " - + "URL [file:" + location + "]")); + assertThat(this.environment, containsProperySource("applicationConfig: [file:" + + location + "]")); } @Test diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java index f4ea7db23dc..37688c33a2b 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java @@ -277,6 +277,16 @@ public class EnableConfigurationPropertiesTests { assertEquals("bar", this.context.getBean(ResourceBindingProperties.class).name); } + @Test + public void testBindingDirectlyToFileWithTwoExplicitSpringProfiles() { + this.context.register(ResourceBindingProperties.class, TestConfiguration.class); + this.context.getEnvironment().setActiveProfiles("super", "other"); + this.context.refresh(); + assertEquals(1, + this.context.getBeanNamesForType(ResourceBindingProperties.class).length); + assertEquals("spam", this.context.getBean(ResourceBindingProperties.class).name); + } + @Test public void testBindingWithTwoBeans() { this.context.register(MoreConfiguration.class, TestConfiguration.class); @@ -357,7 +367,6 @@ public class EnableConfigurationPropertiesTests { assertEquals("value3", bean.mymap.get("key3")); // this should not fail!!! // mymap looks to contain - {key1=, key3=value3} - System.err.println(bean.mymap); assertEquals("value12", bean.mymap.get("key1.key2")); }