From 95b1dbadb0014e791b93d73254903a683bd49433 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sat, 21 May 2011 01:20:33 +0000 Subject: [PATCH] Register nested @Configuration classes automatically The following is now possible: @Configuration public class AppConfig { @Inject DataSource dataSource; @Bean public MyBean myBean() { return new MyBean(dataSource); } @Configuration static class DatabaseConfig { @Bean DataSource dataSource() { return new EmbeddedDatabaseBuilder().build(); } } } public static void main(String... args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); ctx.getBean(MyBean.class); // works ctx.getBean(DataSource.class); // works } Notice that the @Import annotation was not used and that only AppConfig was registered against the context. By virtue of the fact that DatabaseConfig is a member class of AppConfig, it is automatically registered when AppConfig is registered. This avoids an awkward and redundant @Import annotation when the relationship is already implicitly clear. See @Configuration Javadoc for details. Issue: SPR-8186 --- .../context/annotation/Configuration.java | 31 ++++ .../annotation/ConfigurationClassParser.java | 18 +++ .../annotation/ConfigurationClassUtils.java | 19 ++- .../NestedConfigurationClassTests.java | 141 ++++++++++++++++++ 4 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/NestedConfigurationClassTests.java diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/Configuration.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/Configuration.java index a730a96c477..58091101fad 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/Configuration.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/Configuration.java @@ -232,6 +232,37 @@ import org.springframework.stereotype.Component; * } * } * + *

With nested {@code @Configuration} classes

+ * {@code @Configuration} classes may be nested within one another as follows: + *
+ * @Configuration
+ * public class AppConfig {
+ *     @Inject DataSource dataSource;
+ *
+ *     @Bean
+ *     public MyBean myBean() {
+ *         return new MyBean(dataSource);
+ *     }
+ *
+ *     @Configuration
+ *     static class DatabaseConfig {
+ *         @Bean
+ *         DataSource dataSource() {
+ *             return new EmbeddedDatabaseBuilder().build();
+ *         }
+ *     }
+ * }
+ * + * When bootstrapping such an arrangement, only {@code AppConfig} need be registered + * against the application context. By virtue of being a nested {@code @Configuration} + * class, {@code DatabaseConfig} will be registered automatically. This avoids + * the need to use an {@code @Import} annotation when the relationship between + * {@code AppConfig} {@code DatabaseConfig} is already implicitly clear. + * + *

Note also that nested {@code @Configuration} classes can be used to good effect + * with the {@code @Profile} annotation to provide two options of the same bean to the + * enclosing {@code @Configuration} class. + * *

Configuring lazy initialization

*

By default, {@code @Bean} methods will be eagerly instantiated at container * bootstrap time. To avoid this, {@code @Configuration} may be used in conjunction with 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 43d567a57bb..e00c6b24859 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 @@ -16,6 +16,8 @@ package org.springframework.context.annotation; +import static org.springframework.context.annotation.ConfigurationClassUtils.isConfigurationCandidate; + import java.io.IOException; import java.util.Collections; import java.util.Comparator; @@ -157,6 +159,17 @@ class ConfigurationClassParser { } protected void doProcessConfigurationClass(ConfigurationClass configClass, AnnotationMetadata metadata) throws IOException { + + // recursively process any member (nested) classes first + for (String memberClassName : metadata.getMemberClassNames()) { + MetadataReader reader = this.metadataReaderFactory.getMetadataReader(memberClassName); + AnnotationMetadata memberClassMetadata = reader.getAnnotationMetadata(); + if (isConfigurationCandidate(memberClassMetadata)) { + processConfigurationClass(new ConfigurationClass(reader, null)); + } + } + + // process any @PropertySource annotations Map propertySourceAttributes = metadata.getAnnotationAttributes(org.springframework.context.annotation.PropertySource.class.getName()); if (propertySourceAttributes != null) { @@ -169,6 +182,7 @@ class ConfigurationClassParser { this.propertySources.push(ps); } + // process any @ComponentScan annotions Map componentScanAttributes = metadata.getAnnotationAttributes(ComponentScan.class.getName()); if (componentScanAttributes != null) { // the config class is annotated with @ComponentScan -> perform the scan immediately @@ -188,12 +202,14 @@ class ConfigurationClassParser { } } + // process any @Import annotations List> allImportAttribs = AnnotationUtils.findAllAnnotationAttributes(Import.class, metadata.getClassName(), true); for (Map importAttribs : allImportAttribs) { processImport(configClass, (String[]) importAttribs.get("value"), true); } + // process any @ImportResource annotations if (metadata.isAnnotated(ImportResource.class.getName())) { String[] resources = (String[]) metadata.getAnnotationAttributes(ImportResource.class.getName()).get("value"); Class readerClass = (Class) metadata.getAnnotationAttributes(ImportResource.class.getName()).get("reader"); @@ -205,6 +221,8 @@ class ConfigurationClassParser { configClass.addImportedResource(resource, readerClass); } } + + // process individual @Bean methods Set beanMethods = metadata.getAnnotatedMethods(Bean.class.getName()); for (MethodMetadata methodMetadata : beanMethods) { configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java index c3906bb56bc..2d192a809c9 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java @@ -79,12 +79,11 @@ abstract class ConfigurationClassUtils { } if (metadata != null) { - if (metadata.isAnnotated(Configuration.class.getName())) { + if (isFullConfigurationCandidate(metadata)) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL); return true; } - else if (metadata.isAnnotated(Component.class.getName()) || - metadata.hasAnnotatedMethods(Bean.class.getName())) { + else if (isLiteConfigurationCandidate(metadata)) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE); return true; } @@ -92,6 +91,20 @@ abstract class ConfigurationClassUtils { return false; } + public static boolean isConfigurationCandidate(AnnotationMetadata metadata) { + return isFullConfigurationCandidate(metadata) || isLiteConfigurationCandidate(metadata); + } + + public static boolean isFullConfigurationCandidate(AnnotationMetadata metadata) { + return metadata.isAnnotated(Configuration.class.getName()); + } + + public static boolean isLiteConfigurationCandidate(AnnotationMetadata metadata) { + return metadata.isAnnotated(Component.class.getName()) || + metadata.hasAnnotatedMethods(Bean.class.getName()); + } + + /** * Determine whether the given bean definition indicates a full @Configuration class. */ diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/NestedConfigurationClassTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/NestedConfigurationClassTests.java new file mode 100644 index 00000000000..fd0adecba09 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/NestedConfigurationClassTests.java @@ -0,0 +1,141 @@ +/* + * 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.is; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; + +import org.junit.Test; + +import test.beans.TestBean; + +/** + * Tests ensuring that nested static @Configuration classes are automatically detected + * and registered without the need for explicit registration or @Import. See SPR-8186. + * + * @author Chris Beams + * @since 3.1 + */ +public class NestedConfigurationClassTests { + + @Test + public void oneLevelDeep() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(L0Config.L1Config.class); + ctx.refresh(); + + assertFalse(ctx.containsBean("l0Bean")); + + ctx.getBean(L0Config.L1Config.class); + ctx.getBean("l1Bean"); + + ctx.getBean(L0Config.L1Config.L2Config.class); + ctx.getBean("l2Bean"); + + // ensure that override order is correct + assertThat(ctx.getBean("overrideBean", TestBean.class).getName(), is("override-l1")); + } + + @Test + public void twoLevelsDeep() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(L0Config.class); + ctx.refresh(); + + ctx.getBean(L0Config.class); + ctx.getBean("l0Bean"); + + ctx.getBean(L0Config.L1Config.class); + ctx.getBean("l1Bean"); + + ctx.getBean(L0Config.L1Config.L2Config.class); + ctx.getBean("l2Bean"); + + // ensure that override order is correct + assertThat(ctx.getBean("overrideBean", TestBean.class).getName(), is("override-l0")); + } + + @Test + public void twoLevelsDeepWithInheritance() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(S1Config.class); + ctx.refresh(); + + ctx.getBean(S1Config.class); + ctx.getBean("l0Bean"); + + ctx.getBean(L0Config.L1Config.class); + ctx.getBean("l1Bean"); + + ctx.getBean(L0Config.L1Config.L2Config.class); + ctx.getBean("l2Bean"); + + // ensure that override order is correct + assertThat(ctx.getBean("overrideBean", TestBean.class).getName(), is("override-s1")); + } + + + @Configuration + static class L0Config { + @Bean + public TestBean l0Bean() { + return new TestBean("l0"); + } + + @Bean + public TestBean overrideBean() { + return new TestBean("override-l0"); + } + + @Configuration + static class L1Config { + @Bean + public TestBean l1Bean() { + return new TestBean("l1"); + } + + @Bean + public TestBean overrideBean() { + return new TestBean("override-l1"); + } + + @Configuration + protected static class L2Config { + @Bean + public TestBean l2Bean() { + return new TestBean("l2"); + } + + @Bean + public TestBean overrideBean() { + return new TestBean("override-l2"); + } + } + } + } + + + @Configuration + static class S1Config extends L0Config { + @Bean + public TestBean overrideBean() { + return new TestBean("override-s1"); + } + } + +} \ No newline at end of file