diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/NoSuchBeanDefinitionException.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/NoSuchBeanDefinitionException.java index ab04476e7b..fe2cd7f987 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/factory/NoSuchBeanDefinitionException.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/NoSuchBeanDefinitionException.java @@ -53,6 +53,15 @@ public class NoSuchBeanDefinitionException extends BeansException { this.beanName = name; } + /** + * Create a new NoSuchBeanDefinitionException. + * @param type required type of bean + */ + public NoSuchBeanDefinitionException(Class type) { + super("No unique bean of type [" + type.getName() + "] is defined"); + this.beanType = type; + } + /** * Create a new NoSuchBeanDefinitionException. * @param type required type of bean @@ -62,7 +71,7 @@ public class NoSuchBeanDefinitionException extends BeansException { super("No unique bean of type [" + type.getName() + "] is defined: " + message); this.beanType = type; } - + /** * Create a new NoSuchBeanDefinitionException. * @param type required type of bean diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/Bean.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/Bean.java index cc8c218784..96f1b778f0 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/Bean.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/Bean.java @@ -59,9 +59,11 @@ import org.springframework.beans.factory.annotation.Autowire; * @author Chris Beams * @since 3.0 * @see Configuration + * @see DependsOn * @see Lazy * @see Primary - * @see org.springframework.context.annotation.Scope + * @see Scope + * @see Value */ @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) 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 2ff1770541..fe38ac92f9 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 @@ -17,6 +17,7 @@ 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; @@ -51,10 +52,11 @@ import org.springframework.stereotype.Component; * @author Rod Johnson * @author Chris Beams * @since 3.0 - * @see Bean + * @see Import * @see Lazy - * @see Value + * @see Bean * @see ConfigurationClassPostProcessor; + * @see ConfigurationClassApplicationContext; */ @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @@ -62,4 +64,19 @@ import org.springframework.stereotype.Component; @Component public @interface Configuration { + /** + * Explicitly specify the name of the Spring bean definition associated + * with this Configuration class. If left unspecified (the common case), + * a bean name will be automatically generated. + * + *

The custom name applies only if the Configuration class is picked up via + * component scanning or supplied directly to a {@link ConfigurationClassApplicationContext}. + * If the Configuration class is registered as a traditional XML bean definition, + * the name/id of the bean element will take precedence. + * + * @return the specified bean name, if any + * @see org.springframework.beans.factory.support.DefaultBeanNameGenerator + */ + String value() default ""; + } 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 new file mode 100644 index 0000000000..97c97ea512 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassApplicationContext.java @@ -0,0 +1,159 @@ +/* + * 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.context.annotation; + +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.config.BeanDefinition; +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.support.AbstractRefreshableApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + + +/** + * Standalone application context, accepting {@link Configuration}-annotated + * class literals as input. Useful for test harnesses or any other scenario + * where XML-based configuration is unnecessary or undesired. + * + *

In case of multiple Configuration classes, {@link Bean} + * methods defined in later classes will override those defined in earlier + * classes. This can be leveraged to deliberately override certain bean + * definitions via an extra Configuration class. + * + * @author Chris Beams + * @since 3.0 + * @see Configuration + */ +public class ConfigurationClassApplicationContext extends AbstractRefreshableApplicationContext { + + private final Set> configClasses = new LinkedHashSet>(); + + /** + * 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() + */ + public ConfigurationClassApplicationContext(Class... configClasses) { + 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 + * {@link ConfigurationClassApplicationContext#refresh()} must be called in + * order for the context to process the new class. Calls to + * {@link #addConfigurationClass(Class)} are idempotent; adding the same + * Configuration class more than once has no additional effect. + * @param configClass new Configuration class to be processed. + * @see #ConfigurationClassApplicationContext(Class...) + * @see #refresh() + */ + public void addConfigurationClass(Class configClass) { + Assert.notNull( + AnnotationUtils.findAnnotation(configClass, Configuration.class), + "Class [" + configClass.getName() + "] is not annotated with @Configuration"); + this.configClasses.add(configClass); + } + + /** + * Register a {@link BeanDefinition} for each {@link Configuration @Configuration} + * 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); + } + + 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. + * @return bean matching required type + * @throws NoSuchBeanDefinitionException if there is not exactly one matching bean + * found + * @see org.springframework.beans.factory.ListableBeanFactory#getBeansOfType(Class) + * @see org.springframework.beans.factory.BeanFactory#getBean(String, Class) + */ + @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.context/src/test/java/org/springframework/context/annotation/ConfigurationClassApplicationContextTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/ConfigurationClassApplicationContextTests.java new file mode 100644 index 0000000000..85c17b8e20 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/ConfigurationClassApplicationContextTests.java @@ -0,0 +1,231 @@ +/* + * 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.context.annotation; + +import static java.lang.String.format; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +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); + fail("expected exception"); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), + equalTo("Class [" + ConfigMissingAnnotation.class.getName() + "] " + + "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(); + context.addConfigurationClass(Config.class); + context.refresh(); + context.getBean("testBean"); + context.addConfigurationClass(NameConfig.class); + context.refresh(); + context.getBean("name"); + } + + @Test + public void getBeanByType() { + ConfigurationClassApplicationContext context = new ConfigurationClassApplicationContext(Config.class); + TestBean testBean = context.getBean(TestBean.class); + 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 + */ + @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'. + */ + @Test + 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 { + Object bean = context.getBean(targetType); + fail("should have thrown NoSuchBeanDefinitionException, instead got: " + bean); + } catch (NoSuchBeanDefinitionException ex) { + assertThat(ex.getMessage(), equalTo( + format("No unique bean of type [%s] is defined", targetType.getName()))); + } + } + + @Test + public void getBeanByTypeAmbiguityRaisesException() { + ConfigurationClassApplicationContext context = new ConfigurationClassApplicationContext(TwoTestBeanConfig.class); + + try { + context.getBean(TestBean.class); + } catch (RuntimeException ex) { + assertThat(ex.getMessage(), equalTo( + "No unique bean of type [" + TestBean.class.getName() + "] is defined: " + + "2 matching bean definitions found (tb1,tb2). Consider qualifying with " + + "getBean(Class beanType, String beanName) or declaring one bean definition as " + + "@" + Primary.class.getSimpleName())); + } + } + + @Test + public void autowiringIsEnabledByDefault() { + ConfigurationClassApplicationContext context = new ConfigurationClassApplicationContext(AutowiredConfig.class); + assertThat(context.getBean(TestBean.class).name, equalTo("foo")); + } + + + @Configuration + static class Config { + @Bean + public TestBean testBean() { + TestBean testBean = new TestBean(); + testBean.name = "foo"; + return testBean; + } + } + + @Configuration("customConfigBeanName") + static class ConfigWithCustomName { + @Bean + public TestBean testBean() { + 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 { + String name; + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TestBean other = (TestBean) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + + +} \ No newline at end of file diff --git a/org.springframework.samples.petclinic/.settings/org.eclipse.jdt.core.prefs b/org.springframework.samples.petclinic/.settings/org.eclipse.jdt.core.prefs index d258d65749..05a6a07cb3 100644 --- a/org.springframework.samples.petclinic/.settings/org.eclipse.jdt.core.prefs +++ b/org.springframework.samples.petclinic/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,12 @@ -#Wed Jul 15 00:01:30 PDT 2009 +#Sun Oct 04 15:30:45 PDT 2009 eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.source=1.5