From b1ad2c30eb2d88a2bdf6f5486b10c7f17fdf7f8e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 11 May 2015 14:56:42 +0100 Subject: [PATCH] Ignore type-constrained converter when auto-configuring Jackson converter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, JacksonHttpMessageConvertersConfiguration would configure a general-purpose MappingJackson2HttpMessageConverter only if there was no existing MappingJackson2HttpMessageConverter in the application context. This was problematic when a TypeConstrainedMappingJackson2HttpMessageConverter bean was present. Such a bean is only capable of performing conversion for a specific type, and therefore is no substitute for a general purpose converter, yet its presence was causing the auto-configuration of a general purpose converters to be turned off. This would leave Spring MVC’s default converter being used for application/json requests which would not honour the user’s Jackson configuration. This commit enhances @ConditionalOnMissingBean so that the annotation can be used to specify one or more types that should be ignored when searching for beans. This allows the TypeConstrainedMappingJackson2HttpMessageConverter beans that are published by Spring Data REST to be ignored such that the general-purpose MappingJackson2HttpMessageConverter is still auto-configured. Fixes gh-2914 --- .../condition/ConditionalOnMissingBean.java | 16 +++++ .../condition/OnBeanCondition.java | 21 ++++++- ...sonHttpMessageConvertersConfiguration.java | 2 +- .../ConditionalOnMissingBeanTests.java | 63 +++++++++++++++++++ ...ssageConvertersAutoConfigurationTests.java | 50 ++++++++++++++- 5 files changed, 147 insertions(+), 5 deletions(-) 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 55e84e44999..707a84a0a27 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 class 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 011d211f743..26461ff8271 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * 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. @@ -53,6 +53,7 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Dave Syer * @author Jakub Kubrynski + * @author Andy Wilkinson */ @Order(Ordered.LOWEST_PRECEDENCE) public class OnBeanCondition extends SpringBootCondition implements @@ -119,6 +120,10 @@ public class OnBeanCondition extends SpringBootCondition implements 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))); @@ -207,6 +212,8 @@ public class OnBeanCondition extends SpringBootCondition implements private final List annotations = new ArrayList(); + private final List ignoredTypes = new ArrayList(); + private final SearchStrategy strategy; public BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata, @@ -217,6 +224,8 @@ public class OnBeanCondition extends SpringBootCondition implements 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); } @@ -244,8 +253,10 @@ public class OnBeanCondition extends SpringBootCondition implements private void collect(MultiValueMap attributes, String key, List destination) { List valueList = (List) attributes.get(key); - for (String[] valueArray : valueList) { - Collections.addAll(destination, valueArray); + if (valueList != null) { + for (String[] valueArray : valueList) { + Collections.addAll(destination, valueArray); + } } } @@ -303,6 +314,10 @@ public class OnBeanCondition extends SpringBootCondition implements 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 9e315c65157..2acd118de71 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 @@ -54,7 +54,7 @@ class JacksonHttpMessageConvertersConfiguration { private HttpMapperProperties properties = new HttpMapperProperties(); @Bean - @ConditionalOnMissingBean + @ConditionalOnMissingBean(value = MappingJackson2HttpMessageConverter.class, ignoredType = "org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter") public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter( ObjectMapper objectMapper) { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter( 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 f5e02e19686..3caf1020542 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * 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. @@ -22,12 +22,17 @@ import java.util.List; import org.junit.After; import org.junit.Test; import org.springframework.beans.DirectFieldAccessor; +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; @@ -37,8 +42,11 @@ 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.assertNull; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; /** @@ -212,6 +220,36 @@ public class HttpMessageConvertersAutoConfigurationTests { .getPropertyValue("prettyPrint")); } + @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(); + + 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()); @@ -279,4 +317,14 @@ public class HttpMessageConvertersAutoConfigurationTests { } } + @Configuration + protected static class TypeConstrainedConverterConfiguration { + + @Bean + public TypeConstrainedMappingJackson2HttpMessageConverter typeConstrainedConverter() { + return new TypeConstrainedMappingJackson2HttpMessageConverter( + ResourceSupport.class); + } + } + }