diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidate.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidate.java new file mode 100644 index 0000000000..cc5b95ac79 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidate.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.condition; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional} that only matches when the specified bean class is already + * contained in the {@link BeanFactory} and a single candidate can be determined. + *

+ * The conditional will also match if multiple matching bean instances are already + * contained in the {@link BeanFactory} but a primary candidate has been defined; + * essentially, the condition match if auto-wiring a bean with the defined type + * will succeed. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Conditional(OnBeanCondition.class) +public @interface ConditionalOnSingleCandidate { + + /** + * The class type of bean that should be checked. The condition match if the class + * specified is contained in the {@link ApplicationContext} and a primary candidate + * exists in case of multiple instances. + *

This attribute may not be used in conjunction with + * {@link #type()}, but it may be used instead of {@link #type()}. + * @return the class type of the bean to check + */ + Class value() default Object.class; + + /** + * The class type name of bean that should be checked. The condition matches if the + * class specified is contained in the {@link ApplicationContext} and a primary + * candidate exists in case of multiple instances. + *

This attribute may not be used in conjunction with + * {@link #value()}, but it may be used instead of {@link #value()}. + * @return the class type name of the bean to check + */ + String type() default ""; + + /** + * Strategy to decide if the application context hierarchy (parent contexts) should be + * considered. + * @return the search strategy + */ + SearchStrategy search() default SearchStrategy.ALL; + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java index 011d211f74..36923e1ab5 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java @@ -24,13 +24,16 @@ import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; +import java.util.ListIterator; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.HierarchicalBeanFactory; import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Condition; @@ -53,6 +56,7 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Dave Syer * @author Jakub Kubrynski + * @author Stephane Nicoll */ @Order(Ordered.LOWEST_PRECEDENCE) public class OnBeanCondition extends SpringBootCondition implements @@ -88,6 +92,24 @@ public class OnBeanCondition extends SpringBootCondition implements matchMessage.append("@ConditionalOnBean " + spec + " found the following " + matching); } + if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) { + BeanSearchSpec spec = new SingleCandidateBeanSearchSpec(context, metadata, + ConditionalOnSingleCandidate.class); + List matching = getMatchingBeans(context, spec); + if (matching.isEmpty()) { + return ConditionOutcome.noMatch("@ConditionalOnSingleCandidate " + spec + + " found no beans"); + } + else if (hasSingleAutowireCandidate(context.getBeanFactory(), matching)) { + matchMessage.append("@ConditionalOnSingleCandidate " + spec + " found a primary " + + "candidate amongst the following " + matching); + } + else { + return ConditionOutcome.noMatch("@ConditionalOnSingleCandidate " + spec + + " found no primary candidate amongst the" + + " following " + matching); + } + } if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) { BeanSearchSpec spec = new BeanSearchSpec(context, metadata, ConditionalOnMissingBean.class); @@ -199,8 +221,29 @@ public class OnBeanCondition extends SpringBootCondition implements } } + private boolean hasSingleAutowireCandidate(ConfigurableListableBeanFactory beanFactory, + List beans) { + + if (beans.size() == 1) { + return true; + } + boolean primaryFound = false; + for (String bean : beans) { + BeanDefinition beanDefinition = beanFactory.getBeanDefinition(bean); + if (beanDefinition != null && beanDefinition.isPrimary()) { + if (primaryFound) { + return false; + } + primaryFound = true; + } + } + return primaryFound; + } + private static class BeanSearchSpec { + private final Class annotationType; + private final List names = new ArrayList(); private final List types = new ArrayList(); @@ -211,6 +254,7 @@ public class OnBeanCondition extends SpringBootCondition implements public BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata, Class annotationType) { + this.annotationType = annotationType; MultiValueMap attributes = metadata .getAllAnnotationAttributes(annotationType.getName(), true); collect(attributes, "name", this.names); @@ -220,11 +264,15 @@ public class OnBeanCondition extends SpringBootCondition implements if (this.types.isEmpty() && this.names.isEmpty()) { addDeducedBeanType(context, metadata, this.types); } - Assert.isTrue(hasAtLeastOne(this.types, this.names, this.annotations), - annotationName(annotationType) + " annotations must " - + "specify at least one bean (type, name or annotation)"); this.strategy = (SearchStrategy) metadata.getAnnotationAttributes( annotationType.getName()).get("search"); + validate(); + } + + protected void validate() { + Assert.isTrue(hasAtLeastOne(this.types, this.names, this.annotations), + annotationName() + " annotations must " + + "specify at least one bean (type, name or annotation)"); } private boolean hasAtLeastOne(List... lists) { @@ -236,16 +284,23 @@ public class OnBeanCondition extends SpringBootCondition implements return false; } - private String annotationName(Class annotationType) { - return "@" + ClassUtils.getShortName(annotationType); + protected String annotationName() { + return "@" + ClassUtils.getShortName(this.annotationType); } @SuppressWarnings({ "unchecked", "rawtypes" }) private void collect(MultiValueMap attributes, String key, List destination) { - List valueList = (List) attributes.get(key); - for (String[] valueArray : valueList) { - Collections.addAll(destination, valueArray); + List values = attributes.get(key); + if (values != null) { + for (Object value : values) { + if (value instanceof String[]) { + Collections.addAll(destination, (String[]) value); + } + else { + destination.add((String) value); + } + } } } @@ -326,4 +381,27 @@ public class OnBeanCondition extends SpringBootCondition implements } + private static class SingleCandidateBeanSearchSpec extends BeanSearchSpec { + + public SingleCandidateBeanSearchSpec(ConditionContext context, + AnnotatedTypeMetadata metadata, Class annotationType) { + super(context, metadata, annotationType); + } + + @Override + protected void validate() { + List types = getTypes(); + ListIterator it = types.listIterator(); + while (it.hasNext()) { + String value = it.next(); + if (!StringUtils.hasText(value) || Object.class.getName().equals(value)) { + it.remove(); + } + } + Assert.isTrue(types.size() == 1, annotationName() + " annotations must " + + "specify only one type (got " + types + ")"); + + } + } + } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidateTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidateTests.java new file mode 100644 index 0000000000..c1feb8ad44 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidateTests.java @@ -0,0 +1,164 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.condition; + +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import static org.hamcrest.CoreMatchers.isA; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Tests for {@link ConditionalOnSingleCandidate}. + * + * @author Stephane Nicoll + */ +public class ConditionalOnSingleCandidateTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void singleCandidateNoCandidate() { + load(OnBeanSingleCandidateConfiguration.class); + assertFalse(this.context.containsBean("baz")); + } + + @Test + public void singleCandidateOneCandidate() { + load(FooConfiguration.class, + OnBeanSingleCandidateConfiguration.class); + assertTrue(this.context.containsBean("baz")); + assertEquals("foo", this.context.getBean("baz")); + } + + @Test + public void singleCandidateMultipleCandidates() { + load(FooConfiguration.class, BarConfiguration.class, + OnBeanSingleCandidateConfiguration.class); + assertFalse(this.context.containsBean("baz")); + } + + @Test + public void singleCandidateMultipleCandidatesOnePrimary() { + load(FooPrimaryConfiguration.class, BarConfiguration.class, + OnBeanSingleCandidateConfiguration.class); + assertTrue(this.context.containsBean("baz")); + assertEquals("foo", this.context.getBean("baz")); + } + + @Test + public void singleCandidateMultipleCandidatesMultiplePrimary() { + load(FooPrimaryConfiguration.class, BarPrimaryConfiguration.class, + OnBeanSingleCandidateConfiguration.class); + assertFalse(this.context.containsBean("baz")); + } + + @Test + public void invalidAnnotationTwoTypes() { + thrown.expect(IllegalStateException.class); + thrown.expectCause(isA(IllegalArgumentException.class)); + thrown.expectMessage(OnBeanSingleCandidateTwoTypesConfiguration.class.getName()); + load(OnBeanSingleCandidateTwoTypesConfiguration.class); + } + + @Test + public void invalidAnnotationNoType() { + thrown.expect(IllegalStateException.class); + thrown.expectCause(isA(IllegalArgumentException.class)); + thrown.expectMessage(OnBeanSingleCandidateNoTypeConfiguration.class.getName()); + load(OnBeanSingleCandidateNoTypeConfiguration.class); + } + + private void load(Class... classes) { + this.context.register(classes); + this.context.refresh(); + } + + @Configuration + @ConditionalOnSingleCandidate(value = String.class) + protected static class OnBeanSingleCandidateConfiguration { + @Bean + public String baz(String s) { + return s; + } + } + + @Configuration + @ConditionalOnSingleCandidate(value = String.class, type = "java.lang.String") + protected static class OnBeanSingleCandidateTwoTypesConfiguration { + + } + + @Configuration + @ConditionalOnSingleCandidate + protected static class OnBeanSingleCandidateNoTypeConfiguration { + + } + + @Configuration + protected static class FooConfiguration { + @Bean + public String foo() { + return "foo"; + } + } + + @Configuration + protected static class FooPrimaryConfiguration { + @Bean + @Primary + public String foo() { + return "foo"; + } + } + + @Configuration + protected static class BarConfiguration { + @Bean + public String bar() { + return "bar"; + } + } + + @Configuration + protected static class BarPrimaryConfiguration { + @Bean + @Primary + public String bar() { + return "bar"; + } + } +}