diff --git a/docs/howto.md b/docs/howto.md index d8cae2b12a6..a230a18f7c5 100644 --- a/docs/howto.md +++ b/docs/howto.md @@ -740,11 +740,23 @@ without changing the defaults. ## Change the Location of External Properties of an Application -Properties from different sources are added to the Spring +By default properties from different sources are added to the Spring `Environment` in a defined order, and the precedence for resolution is 1) commandline, 2) filesystem (current working directory) -`application.properties`, 3) classpath `application.properties`. To -modify this you can provide System properties (or environment variables) +`application.properties`, 3) classpath `application.properties`. + +A nice way to augment and modify this is to add `@PropertySource` +annotations to your application sources. Classes passed to the +`SpringApplication` static convenience methods, and those added using +`setSources()` are inspected to see if they have `@PropertySources` +and if they do those properties are added to the `Environment` early +enough to be used in all phases of the `ApplicationContext` +lifecycle. Properties added in this way have precendence over any +added using the default locations, but have lower priority than system +properties, environment variables or the command line. + +You can also provide System properties (or environment variables) to +change the default behaviour: * `config.name` (`CONFIG_NAME`), defaults to `application` as the root of the file name diff --git a/spring-boot/src/main/java/org/springframework/boot/config/PropertiesPropertySourceLoader.java b/spring-boot/src/main/java/org/springframework/boot/config/PropertiesPropertySourceLoader.java index 8022374160d..ef56f0f2772 100644 --- a/spring-boot/src/main/java/org/springframework/boot/config/PropertiesPropertySourceLoader.java +++ b/spring-boot/src/main/java/org/springframework/boot/config/PropertiesPropertySourceLoader.java @@ -41,7 +41,7 @@ public class PropertiesPropertySourceLoader implements PropertySourceLoader { } @Override - public PropertySource load(Resource resource) { + public PropertySource load(String name, Resource resource) { try { Properties properties = loadProperties(resource); // N.B. this is off by default unless user has supplied logback config in @@ -49,7 +49,7 @@ public class PropertiesPropertySourceLoader implements PropertySourceLoader { if (logger.isDebugEnabled()) { logger.debug("Properties loaded from " + resource + ": " + properties); } - return new PropertiesPropertySource(resource.getDescription(), properties); + return new PropertiesPropertySource(name, properties); } catch (IOException ex) { throw new IllegalStateException("Could not load properties from " + resource, diff --git a/spring-boot/src/main/java/org/springframework/boot/config/PropertySourceLoader.java b/spring-boot/src/main/java/org/springframework/boot/config/PropertySourceLoader.java index 05eb2469ebe..7ff20be8f2d 100644 --- a/spring-boot/src/main/java/org/springframework/boot/config/PropertySourceLoader.java +++ b/spring-boot/src/main/java/org/springframework/boot/config/PropertySourceLoader.java @@ -34,8 +34,9 @@ public interface PropertySourceLoader { /** * Load the resource into a property source. + * @param name TODO * @return a property source */ - PropertySource load(Resource resource); + PropertySource load(String name, Resource resource); } \ No newline at end of file diff --git a/spring-boot/src/main/java/org/springframework/boot/context/initializer/ConfigFileApplicationContextInitializer.java b/spring-boot/src/main/java/org/springframework/boot/context/initializer/ConfigFileApplicationContextInitializer.java index 5ba1effc2f1..5438b3d01a1 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/initializer/ConfigFileApplicationContextInitializer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/initializer/ConfigFileApplicationContextInitializer.java @@ -17,8 +17,11 @@ package org.springframework.boot.context.initializer; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Random; @@ -35,7 +38,9 @@ import org.springframework.boot.config.YamlPropertySourceLoader; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.PropertySources; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.env.ConfigurableEnvironment; @@ -45,6 +50,8 @@ import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.StandardAnnotationMetadata; import org.springframework.util.ClassUtils; import org.springframework.util.DigestUtils; import org.springframework.util.StringUtils; @@ -97,6 +104,8 @@ public class ConfigFileApplicationContextInitializer implements private ConversionService conversionService = new DefaultConversionService(); + private PropertySourceAnnotations propertySourceAnnotations = new PropertySourceAnnotations(); + /** * Binds the early {@link Environment} to the {@link SpringApplication}. This makes it * possible to set {@link SpringApplication} properties dynamically, like the sources @@ -107,11 +116,13 @@ public class ConfigFileApplicationContextInitializer implements @Override public void initialize(SpringApplication springApplication, String[] args) { if (this.environment instanceof ConfigurableEnvironment) { + extractPropertySources(springApplication.getSources()); ConfigurableEnvironment environment = (ConfigurableEnvironment) this.environment; load(environment, new DefaultResourceLoader()); environment.getPropertySources().addAfter( StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, new RandomValuePropertySource("random")); + int before = springApplication.getSources().size(); // Set bean properties from the early environment PropertyValues propertyValues = new PropertySourcesPropertyValues( environment.getPropertySources()); @@ -119,6 +130,11 @@ public class ConfigFileApplicationContextInitializer implements "spring.main"); binder.setConversionService(this.conversionService); binder.bind(propertyValues); + int after = springApplication.getSources().size(); + if (after > before) { + // Do it again in case there are new @PropertySources + initialize(springApplication, args); + } } } @@ -127,9 +143,51 @@ public class ConfigFileApplicationContextInitializer implements load(applicationContext.getEnvironment(), applicationContext); } + private void extractPropertySources(Set sources) { + for (Object source : sources) { + if (source instanceof Class) { + Class type = (Class) source; + for (AnnotationAttributes propertySource : attributesForRepeatable( + new StandardAnnotationMetadata(type), PropertySources.class, + org.springframework.context.annotation.PropertySource.class)) { + this.propertySourceAnnotations.add( + propertySource.getStringArray("value"), + propertySource.getBoolean("ignoreResourceNotFound"), + propertySource.getString("name")); + } + } + } + } + + @SuppressWarnings("unchecked") + static Set attributesForRepeatable(AnnotationMetadata metadata, + Class containerClass, Class annotationClass) { + Set result = new LinkedHashSet(); + + addAttributesIfNotNull(result, + metadata.getAnnotationAttributes(annotationClass.getName(), false)); + + Map container = metadata.getAnnotationAttributes( + containerClass.getName(), false); + if (container != null && container.containsKey("value")) { + for (Map containedAttributes : (Map[]) container + .get("value")) { + addAttributesIfNotNull(result, containedAttributes); + } + } + return Collections.unmodifiableSet(result); + } + + private static void addAttributesIfNotNull(Set result, + Map attributes) { + if (attributes != null) { + result.add(AnnotationAttributes.fromMap(attributes)); + } + } + private void load(ConfigurableEnvironment environment, ResourceLoader resourceLoader) { - List candidates = getCandidateLocations(); + List candidates = getCandidateLocations(resourceLoader); Collections.reverse(candidates); PropertySource removed = environment.getPropertySources().remove( "defaultProperties"); @@ -164,8 +222,8 @@ public class ConfigFileApplicationContextInitializer implements } } - private List getCandidateLocations() { - List candidates = new ArrayList(); + private List getCandidateLocations(ResourceLoader resourceLoader) { + Set candidates = new LinkedHashSet(); for (String searchLocation : this.searchLocations) { for (String extension : new String[] { ".properties", ".yml" }) { for (String name : StringUtils @@ -176,7 +234,21 @@ public class ConfigFileApplicationContextInitializer implements } } candidates.add(LOCATION_VARIABLE); - return candidates; + /* + * @PropertySource annotation locations go last here (eventually highest + * priority). This unfortunately isn't the same semantics as @PropertySource in + * Spring and it's hard to change that (so the property source gets added again in + * last position by Spring later in the cycle). + */ + for (String location : this.propertySourceAnnotations.locations()) { + Resource resource = resourceLoader.getResource(location); + if (!this.propertySourceAnnotations.ignoreResourceNotFound(location) + && !resource.exists()) { + throw new IllegalStateException("Resource not found: " + location); + } + candidates.add(location); + } + return new ArrayList(candidates); } private PropertySource load(ConfigurableEnvironment environment, @@ -196,7 +268,12 @@ public class ConfigFileApplicationContextInitializer implements } Resource resource = resourceLoader.getResource(location); - PropertySource propertySource = getPropertySource(resource, profile, loaders); + String name = this.propertySourceAnnotations.name(location); + if (name == null) { + name = location; + } + PropertySource propertySource = getPropertySource(name, resource, profile, + loaders); if (propertySource == null) { return null; } @@ -212,15 +289,15 @@ public class ConfigFileApplicationContextInitializer implements return propertySource; } - private PropertySource getPropertySource(Resource resource, String profile, - List loaders) { + private PropertySource getPropertySource(String name, Resource resource, + String profile, List loaders) { String key = resource.getDescription() + (profile == null ? "" : "#" + profile); if (this.cached.containsKey(key)) { return this.cached.get(key); } for (PropertySourceLoader loader : loaders) { if (resource != null && resource.exists() && loader.supports(resource)) { - PropertySource propertySource = loader.load(resource); + PropertySource propertySource = loader.load(name, resource); this.cached.put(key, propertySource); return propertySource; } @@ -281,4 +358,48 @@ public class ConfigFileApplicationContextInitializer implements } + private static class PropertySourceAnnotations { + + private Collection locations = new LinkedHashSet(); + + private Map names = new HashMap(); + + private Map ignores = new HashMap(); + + public void add(String[] locations, boolean ignoreResourceNotFound, String name) { + this.locations.addAll(Arrays.asList(locations)); + if (StringUtils.hasText(name)) { + for (String location : locations) { + this.names.put(location, name); + } + for (String location : locations) { + boolean reallyIgnore = ignoreResourceNotFound; + if (this.ignores.containsKey(location)) { + // Only if they all ignore this location will it be ignored + reallyIgnore &= this.ignores.get(location); + } + this.ignores.put(location, reallyIgnore); + } + } + } + + public boolean ignoreResourceNotFound(String location) { + return this.ignores.containsKey(location) ? this.ignores.get(location) + : false; + } + + public String name(String location) { + String name = this.names.get(location); + if (name == null || Collections.frequency(this.names.values(), name) > 1) { + return null; + } + // Only if there is a unique name for this location + return "boot." + name; + } + + public Collection locations() { + return this.locations; + } + } + } 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 347f30b51cd..e66a398e865 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 @@ -330,7 +330,7 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc if (resource != null && resource.exists()) { for (PropertySourceLoader loader : loaders) { if (loader.supports(resource)) { - PropertySource propertySource = loader.load(resource); + PropertySource propertySource = loader.load(resource.getDescription(), resource); propertySources.addFirst(propertySource); } } diff --git a/spring-boot/src/test/java/org/springframework/boot/context/initializer/ConfigFileApplicationContextInitializerTests.java b/spring-boot/src/test/java/org/springframework/boot/context/initializer/ConfigFileApplicationContextInitializerTests.java index c0e34df54ec..d85113177eb 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/initializer/ConfigFileApplicationContextInitializerTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/initializer/ConfigFileApplicationContextInitializerTests.java @@ -22,6 +22,9 @@ import java.util.Map; import org.junit.After; import org.junit.Test; import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.SimpleCommandLinePropertySource; @@ -30,6 +33,7 @@ import org.springframework.core.env.StandardEnvironment; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; /** @@ -166,9 +170,90 @@ public class ConfigFileApplicationContextInitializerTests { assertThat(property, equalTo("fromspecificlocation")); } + @Test + public void propertySourceAnnotation() throws Exception { + SpringApplication application = new SpringApplication(WithPropertySource.class); + application.setWebEnvironment(false); + ConfigurableApplicationContext context = application.run(); + String property = context.getEnvironment().getProperty("my.property"); + assertThat(property, equalTo("fromspecificlocation")); + assertNotNull(context.getEnvironment().getPropertySources() + .get("classpath:/specificlocation.properties")); + context.close(); + } + + @Test + public void propertySourceAnnotationWithName() throws Exception { + SpringApplication application = new SpringApplication( + WithPropertySourceAndName.class); + application.setWebEnvironment(false); + ConfigurableApplicationContext context = application.run(); + String property = context.getEnvironment().getProperty("my.property"); + assertThat(property, equalTo("fromspecificlocation")); + // In this case "foo" should be the specificlocation.properties source, but Spring + // will have shifted it to the back of the line. + assertNotNull(context.getEnvironment().getPropertySources().get("boot.foo")); + context.close(); + } + + @Test + public void propertySourceAnnotationMultipleLocations() throws Exception { + SpringApplication application = new SpringApplication( + WithPropertySourceMultipleLocations.class); + application.setWebEnvironment(false); + ConfigurableApplicationContext context = application.run(); + String property = context.getEnvironment().getProperty("my.property"); + assertThat(property, equalTo("frommorepropertiesfile")); + assertNotNull(context.getEnvironment().getPropertySources() + .get("classpath:/specificlocation.properties")); + context.close(); + } + + @Test + public void propertySourceAnnotationMultipleLocationsAndName() throws Exception { + SpringApplication application = new SpringApplication( + WithPropertySourceMultipleLocationsAndName.class); + application.setWebEnvironment(false); + ConfigurableApplicationContext context = application.run(); + String property = context.getEnvironment().getProperty("my.property"); + assertThat(property, equalTo("frommorepropertiesfile")); + // foo is there but it is a dead rubber because the individual sources get higher + // priority (and are named after the resource locations) + assertNotNull(context.getEnvironment().getPropertySources().get("foo")); + assertNotNull(context.getEnvironment().getPropertySources() + .get("classpath:/specificlocation.properties")); + context.close(); + } + @Test public void defaultApplicationProperties() throws Exception { } + @Configuration + @PropertySource("classpath:/specificlocation.properties") + protected static class WithPropertySource { + + } + + @Configuration + @PropertySource(value = "classpath:/specificlocation.properties", name = "foo") + protected static class WithPropertySourceAndName { + + } + + @Configuration + @PropertySource({ "classpath:/specificlocation.properties", + "classpath:/moreproperties.properties" }) + protected static class WithPropertySourceMultipleLocations { + + } + + @Configuration + @PropertySource(value = { "classpath:/specificlocation.properties", + "classpath:/moreproperties.properties" }, name = "foo") + protected static class WithPropertySourceMultipleLocationsAndName { + + } + }