From 69a762e86ac075035bfcdc899aadf2b36abaf167 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sat, 28 Mar 2009 22:21:50 +0000 Subject: [PATCH] resolved: + Provide @Primary annotation (SPR-5590) + Provide @Lazy annotation (SPR-5591) + Test @Bean initMethod/destroyMethod functionality (SPR-5592) + Test @Bean dependsOn functionality (SPR-5593) --- .../context/annotation/Bean.java | 11 +- .../context/annotation/BeanMethod.java | 8 +- .../context/annotation/Configuration.java | 4 +- .../annotation/ConfigurationClass.java | 60 +++++-- .../annotation/ConfigurationClassVisitor.java | 32 ++-- ...onfigurationModelBeanDefinitionReader.java | 19 ++- .../context/annotation/Lazy.java | 64 +++++++ .../context/annotation/Primary.java | 51 ++++++ ...anAnnotationAttributePropagationTests.java | 157 ++++++++++++++++++ 9 files changed, 360 insertions(+), 46 deletions(-) create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/Lazy.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/Primary.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/BeanAnnotationAttributePropagationTests.java 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 143411d31b6..a4e77a7d4bf 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 @@ -77,14 +77,19 @@ public @interface Bean { /** * The optional name of a method to call on the bean instance during initialization. * Not commonly used, given that the method may be called programmatically directly - * within the Bean method. + * within the body of a Bean-annotated method. */ String initMethod() default ""; /** * The optional name of a method to call on the bean instance during upon closing * the application context, for example a {@literal close()} - * method on a {@literal DataSource}. + * method on a {@literal DataSource}. The method must have no arguments, but may + * throw any exception. + *

Note: Only invoked on beans whose lifecycle is under the full control of the + * factory which is always the case for singletons, but not guaranteed + * for any other scope. + * see {@link org.springframework.context.ConfigurableApplicationContext#close()} */ String destroyMethod() default ""; @@ -93,6 +98,8 @@ public @interface Bean { * created by the container before this bean. Used infrequently in cases where a bean * does not explicitly depend on another through properties or constructor arguments, * but rather depends on the side effects of another bean's initialization. + *

Note: This attribute will not be inherited by child bean definitions, + * hence it needs to be specified per concrete bean definition. */ String[] dependsOn() default {}; diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/BeanMethod.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/BeanMethod.java index 9e2df803430..be9872be493 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/BeanMethod.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/BeanMethod.java @@ -85,10 +85,10 @@ final class BeanMethod implements BeanMetadataElement { * @see #getRequiredAnnotation(Class) */ @SuppressWarnings("unchecked") - public T getAnnotation(Class annoType) { + public A getAnnotation(Class annoType) { for (Annotation anno : annotations) if (anno.annotationType().equals(annoType)) - return (T) anno; + return (A) anno; return null; } @@ -101,7 +101,9 @@ final class BeanMethod implements BeanMetadataElement { public T getRequiredAnnotation(Class annoType) { T anno = getAnnotation(annoType); - Assert.notNull(anno, format("annotation %s not found on %s", annoType.getSimpleName(), this)); + if(anno == null) + throw new IllegalStateException( + format("required annotation %s is not present on %s", annoType.getSimpleName(), this)); return anno; } 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 33e79acbae2..e4609c494cf 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 @@ -55,13 +55,13 @@ import org.springframework.stereotype.Component; * @see Bean * @see Lazy * @see Value - * @see org.springframework.context.annotation.support.ConfigurationClassPostProcessor; + * @see ConfigurationClassPostProcessor; */ -@Component @Target( { ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented +@Component public @interface Configuration { } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java index 507e67fdd33..73d7a42c3e3 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -18,6 +18,7 @@ package org.springframework.context.annotation; import static java.lang.String.*; +import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; import java.util.HashSet; import java.util.Set; @@ -43,7 +44,7 @@ final class ConfigurationClass extends ModelClass { private String beanName; private int modifiers; - private Configuration configurationAnnotation; + private HashSet annotations = new HashSet(); private HashSet methods = new HashSet(); private ConfigurationClass declaringClass; @@ -63,14 +64,38 @@ final class ConfigurationClass extends ModelClass { Assert.isTrue(modifiers >= 0, "modifiers must be non-negative"); this.modifiers = modifiers; } + + public void addAnnotation(Annotation annotation) { + this.annotations.add(annotation); + } + + /** + * @return the annotation on this class matching annoType or + * {@literal null} if not present. + * @see #getRequiredAnnotation(Class) + */ + @SuppressWarnings("unchecked") + public A getAnnotation(Class annoType) { + for (Annotation annotation : annotations) + if(annotation.annotationType().equals(annoType)) + return (A) annotation; - public Configuration getConfigurationAnnotation() { - return this.configurationAnnotation; + return null; } - public void setConfigurationAnnotation(Configuration configAnno) { - Assert.notNull(configAnno, "configuration annotation must be non-null"); - this.configurationAnnotation = configAnno; + /** + * @return the annotation on this class matching annoType + * @throws {@link IllegalStateException} if not present + * @see #getAnnotation(Class) + */ + public A getRequiredAnnotation(Class annoType) { + A anno = getAnnotation(annoType); + + if(anno == null) + throw new IllegalStateException( + format("required annotation %s is not present on %s", annoType.getSimpleName(), this)); + + return anno; } public Set getBeanMethods() { @@ -93,7 +118,7 @@ final class ConfigurationClass extends ModelClass { public void validate(ProblemReporter problemReporter) { // configuration classes must be annotated with @Configuration - if (configurationAnnotation == null) + if (getAnnotation(Configuration.class) == null) problemReporter.error(new NonAnnotatedConfigurationProblem()); // a configuration class may not be final (CGLIB limitation) @@ -113,9 +138,12 @@ final class ConfigurationClass extends ModelClass { public int hashCode() { final int prime = 31; int result = super.hashCode(); - result = prime * result + ((declaringClass == null) ? 0 : declaringClass.hashCode()); - result = prime * result + ((beanName == null) ? 0 : beanName.hashCode()); - result = prime * result + ((configurationAnnotation == null) ? 0 : configurationAnnotation.hashCode()); + result = prime * result + + ((annotations == null) ? 0 : annotations.hashCode()); + result = prime * result + + ((beanName == null) ? 0 : beanName.hashCode()); + result = prime * result + + ((declaringClass == null) ? 0 : declaringClass.hashCode()); result = prime * result + ((methods == null) ? 0 : methods.hashCode()); result = prime * result + modifiers; return result; @@ -130,20 +158,20 @@ final class ConfigurationClass extends ModelClass { if (getClass() != obj.getClass()) return false; ConfigurationClass other = (ConfigurationClass) obj; - if (declaringClass == null) { - if (other.declaringClass != null) + if (annotations == null) { + if (other.annotations != null) return false; - } else if (!declaringClass.equals(other.declaringClass)) + } else if (!annotations.equals(other.annotations)) return false; if (beanName == null) { if (other.beanName != null) return false; } else if (!beanName.equals(other.beanName)) return false; - if (configurationAnnotation == null) { - if (other.configurationAnnotation != null) + if (declaringClass == null) { + if (other.declaringClass != null) return false; - } else if (!configurationAnnotation.equals(other.configurationAnnotation)) + } else if (!declaringClass.equals(other.declaringClass)) return false; if (methods == null) { if (other.methods != null) diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassVisitor.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassVisitor.java index 1a6338fb815..f86b82c589a 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassVisitor.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassVisitor.java @@ -20,6 +20,7 @@ import static java.lang.String.*; import static org.springframework.context.annotation.AsmUtils.*; import static org.springframework.util.ClassUtils.*; +import java.lang.annotation.Annotation; import java.util.HashMap; import java.util.Stack; @@ -105,26 +106,27 @@ class ConfigurationClassVisitor extends ClassAdapter { /** * Visits a class level annotation on a {@link Configuration @Configuration} class. - * Accounts for all possible class-level annotations that are respected by JavaConfig - * including AspectJ's {@code @Aspect} annotation. - *

- * Upon encountering such an annotation, update the {@link #configClass} model object - * appropriately, and then return an {@link AnnotationVisitor} implementation that can - * populate the annotation appropriately with data. + * + *

Upon encountering such an annotation, updates the {@link #configClass} model + * object appropriately, and then returns an {@link AnnotationVisitor} implementation + * that can populate the annotation appropriately with its attribute data as parsed + * by ASM. * * @see MutableAnnotation + * @see Configuration + * @see Lazy + * @see Import */ @Override public AnnotationVisitor visitAnnotation(String annoTypeDesc, boolean visible) { String annoTypeName = convertAsmTypeDescriptorToClassName(annoTypeDesc); + Class annoClass = loadToolingSafeClass(annoTypeName, classLoader); - if (Configuration.class.getName().equals(annoTypeName)) { - Configuration mutableConfiguration = createMutableAnnotation(Configuration.class, classLoader); - configClass.setConfigurationAnnotation(mutableConfiguration); - return new MutableAnnotationVisitor(mutableConfiguration, classLoader); - } + if (annoClass == null) + // annotation was unable to be loaded -> probably Spring IDE unable to load a user-defined annotation + return super.visitAnnotation(annoTypeDesc, visible); - if (Import.class.getName().equals(annoTypeName)) { + if (Import.class.equals(annoClass)) { ImportStack importStack = ImportStackHolder.getImportStack(); if (!importStack.contains(configClass)) { @@ -135,7 +137,9 @@ class ConfigurationClassVisitor extends ClassAdapter { problemReporter.error(new CircularImportProblem(configClass, importStack)); } - return super.visitAnnotation(annoTypeDesc, visible); + Annotation mutableAnnotation = createMutableAnnotation(annoClass, classLoader); + configClass.addAnnotation(mutableAnnotation); + return new MutableAnnotationVisitor(mutableAnnotation, classLoader); } /** @@ -187,7 +191,7 @@ class ConfigurationClassVisitor extends ClassAdapter { innerConfigClass.setDeclaringClass(innerClasses.get(outerName)); // is the inner class a @Configuration class? If so, add it to the list - if (innerConfigClass.getConfigurationAnnotation() != null) + if (innerConfigClass.getAnnotation(Configuration.class) != null) innerClasses.put(name, innerConfigClass); } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationModelBeanDefinitionReader.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationModelBeanDefinitionReader.java index 3590ffe124b..9c1dba09a30 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationModelBeanDefinitionReader.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationModelBeanDefinitionReader.java @@ -155,15 +155,16 @@ class ConfigurationModelBeanDefinitionReader { } } - // TODO: re-enable for Lazy support - // // is this bean marked as primary for disambiguation? - // if (bean.primary() == Primary.TRUE) - // beanDef.setPrimary(true); - // - // // is this bean lazily instantiated? - // if ((bean.lazy() == Lazy.TRUE) - // || ((bean.lazy() == Lazy.UNSPECIFIED) && (defaults.defaultLazy() == Lazy.TRUE))) - // beanDef.setLazyInit(true); + if (method.getAnnotation(Primary.class) != null) + beanDef.setPrimary(true); + + // is this bean to be instantiated lazily? + Lazy defaultLazy = configClass.getAnnotation(Lazy.class); + if (defaultLazy != null) + beanDef.setLazyInit(defaultLazy.value()); + Lazy lazy = method.getAnnotation(Lazy.class); + if (lazy != null) + beanDef.setLazyInit(lazy.value()); // does this bean have a custom init-method specified? String initMethodName = bean.initMethod(); diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/Lazy.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/Lazy.java new file mode 100644 index 00000000000..c42b4530402 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/Lazy.java @@ -0,0 +1,64 @@ +/* + * 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.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * Indicates whether a bean is to be lazily initialized. + * + *

May be used on any class directly or indirectly annotated with + * {@link org.springframework.stereotype.Component} or on methods annotated with + * {@link Bean}. + * + *

If this annotation is not present on a Component or Bean definition, eager + * initialization will occur. If present and set to {@literal true}, the + * Bean/Component will not be initialized until referenced by another bean or + * explicitly retrieved from the enclosing + * {@link org.springframework.beans.factory.BeanFactory}. If present and set to + * {@literal false}, the bean will be instantiated on startup by bean factories + * that perform eager initialization of singletons. + * + *

If Lazy is present on a {@link Configuration} class, this indicates that all + * {@link Bean} methods within that {@literal Configuration} should be lazily + * initialized. If Lazy is present and false on a Bean method within a + * Lazy-annotated Configuration class, this indicates overriding the 'default + * lazy' behavior and that the bean should be eagerly initialized. + * + * @author Chris Beams + * @since 3.0 + * @see Primary + * @see Bean + * @see Configuration + * @see org.springframework.stereotype.Component + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Lazy { + + /** + * Whether lazy initialization should occur. + */ + boolean value() default true; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/Primary.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/Primary.java new file mode 100644 index 00000000000..2a29b92109a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/Primary.java @@ -0,0 +1,51 @@ +/* + * 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.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * Indicates that a bean should be given preference when multiple candidates + * are qualified to autowire a single-valued dependency. If exactly one 'primary' + * bean exists among the candidates, it will be the autowired value. + * + *

May be used on any class directly or indirectly annotated with + * {@link org.springframework.stereotype.Component} or on methods annotated + * with {@link Bean}. + * + *

Using {@link Primary} at the class level has no effect unless component-scanning + * is being used. If a {@link Primary}-annotated class is declared via XML, + * {@link Primary} annotation metadata is ignored, and + * {@literal } is respected instead. + * + * @author Chris Beams + * @since 3.0 + * @see Lazy + * @see Bean + * @see org.springframework.stereotype.Component + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Primary { + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/BeanAnnotationAttributePropagationTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/BeanAnnotationAttributePropagationTests.java new file mode 100644 index 00000000000..9f772c91eb5 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/BeanAnnotationAttributePropagationTests.java @@ -0,0 +1,157 @@ +/* + * 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.configuration; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ConfigurationClassPostProcessor; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Primary; + + +/** + * Unit tests proving that the various attributes available via the {@link Bean} + * annotation are correctly reflected in the {@link BeanDefinition} created when + * processing the {@link Configuration} class. + * + *

Also includes tests proving that using {@link Lazy} and {@link Primary} + * annotations in conjunction with Bean propagate their respective metadata + * correctly into the resulting BeanDefinition + * + * @author Chris Beams + */ +@SuppressWarnings("unused") // for unused @Bean methods in local classes +public class BeanAnnotationAttributePropagationTests { + + @Test + public void initMethodMetadataIsPropagated() { + @Configuration class Config { + @Bean(initMethod="start") Object foo() { return null; } + } + + assertEquals("init method name was not propagated", + "start", beanDef(Config.class).getInitMethodName()); + } + + @Test + public void destroyMethodMetadataIsPropagated() { + @Configuration class Config { + @Bean(destroyMethod="destroy") Object foo() { return null; } + } + + assertEquals("destroy method name was not propagated", + "destroy", beanDef(Config.class).getDestroyMethodName()); + } + + @Test + public void dependsOnMetadataIsPropagated() { + @Configuration class Config { + @Bean(dependsOn={"bar", "baz"}) Object foo() { return null; } + } + + assertArrayEquals("dependsOn metadata was not propagated", + new String[] {"bar", "baz"}, beanDef(Config.class).getDependsOn()); + } + + @Test + public void primaryMetadataIsPropagated() { + @Configuration class Config { + @Primary @Bean + Object foo() { return null; } + } + + assertTrue("primary metadata was not propagated", + beanDef(Config.class).isPrimary()); + } + + @Test + public void primaryMetadataIsFalseByDefault() { + @Configuration class Config { + @Bean Object foo() { return null; } + } + + assertFalse("@Bean methods should be non-primary by default", + beanDef(Config.class).isPrimary()); + } + + @Test + public void lazyMetadataIsPropagated() { + @Configuration class Config { + @Lazy @Bean + Object foo() { return null; } + } + + assertTrue("lazy metadata was not propagated", + beanDef(Config.class).isLazyInit()); + } + + @Test + public void lazyMetadataIsFalseByDefault() { + @Configuration class Config { + @Bean Object foo() { return null; } + } + + assertFalse("@Bean methods should be non-lazy by default", + beanDef(Config.class).isLazyInit()); + } + + @Test + public void defaultLazyConfigurationPropagatesToIndividualBeans() { + @Lazy @Configuration class Config { + @Bean Object foo() { return null; } + } + + assertTrue("@Bean methods declared in a @Lazy @Configuration should be lazily instantiated", + beanDef(Config.class).isLazyInit()); + } + + @Test + public void eagerBeanOverridesDefaultLazyConfiguration() { + @Lazy @Configuration class Config { + @Lazy(false) @Bean Object foo() { return null; } + } + + assertFalse("@Lazy(false) @Bean methods declared in a @Lazy @Configuration should be eagerly instantiated", + beanDef(Config.class).isLazyInit()); + } + + @Test + public void eagerConfigurationProducesEagerBeanDefinitions() { + @Lazy(false) @Configuration class Config { // will probably never happen, doesn't make much sense + @Bean Object foo() { return null; } + } + + assertFalse("@Lazy(false) @Configuration should produce eager bean definitions", + beanDef(Config.class).isLazyInit()); + } + + private AbstractBeanDefinition beanDef(Class configClass) { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("config", new RootBeanDefinition(configClass)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(factory); + + return (AbstractBeanDefinition) factory.getBeanDefinition("foo"); + } + +}