Commit b1ad2c30 authored by Andy Wilkinson's avatar Andy Wilkinson

Ignore type-constrained converter when auto-configuring Jackson converter

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
parent 663967f6
......@@ -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
......
/*
* 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<String> annotations = new ArrayList<String>();
private final List<String> ignoredTypes = new ArrayList<String>();
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<String, Object> attributes, String key,
List<String> destination) {
List<String[]> 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<String> getIgnoredTypes() {
return this.ignoredTypes;
}
@Override
public String toString() {
StringBuilder string = new StringBuilder();
......
......@@ -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(
......
......@@ -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<ExampleBean> {
public ExampleFactoryBean(String value) {
......
/*
* 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<String> 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);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment