diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java index cfbc4237141..b660b0355b2 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -24,11 +24,9 @@ import org.springframework.beans.factory.support.AutowireCandidateQualifier; import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; -import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; import org.springframework.core.env.StandardEnvironment; -import org.springframework.core.type.AnnotationMetadata; import org.springframework.util.Assert; /** @@ -39,6 +37,7 @@ import org.springframework.util.Assert; * @author Juergen Hoeller * @author Chris Beams * @author Sam Brannen + * @author Phillip Webb * @since 3.0 * @see AnnotationConfigApplicationContext#register */ @@ -134,12 +133,9 @@ public class AnnotatedBeanDefinitionReader { public void registerBean(Class annotatedClass, String name, Class... qualifiers) { AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(annotatedClass); - AnnotationMetadata metadata = abd.getMetadata(); - if (metadata.isAnnotated(Profile.class.getName())) { - AnnotationAttributes profile = MetadataUtils.attributesFor(metadata, Profile.class); - if (!this.environment.acceptsProfiles(profile.getStringArray("value"))) { - return; - } + if (ConditionalAnnotationHelper.shouldSkip(abd, this.registry, + this.environment, this.beanNameGenerator)) { + return; } ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd); abd.setScope(scopeMetadata.getScopeName()); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java index b3f9450f983..a016b885545 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -52,6 +52,7 @@ import org.springframework.util.PatternMatchUtils; * @author Mark Fisher * @author Juergen Hoeller * @author Chris Beams + * @author Phillip Webb * @since 2.5 * @see AnnotationConfigApplicationContext#scan * @see org.springframework.stereotype.Component @@ -298,6 +299,10 @@ public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateCo * bean definition has been found for the specified name */ protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException { + if (ConditionalAnnotationHelper.shouldSkip(beanDefinition, getRegistry(), + getEnvironment(), this.beanNameGenerator)) { + return false; + } if (!this.registry.containsBeanDefinition(beanName)) { return true; } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Condition.java b/spring-context/src/main/java/org/springframework/context/annotation/Condition.java new file mode 100644 index 00000000000..69543830aab --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Condition.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2013 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 org.jruby.internal.runtime.methods.MethodMethod; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.core.type.AnnotationMetadata; + +/** + * A single {@code condition} that must be {@linkplain #matches matched} in order + * for a component to be registered. + * + *

Conditions are checked immediately before a component bean-definition is due to be + * registered and are free to veto registration based on any criteria that can be + * determined at that point. + * + *

Conditions must follow the same restrictions as {@link BeanFactoryPostProcessor} + * and take care to never interact with bean instances. + * + * @author Phillip Webb + * @since 4.0 + * @see Conditional + * @see ConditionContext + */ +public interface Condition { + + /** + * Determine if the condition matches. + * @param context the condition context + * @param metadata meta-data of the {@link AnnotationMetadata class} or + * {@link MethodMethod method} being checked. + * @return {@code true} if the condition matches and the component can be registered + * or {@code false} to veto registration. + */ + boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConditionContext.java b/spring-context/src/main/java/org/springframework/context/annotation/ConditionContext.java new file mode 100644 index 00000000000..564586501c1 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConditionContext.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2013 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 org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; + +/** + * Context information for use by {@link Condition}s. + * + * @author Phillip Webb + * @since 4.0 + */ +public interface ConditionContext { + + /** + * Returns the {@link BeanDefinitionRegistry} that will hold the bean definition + * should the condition match. + * @return the registry (never {@code null}) + */ + BeanDefinitionRegistry getRegistry(); + + /** + * Return the {@link Environment} for which the current application is running or + * {@code null} if no environment is available. + * @return the environment or {@code null} + */ + Environment getEnvironment(); + + /** + * Returns the {@link ConfigurableListableBeanFactory} that will hold the bean + * definition should the condition match. If a + * {@link ConfigurableListableBeanFactory} is unavailable an + * {@link IllegalStateException} will be thrown. + * @return the bean factory + * @throws IllegalStateException if the bean factory could not be obtained + */ + ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException; + + /** + * Returns the {@link ResourceLoader} currently being used or {@code null} if the + * resource loader cannot be obtained. + * @return a resource loader or {@code null} + */ + ResourceLoader getResourceLoader(); + + /** + * Returns the {@link ClassLoader} that should be used to load additional classes + * or {@code null} if the default classloader should be used. + * @return the classloader or {@code null} + */ + ClassLoader getClassLoader(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Conditional.java b/spring-context/src/main/java/org/springframework/context/annotation/Conditional.java new file mode 100644 index 00000000000..9a987142547 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Conditional.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2013 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.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that a component is is only eligible for registration when all + * {@linkplain #value() specified conditions} match. + * + *

A condition is any state that can be determined programmatically + * immediately before the bean is due to be created (see {@link Condition} for details). + * + *

The {@code @Conditional} annotation may be used in any of the following ways: + *

+ * + *

If a {@code @Configuration} class is marked with {@code @Conditional}, all of the + * {@code @Bean} methods and {@link Import @Import} annotations associated with that class + * will be subject to the conditions. + * + * @author Phillip Webb + * @since 4.0 + * @see Condition + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +public @interface Conditional { + + /** + * All {@link Condition}s that must {@linkplain Condition#matches match} in order for + * the component to be registered. + */ + Class[] value(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConditionalAnnotationHelper.java b/spring-context/src/main/java/org/springframework/context/annotation/ConditionalAnnotationHelper.java new file mode 100644 index 00000000000..acd766ea48b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConditionalAnnotationHelper.java @@ -0,0 +1,212 @@ +/* + * Copyright 2002-2013 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.util.Collections; +import java.util.List; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.Environment; +import org.springframework.core.env.EnvironmentCapable; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.MultiValueMap; + +/** + * Helper class used to determine if registration should be skipped based due to a + * {@code @Conditional} annotation. + * + * @author Phillip Webb + * @since 4.0 + * @see Conditional + */ +abstract class ConditionalAnnotationHelper { + + private static final String CONDITIONAL_ANNOTATION = Conditional.class.getName(); + + + public static boolean shouldSkip(BeanDefinition beanDefinition, + BeanDefinitionRegistry registry, Environment environment, + BeanNameGenerator beanNameGenerator) { + if (hasCondition(getMetadata(beanDefinition))) { + ConditionContextImpl context = new ConditionContextImpl(registry, + environment, beanNameGenerator); + return shouldSkip(getMetadata(beanDefinition), context); + } + return false; + } + + public static boolean shouldSkip(BeanMethod beanMethod, + BeanDefinitionRegistry registry, Environment environment, + BeanNameGenerator beanNameGenerator) { + if (hasCondition(getMetadata(beanMethod))) { + ConditionContextImpl context = new ConditionContextImpl(registry, + environment, beanNameGenerator); + return shouldSkip(getMetadata(beanMethod), context); + } + return false; + } + + public static boolean shouldSkip(ConfigurationClass configurationClass, + BeanDefinitionRegistry registry, Environment environment, + BeanNameGenerator beanNameGenerator) { + if (hasCondition(configurationClass)) { + ConditionContextImpl context = new ConditionContextImpl(registry, + environment, beanNameGenerator); + return shouldSkip(configurationClass, context); + } + return false; + } + + public static boolean shouldSkip(ConfigurationClass configClass, + ConditionContextImpl context) { + if (configClass == null) { + return false; + } + return shouldSkip(configClass.getMetadata(), context); + } + + private static boolean shouldSkip(AnnotatedTypeMetadata metadata, + ConditionContextImpl context) { + if (metadata != null) { + for (String[] conditionClasses : getConditionClasses(metadata)) { + for (String conditionClass : conditionClasses) { + if (!getCondition(conditionClass, context.getClassLoader()).matches( + context, metadata)) { + return true; + } + } + } + } + return false; + } + + private static AnnotatedTypeMetadata getMetadata(BeanMethod beanMethod) { + return (beanMethod == null ? null : beanMethod.getMetadata()); + } + + private static AnnotatedTypeMetadata getMetadata(BeanDefinition beanDefinition) { + if (beanDefinition != null && beanDefinition instanceof AnnotatedBeanDefinition) { + return ((AnnotatedBeanDefinition) beanDefinition).getMetadata(); + } + return null; + } + + private static boolean hasCondition(ConfigurationClass configurationClass) { + if (configurationClass == null) { + return false; + } + return hasCondition(configurationClass.getMetadata()) + || hasCondition(configurationClass.getImportedBy()); + } + + private static boolean hasCondition(AnnotatedTypeMetadata metadata) { + return (metadata != null) && metadata.isAnnotated(CONDITIONAL_ANNOTATION); + } + + @SuppressWarnings("unchecked") + private static List getConditionClasses(AnnotatedTypeMetadata metadata) { + MultiValueMap attributes = metadata.getAllAnnotationAttributes( + CONDITIONAL_ANNOTATION, true); + Object values = attributes == null ? null : attributes.get("value"); + return (List) (values == null ? Collections.emptyList() : values); + } + + private static Condition getCondition(String conditionClassName, + ClassLoader classloader) { + Class conditionClass = ClassUtils.resolveClassName(conditionClassName, + classloader); + return (Condition) BeanUtils.instantiateClass(conditionClass); + } + + + /** + * Implementation of a {@link ConditionContext}. + */ + private static class ConditionContextImpl implements ConditionContext { + + private BeanDefinitionRegistry registry; + + private ConfigurableListableBeanFactory beanFactory; + + private Environment environment; + + + public ConditionContextImpl(BeanDefinitionRegistry registry, + Environment environment, BeanNameGenerator beanNameGenerator) { + Assert.notNull(registry, "Registry must not be null"); + this.registry = registry; + this.beanFactory = deduceBeanFactory(registry); + this.environment = environment; + if (this.environment == null) { + this.environment = deduceEnvironment(registry); + } + } + + + private ConfigurableListableBeanFactory deduceBeanFactory(Object source) { + if (source instanceof ConfigurableListableBeanFactory) { + return (ConfigurableListableBeanFactory) source; + } + else if (source instanceof ConfigurableApplicationContext) { + return deduceBeanFactory(((ConfigurableApplicationContext) source).getBeanFactory()); + } + return null; + } + + private Environment deduceEnvironment(BeanDefinitionRegistry registry) { + if (registry instanceof EnvironmentCapable) { + return ((EnvironmentCapable) registry).getEnvironment(); + } + return null; + } + + public BeanDefinitionRegistry getRegistry() { + return this.registry; + } + + public Environment getEnvironment() { + return this.environment; + } + + public ConfigurableListableBeanFactory getBeanFactory() { + Assert.state(this.beanFactory != null, "Unable to locate the BeanFactory"); + return this.beanFactory; + } + + public ResourceLoader getResourceLoader() { + if (registry instanceof ResourceLoader) { + return (ResourceLoader) registry; + } + return null; + } + + public ClassLoader getClassLoader() { + ResourceLoader resourceLoader = getResourceLoader(); + return (resourceLoader == null ? null : resourceLoader.getClassLoader()); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java index 0a5a6460924..6ed1f643093 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -41,6 +41,7 @@ import org.springframework.util.ClassUtils; * * @author Chris Beams * @author Juergen Hoeller + * @author Phillip Webb * @since 3.0 * @see BeanMethod * @see ConfigurationClassParser @@ -58,7 +59,7 @@ final class ConfigurationClass { private String beanName; - private final boolean imported; + private final ConfigurationClass importedBy; /** @@ -66,28 +67,28 @@ final class ConfigurationClass { * @param metadataReader reader used to parse the underlying {@link Class} * @param beanName must not be {@code null} * @throws IllegalArgumentException if beanName is null (as of Spring 3.1.1) - * @see ConfigurationClass#ConfigurationClass(Class, boolean) + * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) */ public ConfigurationClass(MetadataReader metadataReader, String beanName) { Assert.hasText(beanName, "bean name must not be null"); this.metadata = metadataReader.getAnnotationMetadata(); this.resource = metadataReader.getResource(); this.beanName = beanName; - this.imported = false; + this.importedBy = null; } /** * Create a new {@link ConfigurationClass} representing a class that was imported * using the {@link Import} annotation or automatically processed as a nested - * configuration class (if imported is {@code true}). + * configuration class (if importedBy is not {@code null}). * @param metadataReader reader used to parse the underlying {@link Class} - * @param imported whether the given configuration class is being imported + * @param importedBy the configuration class importing this one or {@code null} * @since 3.1.1 */ - public ConfigurationClass(MetadataReader metadataReader, boolean imported) { + public ConfigurationClass(MetadataReader metadataReader, ConfigurationClass importedBy) { this.metadata = metadataReader.getAnnotationMetadata(); this.resource = metadataReader.getResource(); - this.imported = imported; + this.importedBy = importedBy; } /** @@ -95,14 +96,14 @@ final class ConfigurationClass { * @param clazz the underlying {@link Class} to represent * @param beanName name of the {@code @Configuration} class bean * @throws IllegalArgumentException if beanName is null (as of Spring 3.1.1) - * @see ConfigurationClass#ConfigurationClass(Class, boolean) + * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) */ public ConfigurationClass(Class clazz, String beanName) { Assert.hasText(beanName, "bean name must not be null"); this.metadata = new StandardAnnotationMetadata(clazz, true); this.resource = new DescriptiveResource(clazz.toString()); this.beanName = beanName; - this.imported = false; + this.importedBy = null; } /** @@ -110,13 +111,13 @@ final class ConfigurationClass { * using the {@link Import} annotation or automatically processed as a nested * configuration class (if imported is {@code true}). * @param clazz the underlying {@link Class} to represent - * @param imported whether the given configuration class is being imported + * @param importedBy the configuration class importing this one or {@code null} * @since 3.1.1 */ - public ConfigurationClass(Class clazz, boolean imported) { + public ConfigurationClass(Class clazz, ConfigurationClass importedBy) { this.metadata = new StandardAnnotationMetadata(clazz, true); this.resource = new DescriptiveResource(clazz.toString()); - this.imported = imported; + this.importedBy = importedBy; } @@ -136,9 +137,20 @@ final class ConfigurationClass { * Return whether this configuration class was registered via @{@link Import} or * automatically registered due to being nested within another configuration class. * @since 3.1.1 + * @see #getImportedBy() */ public boolean isImported() { - return this.imported; + return this.importedBy != null; + } + + /** + * Returns the configuration class that imported this class or {@code null} if + * this configuration was not imported. + * @since 4.0 + * @see #isImported() + */ + public ConfigurationClass getImportedBy() { + return importedBy; } public void setBeanName(String beanName) { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index 27ae171955c..eab2b0bd26b 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -16,6 +16,8 @@ package org.springframework.context.annotation; +import static org.springframework.context.annotation.MetadataUtils.attributesFor; + import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; @@ -26,7 +28,6 @@ import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; import org.springframework.beans.factory.annotation.Autowire; @@ -51,8 +52,6 @@ import org.springframework.core.type.MethodMetadata; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.util.StringUtils; -import static org.springframework.context.annotation.MetadataUtils.*; - /** * Reads a given fully-populated set of ConfigurationClass instances, registering bean * definitions with the given {@link BeanDefinitionRegistry} based on its contents. @@ -63,6 +62,7 @@ import static org.springframework.context.annotation.MetadataUtils.*; * * @author Chris Beams * @author Juergen Hoeller + * @author Phillip Webb * @since 3.0 * @see ConfigurationClassParser */ @@ -118,7 +118,7 @@ class ConfigurationClassBeanDefinitionReader { * Read a particular {@link ConfigurationClass}, registering bean definitions for the * class itself, all its {@link Bean} methods */ - private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass) { + public void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass) { if (configClass.isImported()) { registerBeanDefinitionForImportedConfigurationClass(configClass); } @@ -153,6 +153,10 @@ class ConfigurationClassBeanDefinitionReader { * with the BeanDefinitionRegistry based on its contents. */ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { + if (ConditionalAnnotationHelper.shouldSkip(beanMethod, this.registry, + this.environment, this.importBeanNameGenerator)) { + return; + } ConfigurationClass configClass = beanMethod.getConfigurationClass(); MethodMetadata metadata = beanMethod.getMetadata(); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 82fbbeba528..5b1736fd6f2 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -25,6 +25,8 @@ import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; @@ -32,19 +34,23 @@ import java.util.Stack; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.Aware; import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.parsing.Location; import org.springframework.beans.factory.parsing.Problem; import org.springframework.beans.factory.parsing.ProblemReporter; +import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionReader; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.NestedIOException; import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; @@ -76,11 +82,21 @@ import static org.springframework.context.annotation.MetadataUtils.*; * * @author Chris Beams * @author Juergen Hoeller + * @author Phillip Webb * @since 3.0 * @see ConfigurationClassBeanDefinitionReader */ class ConfigurationClassParser { + private static final Comparator DEFERRED_IMPORT_COMPARATOR = + new Comparator() { + public int compare(DeferredImportSelectorHolder o1, + DeferredImportSelectorHolder o2) { + return AnnotationAwareOrderComparator.INSTANCE.compare( + o1.getImportSelector(), o2.getImportSelector()); + } + }; + private final MetadataReaderFactory metadataReaderFactory; private final ProblemReporter problemReporter; @@ -103,6 +119,10 @@ class ConfigurationClassParser { private final ComponentScanAnnotationParser componentScanParser; + private final BeanNameGenerator beanNameGenerator; + + private final List deferredImportSelectors = + new LinkedList(); /** * Create a new {@link ConfigurationClassParser} instance that will be used @@ -117,10 +137,28 @@ class ConfigurationClassParser { this.environment = environment; this.resourceLoader = resourceLoader; this.registry = registry; + this.beanNameGenerator = componentScanBeanNameGenerator; this.componentScanParser = new ComponentScanAnnotationParser( resourceLoader, environment, componentScanBeanNameGenerator, registry); } + public void parse(Set configCandidates) { + for (BeanDefinitionHolder holder : configCandidates) { + BeanDefinition bd = holder.getBeanDefinition(); + try { + if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) { + parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName()); + } + else { + parse(bd.getBeanClassName(), holder.getBeanName()); + } + } + catch (IOException ex) { + throw new BeanDefinitionStoreException("Failed to load bean class: " + bd.getBeanClassName(), ex); + } + } + processDeferredImportSelectors(); + } /** * Parse the specified {@link Configuration @Configuration} class. @@ -138,17 +176,16 @@ class ConfigurationClassParser { * @param clazz the Class to parse * @param beanName must not be null (as of Spring 3.1.1) */ - public void parse(Class clazz, String beanName) throws IOException { + protected void parse(Class clazz, String beanName) throws IOException { processConfigurationClass(new ConfigurationClass(clazz, beanName)); } protected void processConfigurationClass(ConfigurationClass configClass) throws IOException { AnnotationMetadata metadata = configClass.getMetadata(); - if (this.environment != null && metadata.isAnnotated(Profile.class.getName())) { - AnnotationAttributes profile = MetadataUtils.attributesFor(metadata, Profile.class); - if (!this.environment.acceptsProfiles(profile.getStringArray("value"))) { - return; - } + + if (ConditionalAnnotationHelper.shouldSkip(configClass, this.registry, + this.environment, this.beanNameGenerator)) { + return; } // recursively process the configuration class and its superclass hierarchy @@ -173,7 +210,7 @@ class ConfigurationClassParser { ConfigurationClass configClass, AnnotationMetadata metadata) throws IOException { // recursively process any member (nested) classes first - processMemberClasses(metadata); + processMemberClasses(configClass, metadata); // process any @PropertySource annotations AnnotationAttributes propertySource = attributesFor(metadata, org.springframework.context.annotation.PropertySource.class); @@ -256,11 +293,12 @@ class ConfigurationClassParser { * @param metadata the metadata representation of the containing class * @throws IOException if there is any problem reading metadata from a member class */ - private void processMemberClasses(AnnotationMetadata metadata) throws IOException { + private void processMemberClasses(ConfigurationClass configClass, + AnnotationMetadata metadata) throws IOException { if (metadata instanceof StandardAnnotationMetadata) { for (Class memberClass : ((StandardAnnotationMetadata) metadata).getIntrospectedClass().getDeclaredClasses()) { if (ConfigurationClassUtils.isConfigurationCandidate(new StandardAnnotationMetadata(memberClass))) { - processConfigurationClass(new ConfigurationClass(memberClass, true)); + processConfigurationClass(new ConfigurationClass(memberClass, configClass)); } } } @@ -269,7 +307,7 @@ class ConfigurationClassParser { MetadataReader reader = this.metadataReaderFactory.getMetadataReader(memberClassName); AnnotationMetadata memberClassMetadata = reader.getAnnotationMetadata(); if (ConfigurationClassUtils.isConfigurationCandidate(memberClassMetadata)) { - processConfigurationClass(new ConfigurationClass(reader, true)); + processConfigurationClass(new ConfigurationClass(reader, configClass)); } } } @@ -365,6 +403,21 @@ class ConfigurationClassParser { } } + private void processDeferredImportSelectors() { + Collections.sort(this.deferredImportSelectors, DEFERRED_IMPORT_COMPARATOR); + for (DeferredImportSelectorHolder deferredImport : this.deferredImportSelectors) { + try { + ConfigurationClass configClass = deferredImport.getConfigurationClass(); + String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata()); + processImport(configClass, Arrays.asList(imports), false); + } + catch (IOException ex) { + throw new BeanDefinitionStoreException("Failed to load bean class: ", ex); + } + } + deferredImportSelectors.clear(); + } + private void processImport(ConfigurationClass configClass, Collection classesToImport, boolean checkForCircularImports) throws IOException { if (checkForCircularImports && this.importStack.contains(configClass)) { this.problemReporter.error(new CircularImportProblem(configClass, this.importStack, configClass.getMetadata())); @@ -380,7 +433,12 @@ class ConfigurationClassParser { Class candidateClass = (candidate instanceof Class ? (Class) candidate : this.resourceLoader.getClassLoader().loadClass((String) candidate)); ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class); invokeAwareMethods(selector); - processImport(configClass, Arrays.asList(selector.selectImports(importingClassMetadata)), false); + if(selector instanceof DeferredImportSelector) { + this.deferredImportSelectors.add(new DeferredImportSelectorHolder( + configClass, (DeferredImportSelector) selector)); + } else { + processImport(configClass, Arrays.asList(selector.selectImports(importingClassMetadata)), false); + } } else if (checkAssignability(ImportBeanDefinitionRegistrar.class, candidateToCheck)) { // the candidate class is an ImportBeanDefinitionRegistrar -> delegate to it to register additional bean definitions @@ -391,9 +449,11 @@ class ConfigurationClassParser { } else { // candidate class not an ImportSelector or ImportBeanDefinitionRegistrar -> process it as a @Configuration class - this.importStack.registerImport(importingClassMetadata.getClassName(), (candidate instanceof Class ? ((Class) candidate).getName() : (String) candidate)); - processConfigurationClass(candidateToCheck instanceof Class ? new ConfigurationClass((Class) candidateToCheck, true) : - new ConfigurationClass((MetadataReader) candidateToCheck, true)); + this.importStack.registerImport(importingClassMetadata.getClassName(), + (candidate instanceof Class ? ((Class) candidate).getName() : (String) candidate)); + processConfigurationClass((candidateToCheck instanceof Class ? + new ConfigurationClass((Class) candidateToCheck, configClass) : + new ConfigurationClass((MetadataReader) candidateToCheck, configClass))); } } } @@ -533,4 +593,24 @@ class ConfigurationClassParser { } } + + private static class DeferredImportSelectorHolder { + + private ConfigurationClass configurationClass; + + private DeferredImportSelector importSelector; + + public DeferredImportSelectorHolder(ConfigurationClass configurationClass, DeferredImportSelector importSelector) { + this.configurationClass = configurationClass; + this.importSelector = importSelector; + } + + public ConfigurationClass getConfigurationClass() { + return configurationClass; + } + + public DeferredImportSelector getImportSelector() { + return importSelector; + } + } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index 7afbc027337..dbd94f33103 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -82,6 +82,7 @@ import static org.springframework.context.annotation.AnnotationConfigUtils.*; * * @author Chris Beams * @author Juergen Hoeller + * @author Phillip Webb * @since 3.0 */ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, @@ -275,20 +276,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo ConfigurationClassParser parser = new ConfigurationClassParser( this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry); - for (BeanDefinitionHolder holder : configCandidates) { - BeanDefinition bd = holder.getBeanDefinition(); - try { - if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) { - parser.parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName()); - } - else { - parser.parse(bd.getBeanClassName(), holder.getBeanName()); - } - } - catch (IOException ex) { - throw new BeanDefinitionStoreException("Failed to load bean class: " + bd.getBeanClassName(), ex); - } - } + parser.parse(configCandidates); parser.validate(); // Handle any @PropertySource annotations @@ -312,7 +300,12 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo registry, this.sourceExtractor, this.problemReporter, this.metadataReaderFactory, this.resourceLoader, this.environment, this.importBeanNameGenerator); } - this.reader.loadBeanDefinitions(parser.getConfigurationClasses()); + for (ConfigurationClass configurationClass : parser.getConfigurationClasses()) { + if (!ConditionalAnnotationHelper.shouldSkip(configurationClass, registry, + this.environment, this.importBeanNameGenerator)) { + reader.loadBeanDefinitionsForConfigurationClass(configurationClass); + } + } // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes if (singletonRegistry != null) { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java b/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java new file mode 100644 index 00000000000..2c104126f9b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2013 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; + +/** + * A variation of {@link ImportSelector} that runs after all {@code @Configuration} beans + * have been processed. This type of selector can be particularly useful when the selected + * imports are {@code @Conditional}. + * + *

Implementations can also extend the {@link org.springframework.core.Ordered} + * interface or use the {@link org.springframework.core.annotation.Order} annotation to + * indicate a precedence against other {@link DeferredImportSelector}s. + * + * @author Phillip Webb + * @since 4.0 + */ +public interface DeferredImportSelector extends ImportSelector { + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java index 5fcb9b960ff..5d8848001df 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java @@ -32,8 +32,14 @@ import org.springframework.core.type.AnnotationMetadata; *

  • {@link org.springframework.context.ResourceLoaderAware ResourceLoaderAware}
  • * * + *

    ImportSelectors are usually processed in the same way as regular {@code @Import} + * annotations, however, it is also possible to defer selection of imports until all + * {@code @Configuration} classes have been processed (see {@link DeferredImportSelector} + * for details). + * * @author Chris Beams * @since 3.1 + * @see DeferredImportSelector * @see Import * @see ImportBeanDefinitionRegistrar * @see Configuration diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Profile.java b/spring-context/src/main/java/org/springframework/context/annotation/Profile.java index ab793599c01..27900952cdc 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Profile.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Profile.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -38,8 +38,9 @@ import org.springframework.core.env.ConfigurableEnvironment; *

    The {@code @Profile} annotation may be used in any of the following ways: *

    * *

    If a {@code @Configuration} class is marked with {@code @Profile}, all of the @@ -65,6 +66,7 @@ import org.springframework.core.env.ConfigurableEnvironment; * {@code spring-beans} XSD (version 3.1 or greater) for details. * * @author Chris Beams + * @author Phillip Webb * @since 3.1 * @see ConfigurableEnvironment#setActiveProfiles * @see ConfigurableEnvironment#setDefaultProfiles @@ -72,7 +74,8 @@ import org.springframework.core.env.ConfigurableEnvironment; * @see AbstractEnvironment#DEFAULT_PROFILES_PROPERTY_NAME */ @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Conditional(ProfileCondition.class) public @interface Profile { /** diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java b/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java new file mode 100644 index 00000000000..424e20ae098 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2013 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 org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * {@link Condition} that matches based on the value of a {@link Profile @Profile} + * annotation. + * + * @author Chris Beams + * @author Phillip Webb + * @since 4.0 + */ +class ProfileCondition implements Condition { + + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + if (context.getEnvironment() != null && metadata.isAnnotated(Profile.class.getName())) { + AnnotationAttributes profile = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(Profile.class.getName())); + if (!context.getEnvironment().acceptsProfiles(profile.getStringArray("value"))) { + return false; + } + } + return true; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassWithConditionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassWithConditionTests.java new file mode 100644 index 00000000000..9506b817c91 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassWithConditionTests.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2013 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.Matchers.equalTo; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.stereotype.Component; + +/** + * Test for {@link Conditional} beans. + * + * @author Phillip Webb + */ +public class ConfigurationClassWithConditionTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void conditionalOnBeanMatch() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(BeanOneConfiguration.class, BeanTwoConfiguration.class); + ctx.refresh(); + assertTrue(ctx.containsBean("bean1")); + assertFalse(ctx.containsBean("bean2")); + } + + @Test + public void conditionalOnBeanNoMatch() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(BeanTwoConfiguration.class); + ctx.refresh(); + assertTrue(ctx.containsBean("bean2")); + } + + @Test + public void metaConditional() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigurationWithMetaCondition.class); + ctx.refresh(); + assertTrue(ctx.containsBean("bean")); + } + + @Test + public void nonConfigurationClass() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(NonConfigurationClass.class); + ctx.refresh(); + thrown.expect(NoSuchBeanDefinitionException.class); + assertNull(ctx.getBean(NonConfigurationClass.class)); + } + + @Test + public void methodConditional() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConditionOnMethodConfiguration.class); + ctx.refresh(); + thrown.expect(NoSuchBeanDefinitionException.class); + assertNull(ctx.getBean(ExampleBean.class)); + } + + @Configuration + static class BeanOneConfiguration { + @Bean + public ExampleBean bean1() { + return new ExampleBean(); + } + } + + @Configuration + @Conditional(NoBeanOneCondition.class) + static class BeanTwoConfiguration { + @Bean + public ExampleBean bean2() { + return new ExampleBean(); + } + } + + @Configuration + @MetaConditional("test") + static class ConfigurationWithMetaCondition { + @Bean + public ExampleBean bean() { + return new ExampleBean(); + } + } + + @Conditional(MetaConditionalFilter.class) + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public static @interface MetaConditional { + String value(); + } + + @Conditional(NeverCondition.class) + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.TYPE, ElementType.METHOD}) + public static @interface Never { + } + + static class NoBeanOneCondition implements Condition { + + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return !context.getBeanFactory().containsBeanDefinition("bean1"); + } + } + + static class MetaConditionalFilter implements Condition { + + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(MetaConditional.class.getName())); + assertThat(attributes.getString("value"), equalTo("test")); + return true; + } + } + + static class NeverCondition implements Condition { + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return false; + } + } + + @Component + @Never + static class NonConfigurationClass { + } + + @Configuration + static class ConditionOnMethodConfiguration { + + @Bean + @Never + public ExampleBean bean1() { + return new ExampleBean(); + } + } + + static class ExampleBean { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java new file mode 100644 index 00000000000..abdfcdbf6d1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2013 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.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.spy; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.Test; +import org.mockito.InOrder; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Tests for {@link ImportSelector} and {@link DeferredImportSelector}. + * + * @author Phillip Webb + */ +public class ImportSelectorTests { + + @Test + public void importSelectors() { + DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory()); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + beanFactory); + context.register(Config.class); + context.refresh(); + context.getBean(Config.class); + InOrder ordered = inOrder(beanFactory); + ordered.verify(beanFactory).registerBeanDefinition(eq("a"), any(BeanDefinition.class)); + ordered.verify(beanFactory).registerBeanDefinition(eq("b"), any(BeanDefinition.class)); + ordered.verify(beanFactory).registerBeanDefinition(eq("d"), any(BeanDefinition.class)); + ordered.verify(beanFactory).registerBeanDefinition(eq("c"), any(BeanDefinition.class)); + } + + @Sample + @Configuration + static class Config { + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Import({ DeferredImportSelector1.class, DeferredImportSelector2.class, + ImportSelector1.class, ImportSelector2.class }) + public static @interface Sample { + } + + public static class ImportSelector1 implements ImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[] { ImportedSelector1.class.getName() }; + } + } + + public static class ImportSelector2 implements ImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[] { ImportedSelector2.class.getName() }; + } + } + + public static class DeferredImportSelector1 implements DeferredImportSelector, Ordered { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[] { DeferredImportedSelector1.class.getName() }; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + } + + @Order(Ordered.HIGHEST_PRECEDENCE) + public static class DeferredImportSelector2 implements DeferredImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[] { DeferredImportedSelector2.class.getName() }; + } + + } + + @Configuration + public static class ImportedSelector1 { + + @Bean + public String a() { + return "a"; + } + } + + @Configuration + public static class ImportedSelector2 { + + @Bean + public String b() { + return "b"; + } + } + + @Configuration + public static class DeferredImportedSelector1 { + + @Bean + public String c() { + return "c"; + } + } + + @Configuration + public static class DeferredImportedSelector2 { + + @Bean + public String d() { + return "d"; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/AnnotatedElementUtils.java b/spring-core/src/main/java/org/springframework/core/type/AnnotatedElementUtils.java new file mode 100644 index 00000000000..ca817f48bd6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/AnnotatedElementUtils.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2013 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.core.type; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Internal utility class used to collect all annotation values including those declared + * on meta-annotations. + * + * @author Phillip Webb + * @since 4.0 + */ +class AnnotatedElementUtils { + + public static Set getMetaAnnotationTypes(AnnotatedElement element, + String annotationType) { + final Set types = new LinkedHashSet(); + process(element, annotationType, new Processor() { + public Object process(Annotation annotation, int depth) { + if (depth > 0) { + types.add(annotation.annotationType().getName()); + } + return null; + } + }); + return (types.size() == 0 ? null : types); + } + + public static boolean hasMetaAnnotationTypes(AnnotatedElement element, + String annotationType) { + return Boolean.TRUE.equals( + process(element, annotationType, new Processor() { + public Boolean process(Annotation annotation, int depth) { + if (depth > 0) { + return true; + } + return null; + } + })); + } + + public static boolean isAnnotated(AnnotatedElement element, String annotationType) { + return Boolean.TRUE.equals( + process(element, annotationType, new Processor() { + public Boolean process(Annotation annotation, int depth) { + return true; + } + })); + } + + public static Map getAnnotationAttributes(AnnotatedElement element, + String annotationType, final boolean classValuesAsString, + final boolean nestedAnnotationsAsMap) { + return process(element, annotationType, new Processor>() { + public Map process(Annotation annotation, int depth) { + return AnnotationUtils.getAnnotationAttributes(annotation, + classValuesAsString, nestedAnnotationsAsMap); + } + }); + } + + public static MultiValueMap getAllAnnotationAttributes( + AnnotatedElement element, final String annotationType, + final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) { + final MultiValueMap attributes = new LinkedMultiValueMap(); + process(element, annotationType, new Processor() { + public Object process(Annotation annotation, int depth) { + if (annotation.annotationType().getName().equals(annotationType)) { + for (Map.Entry entry : AnnotationUtils.getAnnotationAttributes( + annotation, classValuesAsString, nestedAnnotationsAsMap).entrySet()) { + attributes.add(entry.getKey(), entry.getValue()); + } + } + return null; + } + }); + return (attributes.size() == 0 ? null : attributes); + } + + /** + * Process all annotations of the specified annotation type and recursively all + * meta-annotations on the specified type. + * @param element the annotated element + * @param annotationType the annotation type to find. Only items of the specified type + * or meta-annotations of the specified type will be processed + * @param processor the processor + * @return the result of the processor + */ + private static T process(AnnotatedElement element, String annotationType, + Processor processor) { + return recursivelyProcess(element, annotationType, processor, + new HashSet(), 0); + } + + private static T recursivelyProcess(AnnotatedElement element, + String annotationType, Processor processor, Set visited, + int depth) { + T result = null; + if (visited.add(element)) { + for (Annotation annotation : element.getAnnotations()) { + if (annotation.annotationType().getName().equals(annotationType) || depth > 0) { + result = result != null ? result : + processor.process(annotation, depth); + result = result != null ? result : + recursivelyProcess(annotation.annotationType(), annotationType, + processor, visited, depth + 1); + } + } + for (Annotation annotation : element.getAnnotations()) { + result = result != null ? result : + recursivelyProcess(annotation.annotationType(), annotationType, + processor, visited, depth); + } + + } + return result; + } + + /** + * Callback interface used to process an annotation. + * @param the result type + */ + private static interface Processor { + + /** + * Called to process the annotation. + * @param annotation the annotation to process + * @param depth the depth of the annotation relative to the initial match. For + * example a matched annotation will have a depth of 0, a meta-annotation 1 + * and a meta-meta-annotation 2 + * @return the result of the processing or {@code null} to continue + */ + T process(Annotation annotation, int depth); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java b/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java new file mode 100644 index 00000000000..4cef1d48b30 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2013 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.core.type; + +import java.util.Map; + +import org.springframework.util.MultiValueMap; + +/** + * Defines access to the annotations of a specific type ({@link AnnotationMetadata class} + * or {@link MethodMetadata method}), in a form that does not necessarily require the + * class-loading. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @author Mark Pollack + * @author Chris Beams + * @author Phillip Webb + * @since 4.0 + * @see AnnotationMetadata + * @see MethodMetadata + */ +public interface AnnotatedTypeMetadata { + + /** + * Determine whether the underlying type has an annotation or + * meta-annotation of the given type defined. + *

    If this method returns {@code true}, then + * {@link #getAnnotationAttributes} will return a non-null Map. + * @param annotationType the annotation type to look for + * @return whether a matching annotation is defined + */ + boolean isAnnotated(String annotationType); + + /** + * Retrieve the attributes of the annotation of the given type, + * if any (i.e. if defined on the underlying class, as direct + * annotation or as meta-annotation). + * @param annotationType the annotation type to look for + * @return a Map of attributes, with the attribute name as key (e.g. "value") + * and the defined attribute value as Map value. This return value will be + * {@code null} if no matching annotation is defined. + */ + Map getAnnotationAttributes(String annotationType); + + /** + * Retrieve the attributes of the annotation of the given type, + * if any (i.e. if defined on the underlying class, as direct + * annotation or as meta-annotation). + * @param annotationType the annotation type to look for + * @param classValuesAsString whether to convert class references to String + * class names for exposure as values in the returned Map, instead of Class + * references which might potentially have to be loaded first + * @return a Map of attributes, with the attribute name as key (e.g. "value") + * and the defined attribute value as Map value. This return value will be + * {@code null} if no matching annotation is defined. + */ + Map getAnnotationAttributes(String annotationType, boolean classValuesAsString); + + /** + * Retrieve all attributes of all annotations of the given type, if any (i.e. if + * defined on the underlying method, as direct annotation or as meta-annotation). + * @param annotationType the annotation type to look for + * @return a MultiMap of attributes, with the attribute name as key (e.g. "value") and + * a list of the defined attribute values as Map value. This return value will + * be {@code null} if no matching annotation is defined. + */ + MultiValueMap getAllAnnotationAttributes(String annotationType); + + /** + * Retrieve all attributes of all annotations of the given type, if any (i.e. if + * defined on the underlying method, as direct annotation or as meta-annotation). + * @param annotationType the annotation type to look for + * @param classValuesAsString whether to convert class references to String + * @return a MultiMap of attributes, with the attribute name as key (e.g. "value") and + * a list of the defined attribute values as Map value. This return value will + * be {@code null} if no matching annotation is defined. + * @see #getAllAnnotationAttributes(String) + */ + MultiValueMap getAllAnnotationAttributes(String annotationType, + boolean classValuesAsString); + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/AnnotationMetadata.java b/spring-core/src/main/java/org/springframework/core/type/AnnotationMetadata.java index beeb7d935a0..435e934415d 100644 --- a/spring-core/src/main/java/org/springframework/core/type/AnnotationMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/AnnotationMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -16,7 +16,6 @@ package org.springframework.core.type; -import java.util.Map; import java.util.Set; /** @@ -25,11 +24,13 @@ import java.util.Set; * * @author Juergen Hoeller * @author Mark Fisher + * @author Phillip Webb * @since 2.5 * @see StandardAnnotationMetadata * @see org.springframework.core.type.classreading.MetadataReader#getAnnotationMetadata() + * @see AnnotatedTypeMetadata */ -public interface AnnotationMetadata extends ClassMetadata { +public interface AnnotationMetadata extends ClassMetadata, AnnotatedTypeMetadata { /** * Return the names of all annotation types defined on the underlying class. @@ -61,42 +62,6 @@ public interface AnnotationMetadata extends ClassMetadata { */ boolean hasMetaAnnotation(String metaAnnotationType); - /** - * Determine whether the underlying class has an annotation or - * meta-annotation of the given type defined. - *

    This is equivalent to a "hasAnnotation || hasMetaAnnotation" - * check. If this method returns {@code true}, then - * {@link #getAnnotationAttributes} will return a non-null Map. - * @param annotationType the annotation type to look for - * @return whether a matching annotation is defined - */ - boolean isAnnotated(String annotationType); - - /** - * Retrieve the attributes of the annotation of the given type, - * if any (i.e. if defined on the underlying class, as direct - * annotation or as meta-annotation). - * @param annotationType the annotation type to look for - * @return a Map of attributes, with the attribute name as key (e.g. "value") - * and the defined attribute value as Map value. This return value will be - * {@code null} if no matching annotation is defined. - */ - Map getAnnotationAttributes(String annotationType); - - /** - * Retrieve the attributes of the annotation of the given type, - * if any (i.e. if defined on the underlying class, as direct - * annotation or as meta-annotation). - * @param annotationType the annotation type to look for - * @param classValuesAsString whether to convert class references to String - * class names for exposure as values in the returned Map, instead of Class - * references which might potentially have to be loaded first - * @return a Map of attributes, with the attribute name as key (e.g. "value") - * and the defined attribute value as Map value. This return value will be - * {@code null} if no matching annotation is defined. - */ - Map getAnnotationAttributes(String annotationType, boolean classValuesAsString); - /** * Determine whether the underlying class has any methods that are * annotated (or meta-annotated) with the given annotation type. diff --git a/spring-core/src/main/java/org/springframework/core/type/MethodMetadata.java b/spring-core/src/main/java/org/springframework/core/type/MethodMetadata.java index 17d7bab24cb..585965ca32e 100644 --- a/spring-core/src/main/java/org/springframework/core/type/MethodMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/MethodMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -16,8 +16,6 @@ package org.springframework.core.type; -import java.util.Map; - /** * Interface that defines abstract access to the annotations of a specific * class, in a form that does not require that class to be loaded yet. @@ -25,11 +23,13 @@ import java.util.Map; * @author Juergen Hoeller * @author Mark Pollack * @author Chris Beams + * @author Phillip Webb * @since 3.0 * @see StandardMethodMetadata * @see AnnotationMetadata#getAnnotatedMethods + * @see AnnotatedTypeMetadata */ -public interface MethodMetadata { +public interface MethodMetadata extends AnnotatedTypeMetadata { /** * Return the name of the method. @@ -57,23 +57,4 @@ public interface MethodMetadata { */ boolean isOverridable(); - /** - * Determine whether the underlying method has an annotation or - * meta-annotation of the given type defined. - * @param annotationType the annotation type to look for - * @return whether a matching annotation is defined - */ - boolean isAnnotated(String annotationType); - - /** - * Retrieve the attributes of the annotation of the given type, - * if any (i.e. if defined on the underlying method, as direct - * annotation or as meta-annotation). - * @param annotationType the annotation type to look for - * @return a Map of attributes, with the attribute name as key (e.g. "value") - * and the defined attribute value as Map value. This return value will be - * {@code null} if no matching annotation is defined. - */ - Map getAnnotationAttributes(String annotationType); - } diff --git a/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java b/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java index 37473c5a77c..e0c609624cd 100644 --- a/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java @@ -23,7 +23,7 @@ import java.util.Map; import java.util.Set; import org.springframework.core.annotation.AnnotationAttributes; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.MultiValueMap; /** * {@link AnnotationMetadata} implementation that uses standard reflection @@ -32,6 +32,7 @@ import org.springframework.core.annotation.AnnotationUtils; * @author Juergen Hoeller * @author Mark Fisher * @author Chris Beams + * @author Phillip Webb * @since 2.5 */ public class StandardAnnotationMetadata extends StandardClassMetadata implements AnnotationMetadata { @@ -74,21 +75,7 @@ public class StandardAnnotationMetadata extends StandardClassMetadata implements } public Set getMetaAnnotationTypes(String annotationType) { - Annotation[] anns = getIntrospectedClass().getAnnotations(); - for (Annotation ann : anns) { - if (ann.annotationType().getName().equals(annotationType)) { - Set types = new LinkedHashSet(); - Annotation[] metaAnns = ann.annotationType().getAnnotations(); - for (Annotation metaAnn : metaAnns) { - types.add(metaAnn.annotationType().getName()); - for (Annotation metaMetaAnn : metaAnn.annotationType().getAnnotations()) { - types.add(metaMetaAnn.annotationType().getName()); - } - } - return types; - } - } - return null; + return AnnotatedElementUtils.getMetaAnnotationTypes(getIntrospectedClass(), annotationType); } public boolean hasAnnotation(String annotationType) { @@ -102,75 +89,38 @@ public class StandardAnnotationMetadata extends StandardClassMetadata implements } public boolean hasMetaAnnotation(String annotationType) { - Annotation[] anns = getIntrospectedClass().getAnnotations(); - for (Annotation ann : anns) { - Annotation[] metaAnns = ann.annotationType().getAnnotations(); - for (Annotation metaAnn : metaAnns) { - if (metaAnn.annotationType().getName().equals(annotationType)) { - return true; - } - for (Annotation metaMetaAnn : metaAnn.annotationType().getAnnotations()) { - if (metaMetaAnn.annotationType().getName().equals(annotationType)) { - return true; - } - } - } - } - return false; + return AnnotatedElementUtils.hasMetaAnnotationTypes(getIntrospectedClass(), annotationType); } public boolean isAnnotated(String annotationType) { - Annotation[] anns = getIntrospectedClass().getAnnotations(); - for (Annotation ann : anns) { - if (ann.annotationType().getName().equals(annotationType)) { - return true; - } - for (Annotation metaAnn : ann.annotationType().getAnnotations()) { - if (metaAnn.annotationType().getName().equals(annotationType)) { - return true; - } - } - } - return false; + return AnnotatedElementUtils.isAnnotated(getIntrospectedClass(), annotationType); } public Map getAnnotationAttributes(String annotationType) { return this.getAnnotationAttributes(annotationType, false); } - public Map getAnnotationAttributes(String annotationType, boolean classValuesAsString) { - Annotation[] anns = getIntrospectedClass().getAnnotations(); - for (Annotation ann : anns) { - if (ann.annotationType().getName().equals(annotationType)) { - return AnnotationUtils.getAnnotationAttributes( - ann, classValuesAsString, this.nestedAnnotationsAsMap); - } - } - for (Annotation ann : anns) { - for (Annotation metaAnn : ann.annotationType().getAnnotations()) { - if (metaAnn.annotationType().getName().equals(annotationType)) { - return AnnotationUtils.getAnnotationAttributes( - metaAnn, classValuesAsString, this.nestedAnnotationsAsMap); - } - } - } - return null; + public Map getAnnotationAttributes(String annotationType, + boolean classValuesAsString) { + return AnnotatedElementUtils.getAnnotationAttributes(getIntrospectedClass(), + annotationType, classValuesAsString, this.nestedAnnotationsAsMap); + } + + public MultiValueMap getAllAnnotationAttributes(String annotationType) { + return getAllAnnotationAttributes(annotationType, false); + } + + public MultiValueMap getAllAnnotationAttributes( + String annotationType, boolean classValuesAsString) { + return AnnotatedElementUtils.getAllAnnotationAttributes(getIntrospectedClass(), + annotationType, classValuesAsString, this.nestedAnnotationsAsMap); } public boolean hasAnnotatedMethods(String annotationType) { Method[] methods = getIntrospectedClass().getDeclaredMethods(); for (Method method : methods) { - for (Annotation ann : method.getAnnotations()) { - if (ann.annotationType().getName().equals(annotationType)) { - return true; - } - else { - for (Annotation metaAnn : ann.annotationType().getAnnotations()) { - if (metaAnn.annotationType().getName().equals(annotationType)) { - return true; - } - } - } + if (AnnotatedElementUtils.isAnnotated(method, annotationType)) { + return true; } } return false; @@ -180,19 +130,9 @@ public class StandardAnnotationMetadata extends StandardClassMetadata implements Method[] methods = getIntrospectedClass().getDeclaredMethods(); Set annotatedMethods = new LinkedHashSet(); for (Method method : methods) { - for (Annotation ann : method.getAnnotations()) { - if (ann.annotationType().getName().equals(annotationType)) { - annotatedMethods.add(new StandardMethodMetadata(method, this.nestedAnnotationsAsMap)); - break; - } - else { - for (Annotation metaAnn : ann.annotationType().getAnnotations()) { - if (metaAnn.annotationType().getName().equals(annotationType)) { - annotatedMethods.add(new StandardMethodMetadata(method, this.nestedAnnotationsAsMap)); - break; - } - } - } + if (AnnotatedElementUtils.isAnnotated(method, annotationType)) { + annotatedMethods.add(new StandardMethodMetadata(method, + this.nestedAnnotationsAsMap)); } } return annotatedMethods; diff --git a/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java b/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java index c6564c643f7..221cbdb8bec 100644 --- a/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -16,13 +16,12 @@ package org.springframework.core.type; -import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Map; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; /** * {@link MethodMetadata} implementation that uses standard reflection @@ -31,6 +30,7 @@ import org.springframework.util.Assert; * @author Juergen Hoeller * @author Mark Pollack * @author Chris Beams + * @author Phillip Webb * @since 3.0 */ public class StandardMethodMetadata implements MethodMetadata { @@ -89,35 +89,26 @@ public class StandardMethodMetadata implements MethodMetadata { } public boolean isAnnotated(String annotationType) { - Annotation[] anns = this.introspectedMethod.getAnnotations(); - for (Annotation ann : anns) { - if (ann.annotationType().getName().equals(annotationType)) { - return true; - } - for (Annotation metaAnn : ann.annotationType().getAnnotations()) { - if (metaAnn.annotationType().getName().equals(annotationType)) { - return true; - } - } - } - return false; + return AnnotatedElementUtils.isAnnotated(this.introspectedMethod, annotationType); } public Map getAnnotationAttributes(String annotationType) { - Annotation[] anns = this.introspectedMethod.getAnnotations(); - for (Annotation ann : anns) { - if (ann.annotationType().getName().equals(annotationType)) { - return AnnotationUtils.getAnnotationAttributes( - ann, true, nestedAnnotationsAsMap); - } - for (Annotation metaAnn : ann.annotationType().getAnnotations()) { - if (metaAnn.annotationType().getName().equals(annotationType)) { - return AnnotationUtils.getAnnotationAttributes( - metaAnn, true, this.nestedAnnotationsAsMap); - } - } - } - return null; + return getAnnotationAttributes(annotationType, false); + } + + public Map getAnnotationAttributes(String annotationType, boolean classValuesAsString) { + return AnnotatedElementUtils.getAnnotationAttributes(this.introspectedMethod, + annotationType, classValuesAsString, this.nestedAnnotationsAsMap); + } + + public MultiValueMap getAllAnnotationAttributes(String annotationType) { + return getAllAnnotationAttributes(annotationType, false); + } + + public MultiValueMap getAllAnnotationAttributes( + String annotationType, boolean classValuesAsString) { + return AnnotatedElementUtils.getAllAnnotationAttributes(this.introspectedMethod, + annotationType, classValuesAsString, this.nestedAnnotationsAsMap); } } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationAttributesReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationAttributesReadingVisitor.java index 0b89eeec783..b9a1586666c 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationAttributesReadingVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationAttributesReadingVisitor.java @@ -34,13 +34,14 @@ import org.springframework.asm.SpringAsmInfo; import org.springframework.asm.Type; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.MultiValueMap; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; - /** * @author Chris Beams * @author Juergen Hoeller + * @author Phillip Webb * @since 3.1.1 */ abstract class AbstractRecursiveAnnotationVisitor extends AnnotationVisitor { @@ -51,12 +52,14 @@ abstract class AbstractRecursiveAnnotationVisitor extends AnnotationVisitor { protected final ClassLoader classLoader; + public AbstractRecursiveAnnotationVisitor(ClassLoader classLoader, AnnotationAttributes attributes) { super(SpringAsmInfo.ASM_VERSION); this.classLoader = classLoader; this.attributes = attributes; } + public void visit(String attributeName, Object attributeValue) { this.attributes.put(attributeName, attributeValue); } @@ -108,12 +111,14 @@ final class RecursiveAnnotationArrayVisitor extends AbstractRecursiveAnnotationV private final List allNestedAttributes = new ArrayList(); + public RecursiveAnnotationArrayVisitor( String attributeName, AnnotationAttributes attributes, ClassLoader classLoader) { super(classLoader, attributes); this.attributeName = attributeName; } + @Override public void visit(String attributeName, Object attributeValue) { Object newValue = attributeValue; @@ -155,12 +160,14 @@ class RecursiveAnnotationAttributesVisitor extends AbstractRecursiveAnnotationVi private final String annotationType; + public RecursiveAnnotationAttributesVisitor( String annotationType, AnnotationAttributes attributes, ClassLoader classLoader) { super(classLoader, attributes); this.annotationType = annotationType; } + public final void visitEnd() { try { Class annotationClass = this.classLoader.loadClass(this.annotationType); @@ -214,18 +221,20 @@ class RecursiveAnnotationAttributesVisitor extends AbstractRecursiveAnnotationVi * * @author Juergen Hoeller * @author Chris Beams + * @author Phillip Webb * @since 3.0 */ final class AnnotationAttributesReadingVisitor extends RecursiveAnnotationAttributesVisitor { private final String annotationType; - private final Map attributesMap; + private final MultiValueMap attributesMap; private final Map> metaAnnotationMap; + public AnnotationAttributesReadingVisitor( - String annotationType, Map attributesMap, + String annotationType, MultiValueMap attributesMap, Map> metaAnnotationMap, ClassLoader classLoader) { super(annotationType, new AnnotationAttributes(), classLoader); @@ -234,29 +243,33 @@ final class AnnotationAttributesReadingVisitor extends RecursiveAnnotationAttrib this.metaAnnotationMap = metaAnnotationMap; } + @Override public void doVisitEnd(Class annotationClass) { super.doVisitEnd(annotationClass); - this.attributesMap.put(this.annotationType, this.attributes); - registerMetaAnnotations(annotationClass); - } - - private void registerMetaAnnotations(Class annotationClass) { - // Register annotations that the annotation type is annotated with. + List attributes = this.attributesMap.get(this.annotationType); + if (attributes == null) { + this.attributesMap.add(this.annotationType, this.attributes); + } + else { + attributes.add(0, this.attributes); + } Set metaAnnotationTypeNames = new LinkedHashSet(); for (Annotation metaAnnotation : annotationClass.getAnnotations()) { - metaAnnotationTypeNames.add(metaAnnotation.annotationType().getName()); - if (!this.attributesMap.containsKey(metaAnnotation.annotationType().getName())) { - this.attributesMap.put(metaAnnotation.annotationType().getName(), - AnnotationUtils.getAnnotationAttributes(metaAnnotation, true, true)); - } - for (Annotation metaMetaAnnotation : metaAnnotation.annotationType().getAnnotations()) { - metaAnnotationTypeNames.add(metaMetaAnnotation.annotationType().getName()); - } + recusivelyCollectMetaAnnotations(metaAnnotationTypeNames, metaAnnotation); } if (this.metaAnnotationMap != null) { this.metaAnnotationMap.put(annotationClass.getName(), metaAnnotationTypeNames); } } + private void recusivelyCollectMetaAnnotations(Set visited, Annotation annotation) { + if (visited.add(annotation.annotationType().getName())) { + this.attributesMap.add(annotation.annotationType().getName(), + AnnotationUtils.getAnnotationAttributes(annotation, true, true)); + for (Annotation metaMetaAnnotation : annotation.annotationType().getAnnotations()) { + recusivelyCollectMetaAnnotations(visited, metaMetaAnnotation); + } + } + } } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java index 72bf34bbbcb..5a2f959fb2d 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java @@ -41,6 +41,7 @@ import org.springframework.util.MultiValueMap; * @author Juergen Hoeller * @author Mark Fisher * @author Costin Leau + * @author Phillip Webb * @since 2.5 */ public class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor implements AnnotationMetadata { @@ -51,7 +52,7 @@ public class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisito protected final Map> metaAnnotationMap = new LinkedHashMap>(4); - protected final Map attributeMap = new LinkedHashMap(4); + protected final MultiValueMap attributeMap = new LinkedMultiValueMap(4); protected final MultiValueMap methodMetadataMap = new LinkedMultiValueMap(); @@ -105,60 +106,30 @@ public class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisito } public AnnotationAttributes getAnnotationAttributes(String annotationType, boolean classValuesAsString) { - AnnotationAttributes raw = this.attributeMap.get(annotationType); - return convertClassValues(raw, classValuesAsString); + List attributes = this.attributeMap.get(annotationType); + AnnotationAttributes raw = (attributes == null ? null : attributes.get(0)); + return AnnotationReadingVisitorUtils.convertClassValues(this.classLoader, raw, + classValuesAsString); } - private AnnotationAttributes convertClassValues(AnnotationAttributes original, boolean classValuesAsString) { - if (original == null) { + public MultiValueMap getAllAnnotationAttributes(String annotationType) { + return getAllAnnotationAttributes(annotationType, false); + } + + public MultiValueMap getAllAnnotationAttributes( + String annotationType, boolean classValuesAsString) { + MultiValueMap allAttributes = new LinkedMultiValueMap(); + List attributes = this.attributeMap.get(annotationType); + if (attributes == null) { return null; } - AnnotationAttributes result = new AnnotationAttributes(original.size()); - for (Map.Entry entry : original.entrySet()) { - try { - Object value = entry.getValue(); - if (value instanceof AnnotationAttributes) { - value = convertClassValues((AnnotationAttributes) value, classValuesAsString); - } - else if (value instanceof AnnotationAttributes[]) { - AnnotationAttributes[] values = (AnnotationAttributes[])value; - for (int i = 0; i < values.length; i++) { - values[i] = convertClassValues(values[i], classValuesAsString); - } - } - else if (value instanceof Type) { - value = (classValuesAsString ? ((Type) value).getClassName() : - this.classLoader.loadClass(((Type) value).getClassName())); - } - else if (value instanceof Type[]) { - Type[] array = (Type[]) value; - Object[] convArray = (classValuesAsString ? new String[array.length] : new Class[array.length]); - for (int i = 0; i < array.length; i++) { - convArray[i] = (classValuesAsString ? array[i].getClassName() : - this.classLoader.loadClass(array[i].getClassName())); - } - value = convArray; - } - else if (classValuesAsString) { - if (value instanceof Class) { - value = ((Class) value).getName(); - } - else if (value instanceof Class[]) { - Class[] clazzArray = (Class[]) value; - String[] newValue = new String[clazzArray.length]; - for (int i = 0; i < clazzArray.length; i++) { - newValue[i] = clazzArray[i].getName(); - } - value = newValue; - } - } - result.put(entry.getKey(), value); - } - catch (Exception ex) { - // Class not found - can't resolve class reference in annotation attribute. + for (AnnotationAttributes raw : attributes) { + for (Map.Entry entry : AnnotationReadingVisitorUtils.convertClassValues( + this.classLoader, raw, classValuesAsString).entrySet()) { + allAttributes.add(entry.getKey(), entry.getValue()); } } - return result; + return allAttributes; } public boolean hasAnnotatedMethods(String annotationType) { diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationReadingVisitorUtils.java b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationReadingVisitorUtils.java new file mode 100644 index 00000000000..82189cc29fc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationReadingVisitorUtils.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2013 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.core.type.classreading; + +import java.util.Map; + +import org.springframework.asm.Type; +import org.springframework.core.annotation.AnnotationAttributes; + +/** + * Internal utility class used when reading annotations. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @author Costin Leau + * @author Phillip Webb + * @since 4.0 + */ +abstract class AnnotationReadingVisitorUtils { + + public static AnnotationAttributes convertClassValues(ClassLoader classLoader, + AnnotationAttributes original, boolean classValuesAsString) { + + if (original == null) { + return null; + } + + AnnotationAttributes result = new AnnotationAttributes(original.size()); + for (Map.Entry entry : original.entrySet()) { + try { + Object value = entry.getValue(); + if (value instanceof AnnotationAttributes) { + value = convertClassValues(classLoader, (AnnotationAttributes) value, + classValuesAsString); + } + else if (value instanceof AnnotationAttributes[]) { + AnnotationAttributes[] values = (AnnotationAttributes[])value; + for (int i = 0; i < values.length; i++) { + values[i] = convertClassValues(classLoader, values[i], + classValuesAsString); + } + } + else if (value instanceof Type) { + value = (classValuesAsString ? ((Type) value).getClassName() : + classLoader.loadClass(((Type) value).getClassName())); + } + else if (value instanceof Type[]) { + Type[] array = (Type[]) value; + Object[] convArray = (classValuesAsString ? new String[array.length] : new Class[array.length]); + for (int i = 0; i < array.length; i++) { + convArray[i] = (classValuesAsString ? array[i].getClassName() : + classLoader.loadClass(array[i].getClassName())); + } + value = convArray; + } + else if (classValuesAsString) { + if (value instanceof Class) { + value = ((Class) value).getName(); + } + else if (value instanceof Class[]) { + Class[] clazzArray = (Class[]) value; + String[] newValue = new String[clazzArray.length]; + for (int i = 0; i < clazzArray.length; i++) { + newValue[i] = clazzArray[i].getName(); + } + value = newValue; + } + } + result.put(entry.getKey(), value); + } + catch (Exception ex) { + // Class not found - can't resolve class reference in annotation attribute. + } + } + return result; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java index 4b39bbaea7b..b7e9a1e1209 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java @@ -16,7 +16,7 @@ package org.springframework.core.type.classreading; -import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.springframework.asm.AnnotationVisitor; @@ -26,6 +26,7 @@ import org.springframework.asm.SpringAsmInfo; import org.springframework.asm.Type; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.MethodMetadata; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; /** @@ -37,6 +38,7 @@ import org.springframework.util.MultiValueMap; * @author Mark Pollack * @author Costin Leau * @author Chris Beams + * @author Phillip Webb * @since 3.0 */ public class MethodMetadataReadingVisitor extends MethodVisitor implements MethodMetadata { @@ -51,7 +53,7 @@ public class MethodMetadataReadingVisitor extends MethodVisitor implements Metho protected final MultiValueMap methodMetadataMap; - protected final Map attributeMap = new LinkedHashMap(2); + protected final MultiValueMap attributeMap = new LinkedMultiValueMap(2); public MethodMetadataReadingVisitor(String name, int access, String declaringClassName, ClassLoader classLoader, @@ -93,8 +95,34 @@ public class MethodMetadataReadingVisitor extends MethodVisitor implements Metho return this.attributeMap.containsKey(annotationType); } - public AnnotationAttributes getAnnotationAttributes(String annotationType) { - return this.attributeMap.get(annotationType); + public Map getAnnotationAttributes(String annotationType) { + return getAnnotationAttributes(annotationType, false); + } + + public Map getAnnotationAttributes(String annotationType, + boolean classValuesAsString) { + List attributes = this.attributeMap.get(annotationType); + return (attributes == null ? null : AnnotationReadingVisitorUtils.convertClassValues( + this.classLoader, attributes.get(0), classValuesAsString)); + } + + public MultiValueMap getAllAnnotationAttributes(String annotationType) { + return getAllAnnotationAttributes(annotationType, false); + } + + public MultiValueMap getAllAnnotationAttributes( + String annotationType, boolean classValuesAsString) { + if(!this.attributeMap.containsKey(annotationType)) { + return null; + } + MultiValueMap allAttributes = new LinkedMultiValueMap(); + for (AnnotationAttributes annotationAttributes : this.attributeMap.get(annotationType)) { + for (Map.Entry entry : AnnotationReadingVisitorUtils.convertClassValues( + this.classLoader, annotationAttributes, classValuesAsString).entrySet()) { + allAttributes.add(entry.getKey(), entry.getValue()); + } + } + return allAttributes; } public String getDeclaringClassName() { diff --git a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java index cb6808114c0..870ccd3beea 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java @@ -18,7 +18,9 @@ package org.springframework.core.type; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -29,10 +31,12 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; import java.util.Set; import org.junit.Test; - import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; @@ -45,6 +49,7 @@ import org.springframework.stereotype.Component; * * @author Juergen Hoeller * @author Chris Beams + * @author Phillip Webb */ public class AnnotationMetadataTests { @@ -93,7 +98,7 @@ public class AnnotationMetadataTests { assertThat(metadata.hasAnnotation(Component.class.getName()), is(true)); assertThat(metadata.hasAnnotation(Scope.class.getName()), is(true)); assertThat(metadata.hasAnnotation(SpecialAttr.class.getName()), is(true)); - assertThat(metadata.getAnnotationTypes().size(), is(3)); + assertThat(metadata.getAnnotationTypes().size(), is(5)); assertThat(metadata.getAnnotationTypes().contains(Component.class.getName()), is(true)); assertThat(metadata.getAnnotationTypes().contains(Scope.class.getName()), is(true)); assertThat(metadata.getAnnotationTypes().contains(SpecialAttr.class.getName()), is(true)); @@ -104,6 +109,15 @@ public class AnnotationMetadataTests { AnnotationAttributes scopeAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(Scope.class.getName()); assertThat(scopeAttrs.size(), is(1)); assertThat(scopeAttrs.getString("value"), is("myScope")); + + Set methods = metadata.getAnnotatedMethods(DirectAnnotation.class.getName()); + MethodMetadata method = methods.iterator().next(); + assertEquals("direct", method.getAnnotationAttributes(DirectAnnotation.class.getName()).get("value")); + List allMeta = method.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("value"); + assertThat(new HashSet(allMeta), is(equalTo(new HashSet(Arrays.asList("direct", "meta"))))); + + assertTrue(metadata.isAnnotated(IsAnnotatedAnnotation.class.getName())); + { // perform tests with classValuesAsString = false (the default) AnnotationAttributes specialAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(SpecialAttr.class.getName()); assertThat(specialAttrs.size(), is(6)); @@ -137,6 +151,10 @@ public class AnnotationMetadataTests { assertTrue(optionalArray[0].getEnum("anEnum").equals(SomeEnum.DEFAULT)); assertArrayEquals(new Class[]{Void.class}, (Class[])optionalArray[0].get("classArray")); assertArrayEquals(new Class[]{Void.class}, optionalArray[0].getClassArray("classArray")); + + assertEquals("direct", metadata.getAnnotationAttributes(DirectAnnotation.class.getName()).get("value")); + allMeta = metadata.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("value"); + assertThat(new HashSet(allMeta), is(equalTo(new HashSet(Arrays.asList("direct", "meta"))))); } { // perform tests with classValuesAsString = true AnnotationAttributes specialAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(SpecialAttr.class.getName(), true); @@ -161,6 +179,10 @@ public class AnnotationMetadataTests { AnnotationAttributes[] optionalArray = specialAttrs.getAnnotationArray("optionalArray"); assertArrayEquals(new String[]{Void.class.getName()}, (String[])optionalArray[0].get("classArray")); assertArrayEquals(new String[]{Void.class.getName()}, optionalArray[0].getStringArray("classArray")); + + assertEquals(metadata.getAnnotationAttributes(DirectAnnotation.class.getName()).get("value"), "direct"); + allMeta = metadata.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("value"); + assertThat(new HashSet(allMeta), is(equalTo(new HashSet(Arrays.asList("direct", "meta"))))); } } @@ -201,6 +223,29 @@ public class AnnotationMetadataTests { NestedAnno[] optionalArray() default {@NestedAnno(value="optional", anEnum=SomeEnum.DEFAULT, classArray=Void.class)}; } + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface DirectAnnotation { + String value(); + } + + @Target({ ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + public @interface IsAnnotatedAnnotation { + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @DirectAnnotation("meta") + @IsAnnotatedAnnotation + public @interface MetaAnnotation { + } + + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Retention(RetentionPolicy.RUNTIME) + @MetaAnnotation + public @interface MetaMetaAnnotation { + } @Component("myName") @Scope("myScope") @@ -211,6 +256,8 @@ public class AnnotationMetadataTests { @NestedAnno(value = "na1", anEnum = SomeEnum.LABEL2, classArray = {Number.class}) }) @SuppressWarnings({"serial", "unused"}) + @DirectAnnotation("direct") + @MetaMetaAnnotation private static class AnnotatedComponent implements Serializable { @TestAutowired @@ -219,6 +266,11 @@ public class AnnotationMetadataTests { public void doSleep() { } + + @DirectAnnotation("direct") + @MetaMetaAnnotation + public void meta() { + } } }