Update On(Missing)Bean Condition logic

Update OnBeanCondition and OnMissingBeanCondition to work better
with @Configuration classes and to support an optional considerHierarchy
annotation value.

The class value for conditions can now also be inferred when used on
@Bean methods.
This commit is contained in:
Phillip Webb 2013-06-12 12:20:52 -07:00
parent 7d7dc5107d
commit 3536fc68f5
4 changed files with 148 additions and 28 deletions

View File

@ -16,18 +16,24 @@
package org.springframework.bootstrap.context.annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.context.annotation.Condition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.core.type.MethodMetadata;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.MethodCallback;
/**
* Base for {@link OnBeanCondition} and {@link OnMissingBeanCondition}.
@ -35,65 +41,95 @@ import org.springframework.util.MultiValueMap;
* @author Phillip Webb
* @author Dave Syer
*/
abstract class AbstractOnBeanCondition implements Condition {
abstract class AbstractOnBeanCondition implements ConfigurationCondition {
protected Log logger = LogFactory.getLog(getClass());
private List<String> beanClasses;
private List<String> beanNames;
protected abstract Class<?> annotationClass();
protected List<String> getBeanClasses() {
return this.beanClasses;
}
protected List<String> getBeanNames() {
return this.beanNames;
@Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN;
}
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(
annotationClass().getName(), true);
final List<String> beanClasses = collect(attributes, "value");
final List<String> beanNames = collect(attributes, "name");
if (beanClasses.size() == 0) {
if (metadata instanceof MethodMetadata
&& metadata.isAnnotated(Bean.class.getName())) {
try {
final MethodMetadata methodMetadata = (MethodMetadata) metadata;
// We should be safe to load at this point since we are in the
// REGISTER_BEAN phase
Class<?> configClass = ClassUtils.forName(
methodMetadata.getDeclaringClassName(),
context.getClassLoader());
ReflectionUtils.doWithMethods(configClass, new MethodCallback() {
@Override
public void doWith(Method method)
throws IllegalArgumentException, IllegalAccessException {
if (methodMetadata.getMethodName().equals(method.getName())) {
beanClasses.add(method.getReturnType().getName());
}
}
});
} catch (Exception e) {
}
}
}
Assert.isTrue(beanClasses.size() > 0 || beanNames.size() > 0,
"@" + ClassUtils.getShortName(annotationClass())
+ " annotations must specify at least one bean");
return matches(context, metadata, beanClasses, beanNames);
}
protected boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata,
List<String> beanClasses, List<String> beanNames) throws LinkageError {
String checking = ConditionLogUtils.getPrefix(this.logger, metadata);
MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(
annotationClass().getName(), true);
this.beanClasses = collect(attributes, "value");
this.beanNames = collect(attributes, "name");
Assert.isTrue(this.beanClasses.size() > 0 || this.beanNames.size() > 0, "@"
+ ClassUtils.getShortName(annotationClass())
+ " annotations must specify at least one bean");
Boolean considerHierarchy = (Boolean) metadata.getAnnotationAttributes(
annotationClass().getName()).get("considerHierarchy");
considerHierarchy = (considerHierarchy == null ? false : considerHierarchy);
List<String> beanClassesFound = new ArrayList<String>();
List<String> beanNamesFound = new ArrayList<String>();
for (String beanClass : this.beanClasses) {
for (String beanClass : beanClasses) {
try {
// eagerInit set to false to prevent early instantiation (some
// factory beans will not be able to determine their object type at this
// stage, so those are not eligible for matching this condition)
String[] beans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
context.getBeanFactory(),
ClassUtils.forName(beanClass, context.getClassLoader()), false,
false);
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
Class<?> type = ClassUtils.forName(beanClass, context.getClassLoader());
String[] beans = (considerHierarchy ? BeanFactoryUtils
.beanNamesForTypeIncludingAncestors(beanFactory, type, false,
false) : beanFactory.getBeanNamesForType(type, false,
false));
if (beans.length != 0) {
beanClassesFound.add(beanClass);
}
} catch (ClassNotFoundException ex) {
}
}
for (String beanName : this.beanNames) {
if (context.getBeanFactory().containsBeanDefinition(beanName)) {
for (String beanName : beanNames) {
if (considerHierarchy ? context.getBeanFactory().containsBean(beanName)
: context.getBeanFactory().containsLocalBean(beanName)) {
beanNamesFound.add(beanName);
}
}
boolean result = evaluate(beanClassesFound, beanNamesFound);
if (this.logger.isDebugEnabled()) {
logFoundResults(checking, "class", this.beanClasses, beanClassesFound);
logFoundResults(checking, "name", this.beanNames, beanClassesFound);
logFoundResults(checking, "class", beanClasses, beanClassesFound);
logFoundResults(checking, "name", beanNames, beanClassesFound);
this.logger.debug(checking + "Match result is: " + result);
}
return result;

View File

@ -52,4 +52,9 @@ public @interface ConditionalOnBean {
*/
String[] name() default {};
/**
* If the application context hierarchy (parent contexts) should be considered.
*/
boolean considerHierarchy() default true;
}

View File

@ -52,4 +52,9 @@ public @interface ConditionalOnMissingBean {
*/
String[] name() default {};
/**
* If the application context hierarchy (parent contexts) should be considered.
*/
boolean considerHierarchy() default true;
}

View File

@ -21,13 +21,19 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/**
* Tests for {@link OnMissingBeanCondition}.
*
* @author Dave Syer
* @author Phillip Webb
*/
@SuppressWarnings("resource")
public class OnMissingBeanConditionTests {
private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@ -49,6 +55,35 @@ public class OnMissingBeanConditionTests {
assertEquals("foo", this.context.getBean("foo"));
}
@Test
public void hierarchyConsidered() throws Exception {
this.context.register(FooConfiguration.class);
this.context.refresh();
AnnotationConfigApplicationContext childContext = new AnnotationConfigApplicationContext();
childContext.setParent(this.context);
childContext.register(HierarchyConsidered.class);
childContext.refresh();
assertFalse(childContext.containsLocalBean("bar"));
}
@Test
public void hierarchyNotConsidered() throws Exception {
this.context.register(FooConfiguration.class);
this.context.refresh();
AnnotationConfigApplicationContext childContext = new AnnotationConfigApplicationContext();
childContext.setParent(this.context);
childContext.register(HierarchyNotConsidered.class);
childContext.refresh();
assertTrue(childContext.containsLocalBean("bar"));
}
@Test
public void impliedOnBeanMethod() throws Exception {
this.context.register(ExampleBeanConfiguration.class, ImpliedOnBeanMethod.class);
this.context.refresh();
assertThat(this.context.getBeansOfType(ExampleBean.class).size(), equalTo(1));
}
@Configuration
@ConditionalOnMissingBean(name = "foo")
protected static class OnBeanNameConfiguration {
@ -66,4 +101,43 @@ public class OnMissingBeanConditionTests {
}
}
@Configuration
@ConditionalOnMissingBean(name = "foo")
protected static class HierarchyConsidered {
@Bean
public String bar() {
return "bar";
}
}
@Configuration
@ConditionalOnMissingBean(name = "foo", considerHierarchy = false)
protected static class HierarchyNotConsidered {
@Bean
public String bar() {
return "bar";
}
}
@Configuration
protected static class ExampleBeanConfiguration {
@Bean
public ExampleBean exampleBean() {
return new ExampleBean();
}
}
@Configuration
protected static class ImpliedOnBeanMethod {
@Bean
@ConditionalOnMissingBean
public ExampleBean exampleBean2() {
return new ExampleBean();
}
}
public static class ExampleBean {
}
}