Skip to content

Commit

Permalink
Introduce @propertysource
Browse files Browse the repository at this point in the history
Allows a convenient mechanism for contributing a PropertySource to the
enclosing Spring Environment. See @propertysource Javadoc for
complete details and PropertySourceAnnotationTests for examples.

Issue: SPR-8314
  • Loading branch information
cbeams committed May 11, 2011
1 parent 314a054 commit c8bc54e
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,21 @@
import java.util.Set;
import java.util.Stack;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.parsing.Location;
import org.springframework.beans.factory.parsing.Problem;
import org.springframework.beans.factory.parsing.ProblemReporter;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.ResourcePropertySource;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.MethodMetadata;
import org.springframework.core.type.StandardAnnotationMetadata;
Expand Down Expand Up @@ -64,6 +70,8 @@
*/
class ConfigurationClassParser {

private static final Log logger = LogFactory.getLog(ConfigurationClassParser.class);

private final MetadataReaderFactory metadataReaderFactory;

private final ProblemReporter problemReporter;
Expand All @@ -73,6 +81,9 @@ class ConfigurationClassParser {
private final Set<ConfigurationClass> configurationClasses =
new LinkedHashSet<ConfigurationClass>();

private final Stack<PropertySource<?>> propertySources =
new Stack<PropertySource<?>>();

private final Environment environment;

private final ResourceLoader resourceLoader;
Expand Down Expand Up @@ -152,6 +163,18 @@ protected void processConfigurationClass(ConfigurationClass configClass) throws
}

protected void doProcessConfigurationClass(ConfigurationClass configClass, AnnotationMetadata metadata) throws IOException {
Map<String, Object> propertySourceAttributes =
metadata.getAnnotationAttributes(org.springframework.context.annotation.PropertySource.class.getName());
if (propertySourceAttributes != null) {
String name = (String) propertySourceAttributes.get("name");
String location = (String) propertySourceAttributes.get("value");
ClassLoader classLoader = this.resourceLoader.getClassLoader();
ResourcePropertySource ps = StringUtils.hasText(name) ?
new ResourcePropertySource(name, location, classLoader) :
new ResourcePropertySource(location, classLoader);
this.propertySources.push(ps);
}

Map<String, Object> componentScanAttributes = metadata.getAnnotationAttributes(ComponentScan.class.getName());
if (componentScanAttributes != null) {
// the config class is annotated with @ComponentScan -> perform the scan immediately
Expand Down Expand Up @@ -236,6 +259,10 @@ public Set<ConfigurationClass> getConfigurationClasses() {
return this.configurationClasses;
}

public Stack<PropertySource<?>> getPropertySources() {
return this.propertySources;
}

public ImportRegistry getImportRegistry() {
return this.importStack;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
Expand Down Expand Up @@ -49,7 +50,10 @@
import org.springframework.context.annotation.ConfigurationClassParser.ImportRegistry;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
Expand Down Expand Up @@ -242,6 +246,19 @@ public void processConfigBeanDefinitions(ConfigurationClassParser parser, Config
}
parser.validate();

// Handle any @PropertySource annotations
if (!(this.environment instanceof ConfigurableEnvironment)) {
logger.warn("Ignoring @PropertySource annotations. " +
"Reason: Environment must implement ConfigurableEnvironment");
}
else {
MutablePropertySources envPropertySources = ((ConfigurableEnvironment)this.environment).getPropertySources();
Stack<PropertySource<?>> parsedPropertySources = parser.getPropertySources();
while (!parsedPropertySources.isEmpty()) {
envPropertySources.addLast(parsedPropertySources.pop());
}
}

// Read the model and create bean definitions based on its content
reader.loadBeanDefinitions(parser.getConfigurationClasses());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 2002-2011 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.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation providing a convenient and declarative mechanism for adding a
* {@link org.springframework.core.env.PropertySource PropertySource} to Spring's
* {@link org.springframework.core.env.Environment Environment}. To be used in
* conjunction with @{@link Configuration} classes.
*
* <h3>Example usage</h3>
* <p>Given a file {@code app.properties} containing the key/value pair
* {@code testbean.name=myTestBean}, the following {@code @Configuration} class
* uses {@code @PropertySource} to contribute {@code app.properties} to the
* {@code Environment}'s set of {@code PropertySources}.
*
* <pre>
* &#064;Configuration
* &#064;PropertySource("classpath:/com/myco/app.properties")
* public class AppConfig {
* &#064;Autowired
* Environment env;
*
* &#064;Bean
* public TestBean testBean() {
* TestBean testBean = new TestBean();
* testBean.setName(env.getProperty("testbean.name"));
* return testBean;
* }
* }</pre>
*
* Notice that the {@code Environment} object is @{@link Autowired} into the
* configuration class and then used when populating the {@code TestBean}
* object. Given the configuration above, a call to {@code testBean.getName()} will
* return "myTestBean".
*
* <h3>A note on property overriding with @PropertySource</h3>
* In cases where a given property key exists in more than one {@code .properties}
* file, the last {@code @PropertySource} annotation processed will 'win' and override.
*
* For example, given two properties files {@code a.properties} and
* {@code b.properties}, consider the following two configuration classes
* that reference them with {@code @PropertySource} annotations:
*
* <pre>
* &#064;Configuration
* &#064;PropertySource("classpath:/com/myco/a.properties")
* public class ConfigA { }
*
* &#064;Configuration
* &#064;PropertySource("classpath:/com/myco/b.properties")
* public class ConfigB { }
* </pre>
*
* The override ordering depends on the order in which these classes are registered
* with the application context.
* <pre>
* AnnotationConfigApplicationContext ctx =
* new AnnotationConfigApplicationContext();
* ctx.register(ConfigA.class);
* ctx.register(ConfigB.class);
* ctx.refresh();
* </pre>
*
* In the scenario above, the properties in {@code b.properties} will override any
* duplicates that exist in {@code a.properties}, because {@code ConfigB} was registered
* last.
*
* <p>In certain situations, it may not be possible or practical to tightly control
* property source ordering when using {@code @ProperySource} annotations. For example,
* if the {@code @Configuration} classes above were registered via component-scanning,
* the ordering is difficult to predict. In such cases - and if overriding is important -
* it is recommended that the user fall back to using the programmatic PropertySource API.
* See {@link org.springframework.core.env.ConfigurableEnvironment ConfigurableEnvironment} and
* {@link org.springframework.core.env.MutablePropertySources MutablePropertySources} Javadoc
* for details.
*
* @author Chris Beams
* @since 3.1
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PropertySource {

/**
* Indicate the name of this PropertySource. If omitted, a name
* will be generated based on the description of the underlying
* resource.
* @see org.springframework.core.env.PropertySource#getName()
* @see org.springframework.core.io.Resource#getDescription()
*/
String name() default "";

/**
* Indicate the resource location of the properties file to be loaded.
* For example, {@code "classpath:/com/myco/app.properties"} or
* {@code "file:/path/to/file"}. Note that resource location wildcards
* are not permitted, and that a location must evaluate to exactly one
* {@code .properties} resource.
*/
String value();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 2002-2011 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.context.annotation;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

import java.util.Iterator;

import javax.inject.Inject;

import org.junit.Test;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;

import test.beans.TestBean;

/**
* Tests the processing of @PropertySource annotations on @Configuration classes.
*
* @author Chris Beams
* @since 3.1
*/
public class PropertySourceAnnotationTests {

@Test
public void withExplicitName() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ConfigWithExplicitName.class);
ctx.refresh();
assertTrue("property source p1 was not added",
ctx.getEnvironment().getPropertySources().contains("p1"));
assertThat(ctx.getBean(TestBean.class).getName(), equalTo("p1TestBean"));

// assert that the property source was added last to the set of sources
String name;
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
Iterator<org.springframework.core.env.PropertySource<?>> iterator = sources.iterator();
do {
name = iterator.next().getName();
}
while(iterator.hasNext());

assertThat(name, is("p1"));
}

@Test
public void withImplicitName() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ConfigWithImplicitName.class);
ctx.refresh();
assertTrue("property source p1 was not added",
ctx.getEnvironment().getPropertySources().contains("class path resource [org/springframework/context/annotation/p1.properties]"));
assertThat(ctx.getBean(TestBean.class).getName(), equalTo("p1TestBean"));
}

/**
* Tests the LIFO behavior of @PropertySource annotaitons.
* The last one registered should 'win'.
*/
@Test
public void orderingIsLifo() {
{
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ConfigWithImplicitName.class, P2Config.class);
ctx.refresh();
// p2 should 'win' as it was registered last
assertThat(ctx.getBean(TestBean.class).getName(), equalTo("p2TestBean"));
}

{
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(P2Config.class, ConfigWithImplicitName.class);
ctx.refresh();
// p1 should 'win' as it was registered last
assertThat(ctx.getBean(TestBean.class).getName(), equalTo("p1TestBean"));
}
}


@Configuration
@PropertySource(name="p1", value="classpath:org/springframework/context/annotation/p1.properties")
static class ConfigWithExplicitName {
@Inject Environment env;

@Bean
public TestBean testBean() {
return new TestBean(env.getProperty("testbean.name"));
}
}


@Configuration
@PropertySource("classpath:org/springframework/context/annotation/p1.properties")
static class ConfigWithImplicitName {
@Inject Environment env;

@Bean
public TestBean testBean() {
return new TestBean(env.getProperty("testbean.name"));
}
}


@Configuration
@PropertySource("classpath:org/springframework/context/annotation/p2.properties")
static class P2Config {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testbean.name=p1TestBean
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testbean.name=p2TestBean

0 comments on commit c8bc54e

Please sign in to comment.