diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.java index 70345b18de9..ff8bfca78f3 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.java @@ -32,6 +32,7 @@ import org.springframework.context.annotation.Conditional; * not already contained in the {@link BeanFactory}. * * @author Phillip Webb + * @author Andy Wilkinson */ @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @@ -53,6 +54,21 @@ public @interface ConditionalOnMissingBean { */ String[] type() default {}; + /** + * The class type of beans that should be ignored when identifying matching beans. + * @return the class types of beans to ignore + * @since 1.2.5 + */ + Class[] ignored() default {}; + + /** + * The class type names of beans that should be ignored when identifying matching + * beans. + * @return the class type names of beans to ignore + * @since 1.2.5 + */ + String[] ignoredType() default {}; + /** * The annotation type decorating a bean that should be checked. The condition matches * when each annotation specified is missing from all beans in the 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 6810a9db2c4..40ab89ec289 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 @@ -55,6 +55,7 @@ import org.springframework.util.StringUtils; * @author Dave Syer * @author Jakub Kubrynski * @author Stephane Nicoll + * @author Andy Wilkinson */ @Order(Ordered.LOWEST_PRECEDENCE) class OnBeanCondition extends SpringBootCondition implements ConfigurationCondition { @@ -136,6 +137,10 @@ class OnBeanCondition extends SpringBootCondition implements ConfigurationCondit beanNames.addAll(getBeanNamesForType(beanFactory, type, context.getClassLoader(), considerHierarchy)); } + for (String ignoredType : beans.getIgnoredTypes()) { + beanNames.removeAll(getBeanNamesForType(beanFactory, ignoredType, + context.getClassLoader(), considerHierarchy)); + } for (String annotation : beans.getAnnotations()) { beanNames.addAll(Arrays.asList(getBeanNamesForAnnotation(beanFactory, annotation, context.getClassLoader(), considerHierarchy))); @@ -243,6 +248,8 @@ class OnBeanCondition extends SpringBootCondition implements ConfigurationCondit private final List annotations = new ArrayList(); + private final List ignoredTypes = new ArrayList(); + private final SearchStrategy strategy; public BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata, @@ -254,6 +261,8 @@ class OnBeanCondition extends SpringBootCondition implements ConfigurationCondit collect(attributes, "value", this.types); collect(attributes, "type", this.types); collect(attributes, "annotation", this.annotations); + collect(attributes, "ignored", this.ignoredTypes); + collect(attributes, "ignoredType", this.ignoredTypes); if (this.types.isEmpty() && this.names.isEmpty()) { addDeducedBeanType(context, metadata, this.types); } @@ -350,6 +359,10 @@ class OnBeanCondition extends SpringBootCondition implements ConfigurationCondit return this.annotations; } + public List getIgnoredTypes() { + return this.ignoredTypes; + } + @Override public String toString() { StringBuilder string = new StringBuilder(); diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/JacksonHttpMessageConvertersConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/JacksonHttpMessageConvertersConfiguration.java index 316c43cb07c..a1bd62b6f4b 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/JacksonHttpMessageConvertersConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/JacksonHttpMessageConvertersConfiguration.java @@ -45,7 +45,9 @@ class JacksonHttpMessageConvertersConfiguration { protected static class MappingJackson2HttpMessageConverterConfiguration { @Bean - @ConditionalOnMissingBean + @ConditionalOnMissingBean(value = MappingJackson2HttpMessageConverter.class, ignoredType = { + "org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter", + "org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter" }) public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter( ObjectMapper objectMapper) { return new MappingJackson2HttpMessageConverter(objectMapper); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java index 507c2549aa5..c58edd25bb5 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java @@ -32,6 +32,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.util.Assert; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; @@ -187,6 +188,28 @@ public class ConditionalOnMissingBeanTests { equalTo("fromFactory")); } + @Test + public void testOnMissingBeanConditionWithIgnoredSubclass() { + this.context.register(CustomExampleBeanConfiguration.class, + ConditionalOnIgnoredSubclass.class, + PropertyPlaceholderAutoConfiguration.class); + this.context.refresh(); + assertThat(this.context.getBeansOfType(ExampleBean.class).size(), is(equalTo(2))); + assertThat(this.context.getBeansOfType(CustomExampleBean.class).size(), + is(equalTo(1))); + } + + @Test + public void testOnMissingBeanConditionWithIgnoredSubclassByName() { + this.context.register(CustomExampleBeanConfiguration.class, + ConditionalOnIgnoredSubclassByName.class, + PropertyPlaceholderAutoConfiguration.class); + this.context.refresh(); + assertThat(this.context.getBeansOfType(ExampleBean.class).size(), is(equalTo(2))); + assertThat(this.context.getBeansOfType(CustomExampleBean.class).size(), + is(equalTo(1))); + } + @Configuration @ConditionalOnMissingBean(name = "foo") protected static class OnBeanNameConfiguration { @@ -299,6 +322,38 @@ public class ConditionalOnMissingBeanTests { } } + @Configuration + protected static class ConditionalOnIgnoredSubclass { + + @Bean + @ConditionalOnMissingBean(value = ExampleBean.class, ignored = CustomExampleBean.class) + public ExampleBean exampleBean() { + return new ExampleBean("test"); + } + + } + + @Configuration + protected static class ConditionalOnIgnoredSubclassByName { + + @Bean + @ConditionalOnMissingBean(value = ExampleBean.class, ignoredType = "org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBeanTests.CustomExampleBean") + public ExampleBean exampleBean() { + return new ExampleBean("test"); + } + + } + + @Configuration + protected static class CustomExampleBeanConfiguration { + + @Bean + public CustomExampleBean customExampleBean() { + return new CustomExampleBean(); + } + + } + @Configuration @ConditionalOnMissingBean(annotation = EnableScheduling.class) protected static class OnAnnotationConfiguration { @@ -369,6 +424,14 @@ public class ConditionalOnMissingBeanTests { } + public static class CustomExampleBean extends ExampleBean { + + public CustomExampleBean() { + super("custom subclass"); + } + + } + public static class ExampleFactoryBean implements FactoryBean { public ExampleFactoryBean(String value) { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersAutoConfigurationTests.java index 17add6857b6..64e4c78d3c2 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersAutoConfigurationTests.java @@ -18,15 +18,21 @@ package org.springframework.boot.autoconfigure.web; import java.util.Arrays; import java.util.List; +import java.util.Map; import org.junit.After; import org.junit.Test; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.JacksonHttpMessageConvertersConfiguration.MappingJackson2HttpMessageConverterConfiguration; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; @@ -36,7 +42,10 @@ import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConve import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.Gson; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; /** @@ -187,6 +196,39 @@ public class HttpMessageConvertersAutoConfigurationTests { assertConverterBeanRegisteredWithHttpMessageConverters(StringHttpMessageConverter.class); } + @Test + public void typeConstrainedConverterDoesNotPreventAutoConfigurationOfJacksonConverter() + throws Exception { + this.context.register(JacksonObjectMapperBuilderConfig.class, + TypeConstrainedConverterConfiguration.class, + HttpMessageConvertersAutoConfiguration.class); + this.context.refresh(); + + BeanDefinition beanDefinition = this.context + .getBeanDefinition("mappingJackson2HttpMessageConverter"); + assertThat(beanDefinition.getFactoryBeanName(), + is(equalTo(MappingJackson2HttpMessageConverterConfiguration.class + .getName()))); + } + + @Test + public void typeConstrainedConverterFromSpringDataDoesNotPreventAutoConfigurationOfJacksonConverter() + throws Exception { + this.context.register(JacksonObjectMapperBuilderConfig.class, + RepositoryRestMvcConfiguration.class, + HttpMessageConvertersAutoConfiguration.class); + this.context.refresh(); + + Map beansOfType = this.context + .getBeansOfType(MappingJackson2HttpMessageConverter.class); + System.out.println(beansOfType); + BeanDefinition beanDefinition = this.context + .getBeanDefinition("mappingJackson2HttpMessageConverter"); + assertThat(beanDefinition.getFactoryBeanName(), + is(equalTo(MappingJackson2HttpMessageConverterConfiguration.class + .getName()))); + } + private void assertConverterBeanExists(Class type, String beanName) { assertEquals(1, this.context.getBeansOfType(type).size()); List beanNames = Arrays.asList(this.context.getBeanDefinitionNames()); @@ -254,4 +296,14 @@ public class HttpMessageConvertersAutoConfigurationTests { } } + @Configuration + protected static class TypeConstrainedConverterConfiguration { + + @Bean + public TypeConstrainedMappingJackson2HttpMessageConverter typeConstrainedConverter() { + return new TypeConstrainedMappingJackson2HttpMessageConverter( + ResourceSupport.class); + } + } + }