From fd8935ba0babaace88b7987293c1f95891aee1f3 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Mon, 5 Oct 2009 05:27:30 +0000 Subject: [PATCH] SPR-5682: * polishing for ConfigurationClassApplicationContext & tests * added ConfigurationClassWebApplicationContext & tests * next: refactoring out duplications between ConfigurationClassApplicationContext & ConfigurationClassWebApplicationContext --- .../ConfigurationClassApplicationContext.java | 28 ++-- ...AbstractRefreshableApplicationContext.java | 9 +- ...igurationClassApplicationContextTests.java | 62 ++++----- ...nfigurationClassWebApplicationContext.java | 128 ++++++++++++++++++ ...rationClassWebApplicationContextTests.java | 98 ++++++++++++++ 5 files changed, 275 insertions(+), 50 deletions(-) create mode 100644 org.springframework.web/src/main/java/org/springframework/web/context/support/ConfigurationClassWebApplicationContext.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/context/support/ConfigurationClassWebApplicationContextTests.java diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassApplicationContext.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassApplicationContext.java index b9fd40fa06..01d68da30c 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassApplicationContext.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassApplicationContext.java @@ -53,13 +53,13 @@ public class ConfigurationClassApplicationContext extends AbstractRefreshableApp private final Set> configClasses = new LinkedHashSet>(); /** - * Create a new {@link ConfigurationClassApplicationContext}, loading bean + * Create a new {@link ConfigurationClassApplicationContext}, loading bean * definitions from the given {@literal configClasses} and automatically * refreshing the context.

Note: if zero classes are specified, the * context will not be refreshed automatically, assuming that * the user will subsequently call {@link #addConfigurationClass(Class)} * and then manually refresh. - * + * * @param configClasses zero or more {@link Configuration} classes * @see #addConfigurationClass(Class) * @see #refresh() @@ -68,14 +68,14 @@ public class ConfigurationClassApplicationContext extends AbstractRefreshableApp if (configClasses.length == 0) { return; } - + for (Class configClass : configClasses) { addConfigurationClass(configClass); } - + this.refresh(); } - + /** * Add a {@link Configuration} class to be processed. Allows for programmatically * building a {@link ConfigurationClassApplicationContext}. Note that @@ -99,22 +99,22 @@ public class ConfigurationClassApplicationContext extends AbstractRefreshableApp * class specified. Enables the default set of annotation configuration post * processors, such that {@literal @Autowired}, {@literal @Required}, and associated * annotations can be used within Configuration classes. - * + * *

Configuration class bean definitions are registered with generated bean definition names. - * + * * @see AnnotationConfigUtils#registerAnnotationConfigProcessors(org.springframework.beans.factory.support.BeanDefinitionRegistry) * @see ConfigurationClassPostProcessor */ @Override protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws IOException, BeansException { - + // @Autowired and friends must be enabled by default when processing @Configuration classes AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory); - + for (Class configClass : configClasses) { AbstractBeanDefinition def = BeanDefinitionBuilder.rootBeanDefinition(configClass).getBeanDefinition(); - + String name = AnnotationUtils.findAnnotation(configClass, Configuration.class).value(); if (!StringUtils.hasLength(name)) { name = new DefaultBeanNameGenerator().generateBeanName(def, beanFactory); @@ -122,13 +122,13 @@ public class ConfigurationClassApplicationContext extends AbstractRefreshableApp beanFactory.registerBeanDefinition(name, def); } - + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); } /** * Return the bean instance that matches the given object type. - * + * * @param * @param requiredType type the bean must match; can be an interface or superclass. * {@literal null} is disallowed. @@ -141,9 +141,9 @@ public class ConfigurationClassApplicationContext extends AbstractRefreshableApp @SuppressWarnings("unchecked") public T getBean(Class requiredType) { Assert.notNull(requiredType, "requiredType may not be null"); - + Map beansOfType = this.getBeansOfType(requiredType); - + switch (beansOfType.size()) { case 0: throw new NoSuchBeanDefinitionException(requiredType); diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java b/org.springframework.context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java index a49812b1db..805923c67f 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java +++ b/org.springframework.context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java @@ -28,7 +28,7 @@ import org.springframework.core.LocalVariableTableParameterNameDiscoverer; /** * Base class for {@link org.springframework.context.ApplicationContext} - * implementations which are supposed to support multiple refreshs, + * implementations which are supposed to support multiple calls to {@literal refresh}, * creating a new internal bean factory instance every time. * Typically (but not necessarily), such a context will be driven by * a set of config locations to load bean definitions from. @@ -48,7 +48,9 @@ import org.springframework.core.LocalVariableTableParameterNameDiscoverer; *

Concrete standalone subclasses of this base class, reading in a * specific bean definition format, are {@link ClassPathXmlApplicationContext} * and {@link FileSystemXmlApplicationContext}, which both derive from the - * common {@link AbstractXmlApplicationContext} base class. + * common {@link AbstractXmlApplicationContext} base class; + * {@link org.springframework.context.annotation.ConfigurationClassApplicationContext} + * supports {@literal @Configuration}-annotated classes as a source of bean definitions. * * @author Juergen Hoeller * @since 1.1.3 @@ -58,6 +60,7 @@ import org.springframework.core.LocalVariableTableParameterNameDiscoverer; * @see AbstractXmlApplicationContext * @see ClassPathXmlApplicationContext * @see FileSystemXmlApplicationContext + * @see org.springframework.context.annotation.ConfigurationClassApplicationContext */ public abstract class AbstractRefreshableApplicationContext extends AbstractApplicationContext { @@ -130,7 +133,7 @@ public abstract class AbstractRefreshableApplicationContext extends AbstractAppl } } catch (IOException ex) { - throw new ApplicationContextException("I/O error parsing XML document for " + getDisplayName(), ex); + throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex); } } diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/ConfigurationClassApplicationContextTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/ConfigurationClassApplicationContextTests.java index d27d3dc757..6278ce6eee 100644 --- a/org.springframework.context/src/test/java/org/springframework/context/annotation/ConfigurationClassApplicationContextTests.java +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/ConfigurationClassApplicationContextTests.java @@ -20,30 +20,28 @@ import static java.lang.String.format; import static org.hamcrest.CoreMatchers.*; import static org.junit.matchers.JUnitMatchers.*; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import org.junit.Test; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; public class ConfigurationClassApplicationContextTests { - + @Test(expected=IllegalStateException.class) public void emptyConstructorRequiresManualRefresh() { ConfigurationClassApplicationContext context = new ConfigurationClassApplicationContext(); context.getBean("foo"); } - + @Test public void classesMissingConfigurationAnnotationAddedToContextAreDisallowed() { ConfigurationClassApplicationContext ctx = new ConfigurationClassApplicationContext(Config.class); - + // should be fine ctx.addConfigurationClass(ConfigWithCustomName.class); - + // should cause immediate failure (no refresh necessary) try { ctx.addConfigurationClass(ConfigMissingAnnotation.class); @@ -54,19 +52,18 @@ public class ConfigurationClassApplicationContextTests { "is not annotated with @Configuration")); } } - + @Test(expected=IllegalArgumentException.class) public void classesMissingConfigurationAnnotationSuppliedToConstructorAreDisallowed() { new ConfigurationClassApplicationContext(ConfigMissingAnnotation.class); } - - + @Test(expected=IllegalArgumentException.class) public void nullGetBeanParameterIsDisallowed() { ConfigurationClassApplicationContext context = new ConfigurationClassApplicationContext(Config.class); context.getBean((Class)null); } - + @Test public void addConfigurationClass() { ConfigurationClassApplicationContext context = new ConfigurationClassApplicationContext(); @@ -77,7 +74,7 @@ public class ConfigurationClassApplicationContextTests { context.refresh(); context.getBean("name"); } - + @Test public void getBeanByType() { ConfigurationClassApplicationContext context = new ConfigurationClassApplicationContext(Config.class); @@ -85,7 +82,7 @@ public class ConfigurationClassApplicationContextTests { assertNotNull("getBean() should not return null", testBean); assertThat(testBean.name, equalTo("foo")); } - + /** * Tests that Configuration classes are registered according to convention * @see org.springframework.beans.factory.support.DefaultBeanNameGenerator#generateBeanName @@ -93,12 +90,12 @@ public class ConfigurationClassApplicationContextTests { @Test public void defaultConfigClassBeanNameIsGeneratedProperly() { ConfigurationClassApplicationContext context = new ConfigurationClassApplicationContext(Config.class); - + // attempt to retrieve the instance by its generated bean name Config configObject = (Config) context.getBean(Config.class.getName() + "#0"); assertNotNull(configObject); } - + /** * Tests that specifying @Configuration(value="foo") results in registering * the configuration class with bean name 'foo'. @@ -107,17 +104,17 @@ public class ConfigurationClassApplicationContextTests { public void explicitConfigClassBeanNameIsRespected() { ConfigurationClassApplicationContext context = new ConfigurationClassApplicationContext(ConfigWithCustomName.class); - + // attempt to retrieve the instance by its specified name ConfigWithCustomName configObject = (ConfigWithCustomName) context.getBean("customConfigBeanName"); assertNotNull(configObject); } - + @Test public void getBeanByTypeRaisesNoSuchBeanDefinitionException() { ConfigurationClassApplicationContext context = new ConfigurationClassApplicationContext(Config.class); - + // attempt to retrieve a bean that does not exist Class targetType = java.util.regex.Pattern.class; try { @@ -128,12 +125,12 @@ public class ConfigurationClassApplicationContextTests { format("No unique bean of type [%s] is defined", targetType.getName()))); } } - + @SuppressWarnings("unchecked") @Test public void getBeanByTypeAmbiguityRaisesException() { ConfigurationClassApplicationContext context = new ConfigurationClassApplicationContext(TwoTestBeanConfig.class); - + try { context.getBean(TestBean.class); } catch (RuntimeException ex) { @@ -148,14 +145,14 @@ public class ConfigurationClassApplicationContextTests { ); } } - + @Test public void autowiringIsEnabledByDefault() { ConfigurationClassApplicationContext context = new ConfigurationClassApplicationContext(AutowiredConfig.class); assertThat(context.getBean(TestBean.class).name, equalTo("foo")); } - - + + @Configuration static class Config { @Bean @@ -165,7 +162,7 @@ public class ConfigurationClassApplicationContextTests { return testBean; } } - + @Configuration("customConfigBeanName") static class ConfigWithCustomName { @Bean @@ -173,37 +170,37 @@ public class ConfigurationClassApplicationContextTests { return new TestBean(); } } - + static class ConfigMissingAnnotation { @Bean public TestBean testBean() { return new TestBean(); } } - + @Configuration static class TwoTestBeanConfig { @Bean TestBean tb1() { return new TestBean(); } @Bean TestBean tb2() { return new TestBean(); } } - + @Configuration static class NameConfig { @Bean String name() { return "foo"; } } - + @Configuration @Import(NameConfig.class) static class AutowiredConfig { @Autowired String autowiredName; - + @Bean TestBean testBean() { TestBean testBean = new TestBean(); testBean.name = autowiredName; return testBean; } } - + } class TestBean { @@ -233,6 +230,5 @@ class TestBean { return false; return true; } - - -} \ No newline at end of file + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/context/support/ConfigurationClassWebApplicationContext.java b/org.springframework.web/src/main/java/org/springframework/web/context/support/ConfigurationClassWebApplicationContext.java new file mode 100644 index 0000000000..866b161d7a --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/context/support/ConfigurationClassWebApplicationContext.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2009 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.web.context.support; + +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultBeanNameGenerator; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ConfigurationClassPostProcessor; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + + +/** + * {@link org.springframework.web.context.WebApplicationContext} implementation + * which takes its configuration from {@link Configuration @Configuration} classes. + * This is essentially the equivalent of + * {@link org.springframework.context.annotation.ConfigurationClassApplicationContext} + * for a web environment. + * + *

To make use of this application context, the "contextClass" context-param for + * ContextLoader and/or "contextClass" init-param for FrameworkServlet must be set to + * the fully-qualified name of this class. + * + *

Unlike {@link XmlWebApplicationContext}, no default configuration class locations + * are assumed. Rather, it is a requirement to set the "contextConfigLocation" + * context-param for ContextLoader and/or "contextConfigLocation" init-param for + * FrameworkServlet. If these params are not set, an IllegalArgumentException will be thrown. + * + *

Note: In case of multiple {@literal @Configuration} classes, later {@literal @Bean} + * definitions will override ones defined in earlier loaded files. This can be leveraged + * to deliberately override certain bean definitions via an extra Configuration class. + * + * @author Chris Beams + * @see org.springframework.context.annotation.ConfigurationClassApplicationContext + */ +public class ConfigurationClassWebApplicationContext extends AbstractRefreshableWebApplicationContext { + + /** + * @throws IllegalArgumentException if configLocations array is null or empty + * @throws IOException if any one configLocation is not loadable as a class + * @throws IllegalArgumentException if any one loaded class is not annotated with {@literal @Configuration} + * @see #getConfigLocations() + */ + @Override + protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) + throws IOException, BeansException { + + Assert.notEmpty(getConfigLocations(), + "No config locations were specified. Is the 'contextConfigLocations' " + + "context-param and/or init-param set properly in web.xml?"); + + Set> configClasses = new LinkedHashSet>(); + + for (String configLocation : getConfigLocations()) { + try { + Class configClass = ClassUtils.getDefaultClassLoader().loadClass(configLocation); + if (AnnotationUtils.findAnnotation(configClass, Configuration.class) == null) { + throw new IllegalArgumentException("Class [" + configClass.getName() + "] is not annotated with @Configuration"); + } + configClasses.add(configClass); + } catch (ClassNotFoundException ex) { + throw new IOException("Could not load @Configuration class [" + configLocation + "]", ex); + } + } + + // @Autowired and friends must be enabled by default when processing @Configuration classes + AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory); + + for (Class configClass : configClasses) { + AbstractBeanDefinition def = BeanDefinitionBuilder.rootBeanDefinition(configClass).getBeanDefinition(); + + String name = AnnotationUtils.findAnnotation(configClass, Configuration.class).value(); + if (!StringUtils.hasLength(name)) { + name = new DefaultBeanNameGenerator().generateBeanName(def, beanFactory); + } + + beanFactory.registerBeanDefinition(name, def); + } + + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + } + + @SuppressWarnings("unchecked") + public T getBean(Class requiredType) { + Assert.notNull(requiredType, "requiredType may not be null"); + + Map beansOfType = this.getBeansOfType(requiredType); + + switch (beansOfType.size()) { + case 0: + throw new NoSuchBeanDefinitionException(requiredType); + case 1: + return (T) beansOfType.values().iterator().next(); + default: + throw new NoSuchBeanDefinitionException(requiredType, + beansOfType.size() + " matching bean definitions found " + + "(" + StringUtils.collectionToCommaDelimitedString(beansOfType.keySet()) + "). " + + "Consider qualifying with getBean(Class beanType, String beanName) or " + + "declaring one bean definition as @Primary"); + } + } +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/context/support/ConfigurationClassWebApplicationContextTests.java b/org.springframework.web/src/test/java/org/springframework/web/context/support/ConfigurationClassWebApplicationContextTests.java new file mode 100644 index 0000000000..b67cadf608 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/context/support/ConfigurationClassWebApplicationContextTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2009 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.web.context.support; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.junit.matchers.JUnitMatchers.containsString; + +import java.io.IOException; + +import org.junit.Test; +import org.springframework.context.ApplicationContextException; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +public class ConfigurationClassWebApplicationContextTests { + + @Test + public void testSingleWellFormedConfigLocation() { + ConfigurationClassWebApplicationContext ctx = new ConfigurationClassWebApplicationContext(); + ctx.setConfigLocation(Config.class.getName()); + ctx.refresh(); + + TestBean bean = ctx.getBean(TestBean.class); + assertNotNull(bean); + } + + @Test + public void testWithoutExplicitlySettingConfigLocations() { + ConfigurationClassWebApplicationContext ctx = new ConfigurationClassWebApplicationContext(); + try { + ctx.refresh(); + fail("expected exception"); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), containsString( + "Is the 'contextConfigLocations' context-param " + + "and/or init-param set properly in web.xml?")); + } + } + + @Test + public void testMalformedConfigLocation() { + ConfigurationClassWebApplicationContext ctx = new ConfigurationClassWebApplicationContext(); + ctx.setConfigLocation("garbage"); + try { + ctx.refresh(); + fail("expected exception"); + } catch (ApplicationContextException ex) { + assertThat(ex.getCause(), is(IOException.class)); + assertThat(ex.getCause().getMessage(), + containsString("Could not load @Configuration class")); + } + } + + @Test + public void testNonConfigurationClass() { + ConfigurationClassWebApplicationContext ctx = new ConfigurationClassWebApplicationContext(); + ctx.setConfigLocation(NotConfigurationAnnotated.class.getName()); + try { + ctx.refresh(); + fail("expected exception"); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), + containsString("is not annotated with @Configuration")); + } + } + + @Configuration + static class Config { + @Bean + public TestBean testBean() { + return new TestBean(); + } + } + + static class NotConfigurationAnnotated { } + + static class TestBean { + + } +}