From c8bc54e0ccad49f0785856f911349283619eb8ba Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Wed, 11 May 2011 13:28:33 +0000 Subject: [PATCH] Introduce @PropertySource 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 --- .../annotation/ConfigurationClassParser.java | 27 ++++ .../ConfigurationClassPostProcessor.java | 17 +++ .../context/annotation/PropertySource.java | 125 ++++++++++++++++++ .../PropertySourceAnnotationTests.java | 125 ++++++++++++++++++ .../context/annotation/p1.properties | 1 + .../context/annotation/p2.properties | 1 + 6 files changed, 296 insertions(+) create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/PropertySource.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/p1.properties create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/p2.properties diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index b826eb9d88e..0885b72a9d7 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -27,6 +27,8 @@ import java.util.Map; 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; @@ -34,8 +36,12 @@ 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; @@ -64,6 +70,8 @@ import org.springframework.util.StringUtils; */ class ConfigurationClassParser { + private static final Log logger = LogFactory.getLog(ConfigurationClassParser.class); + private final MetadataReaderFactory metadataReaderFactory; private final ProblemReporter problemReporter; @@ -73,6 +81,9 @@ class ConfigurationClassParser { private final Set configurationClasses = new LinkedHashSet(); + private final Stack> propertySources = + new Stack>(); + private final Environment environment; private final ResourceLoader resourceLoader; @@ -152,6 +163,18 @@ class ConfigurationClassParser { } protected void doProcessConfigurationClass(ConfigurationClass configClass, AnnotationMetadata metadata) throws IOException { + Map 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 componentScanAttributes = metadata.getAnnotationAttributes(ComponentScan.class.getName()); if (componentScanAttributes != null) { // the config class is annotated with @ComponentScan -> perform the scan immediately @@ -236,6 +259,10 @@ class ConfigurationClassParser { return this.configurationClasses; } + public Stack> getPropertySources() { + return this.propertySources; + } + public ImportRegistry getImportRegistry() { return this.importStack; } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index 23746ef31cd..b94a4d1fb40 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -21,6 +21,7 @@ import java.util.LinkedHashMap; 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; @@ -49,7 +50,10 @@ import org.springframework.context.ResourceLoaderAware; 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; @@ -242,6 +246,19 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo } 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> 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()); diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/PropertySource.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/PropertySource.java new file mode 100644 index 00000000000..d41e0b46452 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/PropertySource.java @@ -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. + * + *

Example usage

+ *

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}. + * + *

+ * @Configuration
+ * @PropertySource("classpath:/com/myco/app.properties")
+ * public class AppConfig {
+ *     @Autowired
+ *     Environment env;
+ *
+ *     @Bean
+ *     public TestBean testBean() {
+ *         TestBean testBean = new TestBean();
+ *         testBean.setName(env.getProperty("testbean.name"));
+ *         return testBean;
+ *     }
+ * }
+ * + * 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". + * + *

A note on property overriding with @PropertySource

+ * 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: + * + *
+ * @Configuration
+ * @PropertySource("classpath:/com/myco/a.properties")
+ * public class ConfigA { }
+ *
+ * @Configuration
+ * @PropertySource("classpath:/com/myco/b.properties")
+ * public class ConfigB { }
+ * 
+ * + * The override ordering depends on the order in which these classes are registered + * with the application context. + *
+ * AnnotationConfigApplicationContext ctx =
+ *     new AnnotationConfigApplicationContext();
+ * ctx.register(ConfigA.class);
+ * ctx.register(ConfigB.class);
+ * ctx.refresh();
+ * 
+ * + * 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. + * + *

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(); + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java new file mode 100644 index 00000000000..5efa03d3f59 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java @@ -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> 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 { + } +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/p1.properties b/org.springframework.context/src/test/java/org/springframework/context/annotation/p1.properties new file mode 100644 index 00000000000..55a32e793f9 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/p1.properties @@ -0,0 +1 @@ +testbean.name=p1TestBean \ No newline at end of file diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/p2.properties b/org.springframework.context/src/test/java/org/springframework/context/annotation/p2.properties new file mode 100644 index 00000000000..141de759548 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/p2.properties @@ -0,0 +1 @@ +testbean.name=p2TestBean \ No newline at end of file