- * Alternative locations and names can be specified using
- * {@link #setSearchLocations(String[])} and {@link #setNames(String)}.
+ * Alternative search locations and names can be specified using
+ * {@link #setSearchLocations(String)} and {@link #setSearchNames(String)}.
*
* Additional files will also be loaded based on active profiles. For example if a 'web'
* profile is active 'application-web.properties' and 'application-web.yml' will be
* considered.
*
- * The 'spring.config.name' property can be used to specify an alternative name to load or
- * alternatively the 'spring.config.location' property can be used to specify an exact
- * resource location.
+ * The 'spring.config.name' property can be used to specify an alternative name to load
+ * and the 'spring.config.location' property can be used to specify alternative search
+ * locations or specific files.
+ *
+ * Configuration properties are also bound to the {@link SpringApplication}. This makes it
+ * possible to set {@link SpringApplication} properties dynamically, like the sources
+ * ("spring.main.sources" - a CSV list) the flag to indicate a web environment
+ * ("spring.main.web_environment=true") or the flag to switch off the banner
+ * ("spring.main.show_banner=false").
*
* @author Dave Syer
* @author Phillip Webb
*/
public class ConfigFileApplicationListener implements
- ApplicationListener, Ordered {
+ ApplicationListener, Ordered {
+
+ private static final String DEFAULT_PROPERTIES = "defaultProperties";
private static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";
- private static final String LOCATION_VARIABLE = "${spring.config.location}";
+ private static final String CONFIG_NAME_PROPERTY = "spring.config.name";
- public static final int DEFAULT_CONFIG_LISTENER_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;
+ private static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";
- private String[] searchLocations = new String[] { "classpath:/", "file:./",
- "classpath:/config/", "file:./config/" };
+ private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,file:./,"
+ + "classpath:/config/,file:./config/";
- private String names = "${spring.config.name:application}";
+ private static final String DEFAULT_NAMES = "application";
- private int order = DEFAULT_CONFIG_LISTENER_ORDER;
+ public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;
- private final ConversionService conversionService = new DefaultConversionService();
+ private String searchLocations;
- private final Map> cache = new HashMap>();
+ private String names;
- private final PropertySourceAnnotations annotations = new PropertySourceAnnotations();
+ private int order = DEFAULT_ORDER;
- private PropertySourceLoadersFactory propertySourceLoadersFactory = new DefaultPropertySourceLoadersFactory();
+ private final ConversionService conversionService = new DefaultConversionService();
- /**
- * Binds the early {@link Environment} to the {@link SpringApplication}. This makes it
- * possible to set {@link SpringApplication} properties dynamically, like the sources
- * ("spring.main.sources" - a CSV list) the flag to indicate a web environment
- * ("spring.main.web_environment=true") or the flag to switch off the banner
- * ("spring.main.show_banner=false").
- */
@Override
- public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
- Environment environment = event.getEnvironment();
- if (environment instanceof ConfigurableEnvironment) {
- configure((ConfigurableEnvironment) environment, event.getSpringApplication());
+ public void onApplicationEvent(ApplicationEvent event) {
+ if (event instanceof ApplicationEnvironmentPreparedEvent) {
+ onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
}
- }
-
- private void configure(ConfigurableEnvironment environment,
- SpringApplication springApplication) {
- for (Object source : springApplication.getSources()) {
- this.annotations.addFromSource(source);
- }
- load(environment, new DefaultResourceLoader());
- environment.getPropertySources().addAfter(
- StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
- new RandomValuePropertySource("random"));
-
- int sourcesSizeBefore = springApplication.getSources().size();
- // Set bean properties from the early environment
- PropertyValues propertyValues = new PropertySourcesPropertyValues(
- environment.getPropertySources());
- RelaxedDataBinder binder = new RelaxedDataBinder(springApplication, "spring.main");
- binder.setConversionService(this.conversionService);
- binder.bind(propertyValues);
-
- if (springApplication.getSources().size() > sourcesSizeBefore) {
- // Configure again in case there are new @PropertySources
- configure(environment, springApplication);
+ if (event instanceof ApplicationPreparedEvent) {
+ onApplicationPreparedEvent((ApplicationPreparedEvent) event);
}
- }
-
- private void load(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
-
- LoadCandidates candidates = new LoadCandidates(environment, resourceLoader);
- PropertySource> defaultProperties = environment.getPropertySources().remove(
- "defaultProperties");
-
- // Load to allow a file that defines active profiles to be considered
- String firstPropertySourceName = loadInitial(environment, resourceLoader,
- candidates);
-
- // Apply the active profiles (if any) from the first property source
- if (environment.containsProperty(ACTIVE_PROFILES_PROPERTY)) {
- activeProfilesFromProperty(environment,
- environment.getProperty(ACTIVE_PROFILES_PROPERTY), true);
- }
-
- // Apply any profile additions from any source
- activeProfileAdditionsFromAnySource(environment);
-
- // Repeatedly load property sources in case additional profiles are activated
- int numberOfPropertySources;
- do {
- numberOfPropertySources = environment.getPropertySources().size();
- activeProfileAdditionsFromAnySource(environment);
- loadAgain(environment, resourceLoader, candidates, firstPropertySourceName);
- }
- while (environment.getPropertySources().size() > numberOfPropertySources);
-
- if (defaultProperties != null) {
- environment.getPropertySources().addLast(defaultProperties);
- }
- }
-
- private void activeProfileAdditionsFromAnySource(ConfigurableEnvironment environment) {
- for (PropertySource> propertySource : environment.getPropertySources()) {
- if (propertySource.containsProperty(ACTIVE_PROFILES_PROPERTY)) {
- activeProfilesFromProperty(environment,
- propertySource.getProperty(ACTIVE_PROFILES_PROPERTY), false);
- }
- }
- }
-
- private void activeProfilesFromProperty(ConfigurableEnvironment environment,
- Object property, boolean addAll) {
- for (String profile : StringUtils.commaDelimitedListToSet(property.toString())) {
- boolean addition = profile.startsWith("+");
- profile = (addition ? profile.substring(1) : profile);
- if (addAll || addition) {
- environment.addActiveProfile(profile);
- }
- }
- }
+ };
- private String loadInitial(ConfigurableEnvironment environment,
- ResourceLoader resourceLoader, LoadCandidates candidates) {
- String firstSourceName = null;
- // Initial load allows profiles to be activated
- for (String candidate : candidates) {
- for (String path : StringUtils.commaDelimitedListToStringArray(environment
- .resolvePlaceholders(candidate))) {
-
- if (LOCATION_VARIABLE.equals(candidate) && !path.contains("$")) {
- if (!path.contains(":")) {
- path = "file:" + path;
- }
- path = StringUtils.cleanPath(path);
- }
-
- PropertySource> source = loadPropertySource(environment,
- resourceLoader, path, null);
- if (source != null) {
- if (firstSourceName == null) {
- firstSourceName = source.getName();
- }
- environment.getPropertySources().addLast(source);
- }
- }
- }
- return firstSourceName;
- }
-
- private void loadAgain(ConfigurableEnvironment environment,
- ResourceLoader resourceLoader, LoadCandidates candidates,
- String firstPropertySourceName) {
- for (String profile : environment.getActiveProfiles()) {
- for (String candidate : candidates) {
- PropertySource> source = loadPropertySource(environment,
- resourceLoader, candidate, profile);
- addBeforeOrLast(environment, firstPropertySourceName, source);
- }
+ private void onApplicationEnvironmentPreparedEvent(
+ ApplicationEnvironmentPreparedEvent event) {
+ Environment environment = event.getEnvironment();
+ if (environment instanceof ConfigurableEnvironment) {
+ onApplicationEnvironmentPreparedEvent((ConfigurableEnvironment) environment,
+ event.getSpringApplication());
}
}
- private void addBeforeOrLast(ConfigurableEnvironment environment,
- String relativePropertySourceName, PropertySource> source) {
- if (source != null) {
- MutablePropertySources propertySources = environment.getPropertySources();
- // Originals go at the end so they don't override the specific profiles
- if (relativePropertySourceName != null) {
- propertySources.addBefore(relativePropertySourceName, source);
+ private void onApplicationEnvironmentPreparedEvent(
+ ConfigurableEnvironment environment, SpringApplication application) {
+ RandomValuePropertySource.addToEnvironment(environment);
+ try {
+ PropertySource> defaultProperties = environment.getPropertySources()
+ .remove(DEFAULT_PROPERTIES);
+ new Loader(environment).load();
+ if (defaultProperties != null) {
+ environment.getPropertySources().addLast(defaultProperties);
}
- else {
- propertySources.addLast(source);
- }
- }
- }
-
- private PropertySource> loadPropertySource(ConfigurableEnvironment environment,
- ResourceLoader resourceLoader, String location, String profile) {
-
- Class> type = this.annotations.configuration(location);
-
- String suffix = "." + StringUtils.getFilenameExtension(location);
- if (StringUtils.hasLength(profile)) {
- location = location.replace(suffix, "-" + profile + suffix);
}
-
- if (isPropertySourceAnnotationOnExcludedType(environment, profile, type, location)) {
- return null;
+ catch (IOException ex) {
+ throw new IllegalStateException("Unable to load configuration files", ex);
}
-
- Resource resource = resourceLoader.getResource(location);
- String name = this.annotations.name(location);
- name = (name != null ? name : location);
- return getPropertySource(environment, name, resource, profile);
+ bindToSpringApplication(application, environment);
}
- private boolean isPropertySourceAnnotationOnExcludedType(Environment environment,
- String profile, Class> type, String location) {
-
- if (type == null) {
- // No configuration class to worry about, just a vanilla properties location
- return false;
- }
-
- if (StringUtils.hasText(profile)
- && !this.annotations.getLocations().contains(location)) {
- // We are looking for profile specific properties and this one isn't
- // explicitly asked for in propertySourceAnnotations
- return true;
- }
-
- AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(
- new DefaultListableBeanFactory(), environment);
- int before = reader.getRegistry().getBeanDefinitionCount();
- reader.register(type);
- int after = reader.getRegistry().getBeanDefinitionCount();
-
- // Return if the configuration class was @Conditional and excluded
- return (after == before);
+ private void bindToSpringApplication(SpringApplication application,
+ ConfigurableEnvironment environment) {
+ RelaxedDataBinder binder = new RelaxedDataBinder(application, "spring.main");
+ binder.setConversionService(this.conversionService);
+ binder.bind(new PropertySourcesPropertyValues(environment.getPropertySources()));
}
- private PropertySource> getPropertySource(Environment environment, String name,
- Resource resource, String profile) {
- if (resource == null || !resource.exists()) {
- return null;
- }
- String key = resource.getDescription() + (profile == null ? "" : "#" + profile);
- if (this.cache.containsKey(key)) {
- return this.cache.get(key);
- }
- for (PropertySourceLoader loader : this.propertySourceLoadersFactory
- .getLoaders(environment)) {
- if (loader.supports(resource)) {
- PropertySource> propertySource = loader.load(name, resource);
- this.cache.put(key, propertySource);
- return propertySource;
- }
- }
- throw new IllegalStateException("No supported loader found for "
- + "configuration resource: " + resource);
+ private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
+ ConfigurableApplicationContext context = event.getApplicationContext();
+ context.addBeanFactoryPostProcessor(new PropertySourceOrderingPostProcessor(
+ context));
}
public void setOrder(int order) {
@@ -323,181 +169,233 @@ public class ConfigFileApplicationListener implements
}
/**
- * Sets the names of the files that should be loaded (excluding file extension) as a
- * comma separated list. Defaults to "application".
+ * Set the search locations that will be considered as a comma-separated list.
*/
- public void setNames(String names) {
- this.names = names;
+ public void setSearchLocations(String locations) {
+ Assert.hasLength(locations, "Locations must not be empty");
+ this.searchLocations = locations;
}
/**
- * Set the search locations that will be considered.
+ * Sets the names of the files that should be loaded (excluding file extension) as a
+ * comma-separated list.
*/
- public void setSearchLocations(String[] searchLocations) {
- this.searchLocations = (searchLocations == null ? null : searchLocations.clone());
+ public void setSearchNames(String names) {
+ Assert.hasLength(names, "Names must not be empty");
+ this.names = names;
}
/**
- * Set the {@link PropertySourceLoadersFactory} that will be used to create
- * {@link PropertySourceLoader}s.
+ * {@link BeanFactoryPostProcessor} to re-order our property sources below any
+ * {@code @ProperySource} items added by the {@link ConfigurationClassPostProcessor}.
*/
- public void setPropertySourceLoadersFactory(
- PropertySourceLoadersFactory propertySourceLoaderFactory) {
- this.propertySourceLoadersFactory = propertySourceLoaderFactory;
- }
+ private class PropertySourceOrderingPostProcessor implements
+ BeanFactoryPostProcessor, Ordered {
- /**
- * Provides {@link Iterable} access to candidate property sources.
- */
- private class LoadCandidates implements Iterable {
-
- private final List candidates;
-
- public LoadCandidates(ConfigurableEnvironment environment,
- ResourceLoader resourceLoader) {
- Set candidates = new LinkedHashSet();
- addLoadCandidatesFromSearchLocations(environment, candidates);
- candidates.add(LOCATION_VARIABLE);
- // @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).
- addLoadCandidatesFromAnnotations(environment, resourceLoader, candidates);
- this.candidates = new ArrayList(candidates);
- Collections.reverse(this.candidates);
- }
+ private ConfigurableApplicationContext context;
- private void addLoadCandidatesFromSearchLocations(
- ConfigurableEnvironment environment, Set candidates) {
- String[] names = StringUtils.commaDelimitedListToStringArray(environment
- .resolvePlaceholders(ConfigFileApplicationListener.this.names));
- for (String location : ConfigFileApplicationListener.this.searchLocations) {
- for (String extension : new String[] { ".properties", ".yml" }) {
- for (int i = names.length - 1; i >= 0; i--) {
- candidates.add(location + names[i] + extension);
- }
- }
- }
+ public PropertySourceOrderingPostProcessor(ConfigurableApplicationContext context) {
+ this.context = context;
}
- private void addLoadCandidatesFromAnnotations(
- ConfigurableEnvironment environment, ResourceLoader resourceLoader,
- Set candidates) {
- for (String location : ConfigFileApplicationListener.this.annotations
- .getLocations()) {
- Resource resource = resourceLoader.getResource(environment
- .resolvePlaceholders(location));
- if (!ConfigFileApplicationListener.this.annotations
- .ignoreResourceNotFound(location) && !resource.exists()) {
- throw new IllegalStateException("Resource not found: " + location);
- }
- candidates.add(location);
- }
+ @Override
+ public int getOrder() {
+ return Ordered.HIGHEST_PRECEDENCE;
}
@Override
- public Iterator iterator() {
- return this.candidates.iterator();
+ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
+ throws BeansException {
+ reorderSources(this.context.getEnvironment());
+ }
+
+ private void reorderSources(ConfigurableEnvironment environment) {
+ ConfigurationPropertySources.finishAndRelocate(environment
+ .getPropertySources());
+ PropertySource> defaultProperties = environment.getPropertySources()
+ .remove(DEFAULT_PROPERTIES);
+ if (defaultProperties != null) {
+ environment.getPropertySources().addLast(defaultProperties);
+ }
}
}
/**
- * {@link PropertySource} that returns a random value for any property that starts
- * with {@literal "random."}. Return a {@code byte[]} unless the property name ends
- * with {@literal ".int} or {@literal ".long"}.
+ * Loads candidate property sources and configures the active profiles.
*/
- private static class RandomValuePropertySource extends PropertySource {
+ private class Loader {
- public RandomValuePropertySource(String name) {
- super(name, new Random());
- }
+ private final ConfigurableEnvironment environment;
- @Override
- public Object getProperty(String name) {
- if (!name.startsWith("random.")) {
- return null;
- }
- if (name.endsWith("int")) {
- return getSource().nextInt();
- }
- if (name.endsWith("long")) {
- return getSource().nextLong();
- }
- byte[] bytes = new byte[32];
- getSource().nextBytes(bytes);
- return DigestUtils.md5DigestAsHex(bytes);
- }
+ private final ResourceLoader resourceLoader = new DefaultResourceLoader();
- }
+ private PropertySourcesLoader propertiesLoader;
- /**
- * Holds details collected from
- * {@link org.springframework.context.annotation.PropertySource} annotations.
- */
- private static class PropertySourceAnnotations {
+ private Queue profiles;
- private final Collection locations = new LinkedHashSet();
+ private boolean activatedProfiles;
- private final Map names = new HashMap();
+ public Loader(ConfigurableEnvironment environment) {
+ this.environment = environment;
+ }
- private final Map> configs = new HashMap>();
+ public void load() throws IOException {
+ this.propertiesLoader = new PropertySourcesLoader();
+ this.profiles = new LinkedList();
+ this.profiles.add(null);
+ this.profiles.addAll(Arrays.asList(this.environment.getActiveProfiles()));
+ this.activatedProfiles = false;
+ addActiveProfiles(this.environment.getProperty(ACTIVE_PROFILES_PROPERTY));
+
+ while (!this.profiles.isEmpty()) {
+ String profile = this.profiles.poll();
+ for (String location : getSearchLocations()) {
+ for (String name : getSearchNames()) {
+ load(location, name, profile);
+ }
+ }
+ }
- private final Map ignores = new HashMap();
+ addConfigurationProperties(this.propertiesLoader.getPropertySources());
+ }
+
+ private void load(String location, String name, String profile)
+ throws IOException {
- public void addFromSource(Object source) {
- if (source instanceof Class>) {
- addFromSource((Class>) source);
+ // Try to load directly from the location
+ PropertySource> locationPropertySource = load(location, profile);
+
+ // If that fails, try a search
+ if (locationPropertySource == null) {
+ for (String ext : this.propertiesLoader.getAllFileExtensions()) {
+ if (profile != null) {
+ // Try the profile specific file (with a null profile section)
+ load(location + name + "-" + profile + "." + ext, null);
+ }
+ // Try the profile (if any) specific section of the normal file
+ load(location + name + "." + ext, profile);
+ }
}
}
- private void addFromSource(Class> source) {
- for (org.springframework.context.annotation.PropertySource propertySource : AnnotationUtils
- .getRepeatableAnnotation(source, PropertySources.class,
- org.springframework.context.annotation.PropertySource.class)) {
- add(source, propertySource);
+ private PropertySource> load(String resourceLocation, String profile)
+ throws IOException {
+ Resource resource = this.resourceLoader.getResource(resourceLocation);
+ if (resource != null && resource.exists()) {
+ String name = "applicationConfig: " + resource.getDescription();
+ if (StringUtils.hasLength(profile)) {
+ name += " " + profile;
+ }
+ PropertySource> propertySource = this.propertiesLoader.load(resource,
+ name, profile);
+ if (propertySource != null) {
+ addActiveProfiles(propertySource
+ .getProperty(ACTIVE_PROFILES_PROPERTY));
+ }
+ return propertySource;
}
+ return null;
}
- private void add(Class> source,
- org.springframework.context.annotation.PropertySource annotation) {
- this.locations.addAll(Arrays.asList(annotation.value()));
- if (StringUtils.hasText(annotation.name())) {
- for (String location : annotation.value()) {
- this.names.put(location, annotation.name());
+ private void addActiveProfiles(Object property) {
+ String profiles = (property == null ? null : property.toString());
+ boolean profilesNotActivatedWhenCalled = !this.activatedProfiles;
+ for (String profile : asResolvedSet(profiles, null)) {
+ boolean addition = profile.startsWith("+");
+ profile = (addition ? profile.substring(1) : profile);
+ if (profilesNotActivatedWhenCalled || addition) {
+ this.profiles.add(profile);
+ this.environment.addActiveProfile(profile);
+ this.activatedProfiles = true;
}
}
- for (String location : annotation.value()) {
- boolean reallyIgnore = annotation.ignoreResourceNotFound();
- if (this.ignores.containsKey(location)) {
- // Only if they all ignore this location will it be ignored
- reallyIgnore &= this.ignores.get(location);
+ }
+
+ public Set getSearchLocations() {
+ Set locations = new LinkedHashSet();
+ locations.addAll(asResolvedSet(
+ ConfigFileApplicationListener.this.searchLocations,
+ DEFAULT_SEARCH_LOCATIONS));
+ if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
+ for (String path : asResolvedSet(
+ this.environment.getProperty(CONFIG_LOCATION_PROPERTY), null)) {
+ if (!path.contains("$")) {
+ if (!path.contains(":")) {
+ path = "file:" + path;
+ }
+ path = StringUtils.cleanPath(path);
+ }
+ locations.add(path);
}
- this.ignores.put(location, reallyIgnore);
- this.configs.put(location, source);
}
+ return locations;
+ }
+
+ public Set getSearchNames() {
+ if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
+ return asResolvedSet(this.environment.getProperty(CONFIG_NAME_PROPERTY),
+ null);
+ }
+ return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
+ }
+
+ private Set asResolvedSet(String value, String fallback) {
+ List list = Arrays.asList(StringUtils
+ .commaDelimitedListToStringArray(value != null ? this.environment
+ .resolvePlaceholders(value) : fallback));
+ Collections.reverse(list);
+ return new LinkedHashSet(list);
}
- public Class> configuration(String location) {
- return this.configs.get(location);
+ private void addConfigurationProperties(MutablePropertySources sources) {
+ List> reorderedSources = new ArrayList>();
+ for (PropertySource> item : sources) {
+ reorderedSources.add(item);
+ }
+ Collections.reverse(reorderedSources);
+ this.environment.getPropertySources().addLast(
+ new ConfigurationPropertySources(reorderedSources));
}
- public boolean ignoreResourceNotFound(String location) {
- return Boolean.TRUE.equals(this.ignores.get(location));
+ }
+
+ /**
+ * Holds the configuration {@link PropertySource}s as they are loaded can relocate
+ * them once configuration classes have been processed.
+ */
+ static class ConfigurationPropertySources extends PropertySource