[bs-168] Support convenient binding of @Bean to external source

@ConfigurationProperties now has a path() attribute that can be used
to specify a resource location explicitly.

[Fixes #51968657]
This commit is contained in:
Dave Syer 2013-07-11 13:05:33 +01:00
parent 15ba11f302
commit d5aad97d1f
13 changed files with 483 additions and 148 deletions

View File

@ -38,8 +38,9 @@ import org.springframework.validation.ObjectError;
import org.springframework.validation.Validator;
/**
* Validate some {@link Properties} by binding them to an object of a specified type and
* then optionally running a {@link Validator} over it.
* Validate some {@link Properties} (or optionally {@link PropertySources}) by binding
* them to an object of a specified type and then optionally running a {@link Validator}
* over it.
*
* @author Dave Syer
*/
@ -195,15 +196,13 @@ public class PropertiesConfigurationFactory<T> implements FactoryBean<T>,
if (this.logger.isTraceEnabled()) {
if (this.properties != null) {
this.logger.trace("Properties:\n" + this.properties);
}
else {
} else {
this.logger.trace("Property Sources: " + this.propertySources);
}
}
this.hasBeenBound = true;
doBindPropertiesToTarget();
}
catch (BindException ex) {
} catch (BindException ex) {
if (this.exceptionIfInvalid) {
throw ex;
}

View File

@ -0,0 +1,40 @@
/*
* 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.bootstrap.config;
import java.util.Properties;
import org.springframework.bootstrap.config.YamlProcessor.DocumentMatcher;
import org.springframework.bootstrap.config.YamlProcessor.MatchStatus;
/**
* A {@link DocumentMatcher} that matches the default profile implicitly but not
* explicitly (i.e. matches if "spring.profiles" is not found and not otherwise).
*
* @author Dave Syer
*
*/
public final class DefaultProfileDocumentMatcher implements DocumentMatcher {
@Override
public MatchStatus matches(Properties properties) {
if (!properties.containsKey("spring.profiles")) {
return MatchStatus.FOUND;
} else {
return MatchStatus.NOT_FOUND;
}
}
}

View File

@ -0,0 +1,52 @@
/*
* 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.bootstrap.config;
import java.io.IOException;
import java.util.Properties;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
/**
* Strategy to load '.properties' files into a {@link PropertySource}.
*/
public class PropertiesPropertySourceLoader implements PropertySourceLoader {
@Override
public boolean supports(Resource resource) {
return resource.getFilename().endsWith(".properties");
}
@Override
public PropertySource<?> load(Resource resource, Environment environment) {
try {
Properties properties = loadProperties(resource, environment);
return new PropertiesPropertySource(resource.getDescription(), properties);
} catch (IOException ex) {
throw new IllegalStateException("Could not load properties from " + resource,
ex);
}
}
protected Properties loadProperties(Resource resource, Environment environment)
throws IOException {
return PropertiesLoaderUtils.loadProperties(resource);
}
}

View File

@ -0,0 +1,38 @@
/*
* 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.bootstrap.config;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
/**
* Strategy interface used to load a {@link PropertySource}.
*/
public interface PropertySourceLoader {
/**
* @return Is this resource supported?
*/
public boolean supports(Resource resource);
/**
* Load the resource into a property source.
* @return a property source
*/
PropertySource<?> load(Resource resource, Environment environment);
}

View File

@ -0,0 +1,49 @@
/*
* 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.bootstrap.config;
import java.util.Properties;
import org.springframework.bootstrap.config.YamlProcessor.ArrayDocumentMatcher;
import org.springframework.bootstrap.config.YamlProcessor.DocumentMatcher;
import org.springframework.bootstrap.config.YamlProcessor.MatchStatus;
import org.springframework.core.env.Environment;
/**
* @author Dave Syer
*
*/
public class SpringProfileDocumentMatcher implements DocumentMatcher {
private final Environment environment;
/**
* @param environment
*/
public SpringProfileDocumentMatcher(Environment environment) {
this.environment = environment;
}
@Override
public MatchStatus matches(Properties properties) {
String[] profiles = this.environment.getActiveProfiles();
if (profiles.length == 0) {
profiles = new String[] { "default" };
}
return new ArrayDocumentMatcher("spring.profiles", profiles).matches(properties);
}
}

View File

@ -0,0 +1,81 @@
/*
* 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.bootstrap.config;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import org.springframework.bootstrap.config.YamlProcessor.DocumentMatcher;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
/**
* Strategy to load '.yml' files into a {@link PropertySource}.
*/
public class YamlPropertySourceLoader extends PropertiesPropertySourceLoader {
private List<DocumentMatcher> matchers;
/**
* A property source loader that loads all properties and matches all documents.
*
* @return a property source loader
*/
public static YamlPropertySourceLoader matchAllLoader() {
return new YamlPropertySourceLoader();
}
/**
* A property source loader that matches documents that have no explicit profile or
* which have an explicit "spring.profiles.active" value in the current active
* profiles.
*
* @return a property source loader
*/
public static YamlPropertySourceLoader springProfileAwareLoader(
Environment environment) {
return new YamlPropertySourceLoader(new SpringProfileDocumentMatcher(environment),
new DefaultProfileDocumentMatcher());
}
/**
* @param matchers
*/
public YamlPropertySourceLoader(DocumentMatcher... matchers) {
this.matchers = Arrays.asList(matchers);
}
@Override
public boolean supports(Resource resource) {
return resource.getFilename().endsWith(".yml");
}
@Override
protected Properties loadProperties(final Resource resource,
final Environment environment) throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
if (this.matchers != null && !this.matchers.isEmpty()) {
factory.setMatchDefault(false);
factory.setDocumentMatchers(this.matchers);
}
factory.setResources(new Resource[] { resource });
return factory.getObject();
}
}

View File

@ -16,26 +16,21 @@
package org.springframework.bootstrap.context.initializer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import org.springframework.bootstrap.config.YamlPropertiesFactoryBean;
import org.springframework.bootstrap.config.YamlProcessor.ArrayDocumentMatcher;
import org.springframework.bootstrap.config.YamlProcessor.DocumentMatcher;
import org.springframework.bootstrap.config.YamlProcessor.MatchStatus;
import org.springframework.bootstrap.config.SpringProfileDocumentMatcher;
import org.springframework.bootstrap.config.PropertiesPropertySourceLoader;
import org.springframework.bootstrap.config.PropertySourceLoader;
import org.springframework.bootstrap.config.YamlPropertySourceLoader;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.Ordered;
import org.springframework.core.env.CommandLinePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.util.StringUtils;
/**
@ -69,8 +64,6 @@ import org.springframework.util.StringUtils;
public class ConfigFileApplicationContextInitializer implements
ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
private static final Loader[] LOADERS = { new PropertiesLoader(), new YamlLoader() };
private static final String LOCATION_VARIABLE = "${spring.config.location}";
private String[] searchLocations = new String[] { "classpath:", "file:./",
@ -100,11 +93,9 @@ public class ConfigFileApplicationContextInitializer implements
private List<String> getCandidateLocations() {
List<String> candidates = new ArrayList<String>();
for (String searchLocation : this.searchLocations) {
for (Loader loader : LOADERS) {
for (String extension : loader.getExtensions()) {
String location = searchLocation + this.name + extension;
candidates.add(location);
}
for (String extension : new String[] { ".properties", ".yml" }) {
String location = searchLocation + this.name + extension;
candidates.add(location);
}
}
candidates.add(LOCATION_VARIABLE);
@ -113,16 +104,30 @@ public class ConfigFileApplicationContextInitializer implements
private void load(ConfigurableApplicationContext applicationContext, String location,
String profile) {
location = applicationContext.getEnvironment().resolvePlaceholders(location);
ConfigurableEnvironment environment = applicationContext.getEnvironment();
location = environment.resolvePlaceholders(location);
String suffix = "." + StringUtils.getFilenameExtension(location);
if (StringUtils.hasLength(profile)) {
location = location.replace(suffix, "-" + profile + suffix);
}
for (Loader loader : LOADERS) {
if (loader.getExtensions().contains(suffix.toLowerCase())) {
Resource resource = applicationContext.getResource(location);
if (resource != null && resource.exists()) {
loader.load(resource, applicationContext);
PropertySourceLoader[] loaders = {
new PropertiesPropertySourceLoader(),
new YamlPropertySourceLoader(new SpringProfileDocumentMatcher(environment),
new ProfileSettingDocumentMatcher(environment)) };
for (PropertySourceLoader loader : loaders) {
Resource resource = applicationContext.getResource(location);
if (resource != null && resource.exists() && loader.supports(resource)) {
PropertySource<?> propertySource = loader.load(resource, environment);
MutablePropertySources propertySources = environment.getPropertySources();
if (propertySources
.contains(CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME)) {
propertySources.addAfter(
CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME,
propertySource);
} else {
propertySources.addFirst(propertySource);
}
return;
}
@ -152,118 +157,4 @@ public class ConfigFileApplicationContextInitializer implements
this.searchLocations = (searchLocations == null ? null : searchLocations.clone());
}
/**
* Strategy interface used to load a {@link PropertySource}.
*/
private static interface Loader {
/**
* @return The supported extensions (including '.' and in lowercase)
*/
public Set<String> getExtensions();
/**
* Load the resource into the destination application context.
*/
void load(Resource resource, ConfigurableApplicationContext applicationContext);
}
/**
* Strategy to load '.properties' files.
*/
private static class PropertiesLoader implements Loader {
@Override
public Set<String> getExtensions() {
return Collections.singleton(".properties");
}
@Override
public void load(Resource resource,
ConfigurableApplicationContext applicationContext) {
try {
Properties properties = loadProperties(resource, applicationContext);
MutablePropertySources propertySources = applicationContext
.getEnvironment().getPropertySources();
if (propertySources
.contains(CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME)) {
propertySources.addAfter(
CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME,
new PropertiesPropertySource(resource.getDescription(),
properties));
}
else {
propertySources.addFirst(new PropertiesPropertySource(resource
.getDescription(), properties));
}
}
catch (IOException ex) {
throw new IllegalStateException("Could not load properties file from "
+ resource, ex);
}
}
protected Properties loadProperties(Resource resource,
ConfigurableApplicationContext applicationContext) throws IOException {
return PropertiesLoaderUtils.loadProperties(resource);
}
}
/**
* Strategy to load '.yml' files.
*/
private static class YamlLoader extends PropertiesLoader {
@Override
public Set<String> getExtensions() {
return Collections.singleton(".yml");
}
@Override
protected Properties loadProperties(final Resource resource,
final ConfigurableApplicationContext applicationContext)
throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
List<DocumentMatcher> matchers = new ArrayList<DocumentMatcher>();
matchers.add(new DocumentMatcher() {
@Override
public MatchStatus matches(Properties properties) {
String[] profiles = applicationContext.getEnvironment()
.getActiveProfiles();
if (profiles.length == 0) {
profiles = new String[] { "default" };
}
return new ArrayDocumentMatcher("spring.profiles", profiles)
.matches(properties);
}
});
matchers.add(new DocumentMatcher() {
@Override
public MatchStatus matches(Properties properties) {
if (!properties.containsKey("spring.profiles")) {
Set<String> profiles = StringUtils
.commaDelimitedListToSet(properties.getProperty(
"spring.profiles.active", ""));
for (String profile : profiles) {
// allow document with no profile to set the active one
applicationContext.getEnvironment().addActiveProfile(profile);
}
// matches default profile
return MatchStatus.FOUND;
}
else {
return MatchStatus.NOT_FOUND;
}
}
});
factory.setMatchDefault(false);
factory.setDocumentMatchers(matchers);
factory.setResources(new Resource[] { resource });
return factory.getObject();
}
}
}

View File

@ -0,0 +1,60 @@
/*
* 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.bootstrap.context.initializer;
import java.util.Properties;
import java.util.Set;
import org.springframework.bootstrap.config.YamlProcessor.DocumentMatcher;
import org.springframework.bootstrap.config.YamlProcessor.MatchStatus;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
/**
* A {@link DocumentMatcher} that sets the active profile if it finds a document with
* a key <code>spring.profiles.active</code>.
*
* @author Dave Syer
*
*/
public final class ProfileSettingDocumentMatcher implements DocumentMatcher {
private final Environment environment;
public ProfileSettingDocumentMatcher(Environment environment) {
this.environment = environment;
}
@Override
public MatchStatus matches(Properties properties) {
if (!properties.containsKey("spring.profiles")) {
Set<String> profiles = StringUtils.commaDelimitedListToSet(properties
.getProperty("spring.profiles.active", ""));
if (this.environment instanceof ConfigurableEnvironment) {
ConfigurableEnvironment configurable = (ConfigurableEnvironment) this.environment;
for (String profile : profiles) {
// allow document with no profile to set the active one
configurable.addActiveProfile(profile);
}
}
// matches default profile
return MatchStatus.FOUND;
} else {
return MatchStatus.NOT_FOUND;
}
}
}

View File

@ -70,4 +70,12 @@ public @interface ConfigurationProperties {
*/
boolean ignoreUnknownFields() default true;
/**
* Optionally provide an explicit resource path to bind to instead of using the
* default environment.
*
* @return the path (or paths) of resources to bind to
*/
String[] path() default {};
}

View File

@ -23,11 +23,23 @@ import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.bootstrap.bind.PropertiesConfigurationFactory;
import org.springframework.bootstrap.config.PropertiesPropertySourceLoader;
import org.springframework.bootstrap.config.PropertySourceLoader;
import org.springframework.bootstrap.config.YamlPropertySourceLoader;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.PropertySources;
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.util.StringUtils;
import org.springframework.validation.Validator;
@ -38,7 +50,7 @@ import org.springframework.validation.Validator;
* @author Dave Syer
*/
public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,
BeanFactoryAware {
BeanFactoryAware, ResourceLoaderAware, EnvironmentAware {
private PropertySources propertySources;
@ -52,6 +64,10 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
private boolean initialized = false;
private ResourceLoader resourceLoader = new DefaultResourceLoader();
private Environment environment = new StandardEnvironment();
/**
* @param propertySources
*/
@ -78,6 +94,16 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
this.beanFactory = beanFactory;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
@ -101,7 +127,12 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
.getTarget() : bean);
PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<Object>(
target);
factory.setPropertySources(this.propertySources);
if (annotation != null && annotation.path().length != 0) {
factory.setPropertySources(loadPropertySources(annotation.path()));
} else {
factory.setPropertySources(this.propertySources);
}
factory.setValidator(this.validator);
// If no explicit conversion service is provided we add one so that (at least)
// comma-separated arrays of convertibles can be bound automatically
@ -118,12 +149,31 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
}
try {
factory.bindPropertiesToTarget();
}
catch (Exception ex) {
} catch (Exception ex) {
throw new BeanCreationException(beanName, "Could not bind properties", ex);
}
}
private PropertySources loadPropertySources(String[] path) {
MutablePropertySources propertySources = new MutablePropertySources();
PropertySourceLoader[] loaders = { new PropertiesPropertySourceLoader(),
YamlPropertySourceLoader.springProfileAwareLoader(this.environment) };
for (String location : path) {
location = this.environment.resolvePlaceholders(location);
Resource resource = this.resourceLoader.getResource(location);
if (resource != null && resource.exists()) {
for (PropertySourceLoader loader : loaders) {
if (loader.supports(resource)) {
PropertySource<?> propertySource = loader.load(resource,
this.environment);
propertySources.addFirst(propertySource);
}
}
}
}
return propertySources;
}
private ConversionService getDefaultConversionService() {
if (!this.initialized && this.beanFactory instanceof ListableBeanFactory) {
for (Converter<?, ?> converter : ((ListableBeanFactory) this.beanFactory)

View File

@ -120,6 +120,49 @@ public class EnableConfigurationPropertiesTests {
assertEquals("bar", this.context.getBean(TestProperties.class).getName());
}
@Test
public void testBindingDirectlyToFile() {
this.context.register(ResourceBindingProperties.class, TestConfiguration.class);
this.context.refresh();
assertEquals(1,
this.context.getBeanNamesForType(ResourceBindingProperties.class).length);
assertEquals("foo", this.context.getBean(ResourceBindingProperties.class)
.getName());
}
@Test
public void testBindingDirectlyToFileResolvedFromEnvironment() {
TestUtils.addEnviroment(this.context, "binding.location:classpath:other.yml");
this.context.register(ResourceBindingProperties.class, TestConfiguration.class);
this.context.refresh();
assertEquals(1,
this.context.getBeanNamesForType(ResourceBindingProperties.class).length);
assertEquals("other", this.context.getBean(ResourceBindingProperties.class)
.getName());
}
@Test
public void testBindingDirectlyToFileWithDefaultsWhenProfileNotFound() {
this.context.register(ResourceBindingProperties.class, TestConfiguration.class);
this.context.getEnvironment().addActiveProfile("nonexistent");
this.context.refresh();
assertEquals(1,
this.context.getBeanNamesForType(ResourceBindingProperties.class).length);
assertEquals("foo", this.context.getBean(ResourceBindingProperties.class)
.getName());
}
@Test
public void testBindingDirectlyToFileWithExplicitSpringProfile() {
this.context.register(ResourceBindingProperties.class, TestConfiguration.class);
this.context.getEnvironment().addActiveProfile("super");
this.context.refresh();
assertEquals(1,
this.context.getBeanNamesForType(ResourceBindingProperties.class).length);
assertEquals("bar", this.context.getBean(ResourceBindingProperties.class)
.getName());
}
@Test
public void testBindingWithTwoBeans() {
this.context.register(MoreConfiguration.class, TestConfiguration.class);
@ -246,4 +289,16 @@ public class EnableConfigurationPropertiesTests {
}
}
@ConfigurationProperties(path = "${binding.location:classpath:name.yml}")
protected static class ResourceBindingProperties {
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

@ -0,0 +1,10 @@
---
name: foo
---
spring.profiles: super
name: bar
---
spring.profiles: other
name: spam

View File

@ -0,0 +1,2 @@
---
name: other