From b257253a2b6890ef532f27a662c441fc4fd21f3b Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 20 Feb 2013 10:32:53 -0800 Subject: [PATCH] Support for @Conditional configuration Introduce new @Conditional annotation that can be used to filter which @Configuration classes or methods are loaded. @Conditional can be used directly or as a meta-annotation. Condition implementations are provided via the 'Condition' interface and are free to filter based on any criteria available at the time that they run. The ConditionalContext provides access to the BeanDefinitionRegistry, Environment and ConfigurableListableBeanFactory along with a ResourceLoader and ClassLoader. The existing @Profile annotation has been refactored as a @Conditional with the added benefit that it can now be used as a method level annotation. --- .../AnnotatedBeanDefinitionReader.java | 14 +- .../ClassPathBeanDefinitionScanner.java | 7 +- .../context/annotation/Condition.java | 52 +++++ .../context/annotation/ConditionContext.java | 70 ++++++ .../context/annotation/Conditional.java | 58 +++++ .../ConditionalAnnotationHelper.java | 212 ++++++++++++++++++ .../annotation/ConfigurationClass.java | 38 ++-- ...onfigurationClassBeanDefinitionReader.java | 12 +- .../annotation/ConfigurationClassParser.java | 30 ++- .../ConfigurationClassPostProcessor.java | 10 +- .../context/annotation/Profile.java | 11 +- .../context/annotation/ProfileCondition.java | 42 ++++ .../ConfigurationClassWithConditionTests.java | 170 ++++++++++++++ 13 files changed, 681 insertions(+), 45 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/context/annotation/Condition.java create mode 100644 spring-context/src/main/java/org/springframework/context/annotation/ConditionContext.java create mode 100644 spring-context/src/main/java/org/springframework/context/annotation/Conditional.java create mode 100644 spring-context/src/main/java/org/springframework/context/annotation/ConditionalAnnotationHelper.java create mode 100644 spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java create mode 100644 spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassWithConditionTests.java 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 cfbc423714..b660b0355b 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 b3f9450f98..a016b88554 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 0000000000..69543830aa --- /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 0000000000..564586501c --- /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 0000000000..9a98714254 --- /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 0000000000..acd766ea48 --- /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 0a5a646092..6ed1f64309 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 27ae171955..eab2b0bd26 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 82fbbeba52..93731a0025 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 @@ -76,6 +76,7 @@ import static org.springframework.context.annotation.MetadataUtils.*; * * @author Chris Beams * @author Juergen Hoeller + * @author Phillip Webb * @since 3.0 * @see ConfigurationClassBeanDefinitionReader */ @@ -103,6 +104,8 @@ class ConfigurationClassParser { private final ComponentScanAnnotationParser componentScanParser; + private final BeanNameGenerator beanNameGenerator; + /** * Create a new {@link ConfigurationClassParser} instance that will be used @@ -117,6 +120,7 @@ class ConfigurationClassParser { this.environment = environment; this.resourceLoader = resourceLoader; this.registry = registry; + this.beanNameGenerator = componentScanBeanNameGenerator; this.componentScanParser = new ComponentScanAnnotationParser( resourceLoader, environment, componentScanBeanNameGenerator, registry); } @@ -144,11 +148,10 @@ class ConfigurationClassParser { 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 +176,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 +259,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 +273,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)); } } } @@ -391,9 +395,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))); } } } 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 7afbc02733..933192970a 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, @@ -312,7 +313,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/Profile.java b/spring-context/src/main/java/org/springframework/context/annotation/Profile.java index ab793599c0..27900952cd 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 0000000000..424e20ae09 --- /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 0000000000..9506b817c9 --- /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 { + } + +}