Parse @PropertySource annotations on SpringApplication sources
If any of the sources has a @PropertySource annotation (or many) then we can add those properties to the Environment. It's a nice convenient way of specifying a custom external properties location for an app. One problem is that Spring will come along and parse the same annotations later as part of the @Configuration parsing. The user has pretty limited control over how that is done, and it will never be done in a "natural" way for a Boot application (which would prefer that the default application.properties is applied *last*, whereas Spring will apply the @PropertySource last). To get round that problem we add the property sources with a different name (key in the PropertySources in Environment), prefixing named property sources with "boot.", and adding others with a name that is the same as the resource location (instead of its description, which is the default for Spring). Another problem is that Spring doesn't know about YAML, so the user is currently restricted to using properties files with this annotation.
This commit is contained in:
parent
808daa54e5
commit
bd0a499ab8
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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<Object> 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<AnnotationAttributes> attributesForRepeatable(AnnotationMetadata metadata,
|
||||
Class<?> containerClass, Class<?> annotationClass) {
|
||||
Set<AnnotationAttributes> result = new LinkedHashSet<AnnotationAttributes>();
|
||||
|
||||
addAttributesIfNotNull(result,
|
||||
metadata.getAnnotationAttributes(annotationClass.getName(), false));
|
||||
|
||||
Map<String, Object> container = metadata.getAnnotationAttributes(
|
||||
containerClass.getName(), false);
|
||||
if (container != null && container.containsKey("value")) {
|
||||
for (Map<String, Object> containedAttributes : (Map<String, Object>[]) container
|
||||
.get("value")) {
|
||||
addAttributesIfNotNull(result, containedAttributes);
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableSet(result);
|
||||
}
|
||||
|
||||
private static void addAttributesIfNotNull(Set<AnnotationAttributes> result,
|
||||
Map<String, Object> attributes) {
|
||||
if (attributes != null) {
|
||||
result.add(AnnotationAttributes.fromMap(attributes));
|
||||
}
|
||||
}
|
||||
|
||||
private void load(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
|
||||
|
||||
List<String> candidates = getCandidateLocations();
|
||||
List<String> candidates = getCandidateLocations(resourceLoader);
|
||||
Collections.reverse(candidates);
|
||||
PropertySource<?> removed = environment.getPropertySources().remove(
|
||||
"defaultProperties");
|
||||
|
@ -164,8 +222,8 @@ public class ConfigFileApplicationContextInitializer implements
|
|||
}
|
||||
}
|
||||
|
||||
private List<String> getCandidateLocations() {
|
||||
List<String> candidates = new ArrayList<String>();
|
||||
private List<String> getCandidateLocations(ResourceLoader resourceLoader) {
|
||||
Set<String> candidates = new LinkedHashSet<String>();
|
||||
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<String>(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<PropertySourceLoader> loaders) {
|
||||
private PropertySource<?> getPropertySource(String name, Resource resource,
|
||||
String profile, List<PropertySourceLoader> 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<String> locations = new LinkedHashSet<String>();
|
||||
|
||||
private Map<String, String> names = new HashMap<String, String>();
|
||||
|
||||
private Map<String, Boolean> ignores = new HashMap<String, Boolean>();
|
||||
|
||||
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<String> locations() {
|
||||
return this.locations;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue