Simplify media files detection in WebMvcConfigurationSupport

Prior to this commit, `WebMvcConfigurationSupport` would configure file
extensions/media types registrations based on classpath detection.
Since gh-33894, the detection of message converters is located in a
single place, `HttpMessageConverters`.

This commit updates the `WebMvcConfigurationSupport` to use the actual
message converters configured to decide which file extensions should be
set up for content negotiation.

See gh-33894
This commit is contained in:
Brian Clozel 2025-06-27 14:58:53 +02:00
parent 02ff681c73
commit 28f9adf88e
3 changed files with 38 additions and 55 deletions

View File

@ -16,11 +16,14 @@
package org.springframework.web.servlet.config.annotation; package org.springframework.web.servlet.config.annotation;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContext;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
@ -177,63 +180,19 @@ import org.springframework.web.util.pattern.PathPatternParser;
*/ */
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware { public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
private static final boolean romePresent;
private static final boolean jaxb2Present;
private static final boolean jacksonPresent; private static final boolean jacksonPresent;
private static final boolean jackson2Present; private static final boolean jackson2Present;
private static final boolean jacksonXmlPresent;
private static final boolean jackson2XmlPresent;
private static final boolean jacksonSmilePresent;
private static final boolean jackson2SmilePresent;
private static final boolean jacksonCborPresent;
private static final boolean jackson2CborPresent;
private static final boolean jacksonYamlPresent;
private static final boolean jackson2YamlPresent;
private static final boolean gsonPresent;
private static final boolean jsonbPresent;
private static final boolean kotlinSerializationPresent; private static final boolean kotlinSerializationPresent;
private static final boolean kotlinSerializationCborPresent;
private static final boolean kotlinSerializationJsonPresent;
private static final boolean kotlinSerializationProtobufPresent;
static { static {
ClassLoader classLoader = WebMvcConfigurationSupport.class.getClassLoader(); ClassLoader classLoader = WebMvcConfigurationSupport.class.getClassLoader();
romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
jaxb2Present = ClassUtils.isPresent("jakarta.xml.bind.Binder", classLoader);
jacksonPresent = ClassUtils.isPresent("tools.jackson.databind.ObjectMapper", classLoader); jacksonPresent = ClassUtils.isPresent("tools.jackson.databind.ObjectMapper", classLoader);
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) && jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader); ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
jacksonXmlPresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.xml.XmlMapper", classLoader);
jackson2XmlPresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
jacksonSmilePresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.smile.SmileMapper", classLoader);
jackson2SmilePresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
jacksonCborPresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.cbor.CBORMapper", classLoader);
jackson2CborPresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
jacksonYamlPresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.yaml.YAMLMapper", classLoader);
jackson2YamlPresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
kotlinSerializationPresent = ClassUtils.isPresent("kotlinx.serialization.Serializable", classLoader); kotlinSerializationPresent = ClassUtils.isPresent("kotlinx.serialization.Serializable", classLoader);
kotlinSerializationCborPresent = kotlinSerializationPresent && ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader);
kotlinSerializationJsonPresent = kotlinSerializationPresent && ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
kotlinSerializationProtobufPresent = kotlinSerializationPresent && ClassUtils.isPresent("kotlinx.serialization.protobuf.ProtoBuf", classLoader);
} }
@ -444,23 +403,32 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
protected Map<String, MediaType> getDefaultMediaTypes() { protected Map<String, MediaType> getDefaultMediaTypes() {
Map<String, MediaType> map = new HashMap<>(4); Map<String, MediaType> map = new HashMap<>(4);
if (romePresent) { List<HttpMessageConverter<?>> messageConverters = getMessageConverters();
Set<MediaType> supportedMediaTypes = messageConverters.stream()
.flatMap(converter -> converter.getSupportedMediaTypes().stream())
.collect(Collectors.toSet());
if (supportedMediaTypes.contains(MediaType.APPLICATION_ATOM_XML)) {
map.put("atom", MediaType.APPLICATION_ATOM_XML); map.put("atom", MediaType.APPLICATION_ATOM_XML);
}
if (supportedMediaTypes.contains(MediaType.APPLICATION_RSS_XML)) {
map.put("rss", MediaType.APPLICATION_RSS_XML); map.put("rss", MediaType.APPLICATION_RSS_XML);
} }
if (jaxb2Present || jacksonXmlPresent || jackson2XmlPresent) { MediaType xmlUtf8MediaType = new MediaType("application", "xml", StandardCharsets.UTF_8);
if (supportedMediaTypes.contains(MediaType.APPLICATION_XML) ||
supportedMediaTypes.contains(xmlUtf8MediaType)) {
map.put("xml", MediaType.APPLICATION_XML); map.put("xml", MediaType.APPLICATION_XML);
} }
if (jacksonPresent || jackson2Present || gsonPresent || jsonbPresent || kotlinSerializationJsonPresent) { if (supportedMediaTypes.contains(MediaType.APPLICATION_JSON)) {
map.put("json", MediaType.APPLICATION_JSON); map.put("json", MediaType.APPLICATION_JSON);
} }
if (jacksonSmilePresent || jackson2SmilePresent) { MediaType smileMediaType = new MediaType("application", "x-jackson-smile");
map.put("smile", MediaType.valueOf("application/x-jackson-smile")); if (supportedMediaTypes.contains(smileMediaType)) {
map.put("smile", smileMediaType);
} }
if (jacksonCborPresent || jackson2CborPresent || kotlinSerializationCborPresent) { if (supportedMediaTypes.contains(MediaType.APPLICATION_CBOR)) {
map.put("cbor", MediaType.APPLICATION_CBOR); map.put("cbor", MediaType.APPLICATION_CBOR);
} }
if (jacksonYamlPresent || jackson2YamlPresent) { if (supportedMediaTypes.contains(MediaType.APPLICATION_ATOM_XML)) {
map.put("yaml", MediaType.APPLICATION_YAML); map.put("yaml", MediaType.APPLICATION_YAML);
} }
return map; return map;

View File

@ -91,7 +91,6 @@ import org.springframework.web.util.UrlPathHelper;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.http.MediaType.APPLICATION_XML;
/** /**
* A test fixture with a subclass of {@link WebMvcConfigurationSupport} that also * A test fixture with a subclass of {@link WebMvcConfigurationSupport} that also
@ -276,9 +275,6 @@ class WebMvcConfigurationSupportExtensionTests {
ContentNegotiationManager manager = mapping.getContentNegotiationManager(); ContentNegotiationManager manager = mapping.getContentNegotiationManager();
assertThat(manager.resolveMediaTypes(webRequest)).isEqualTo(Collections.singletonList(APPLICATION_JSON)); assertThat(manager.resolveMediaTypes(webRequest)).isEqualTo(Collections.singletonList(APPLICATION_JSON));
request.setParameter("f", "xml");
assertThat(manager.resolveMediaTypes(webRequest)).isEqualTo(Collections.singletonList(APPLICATION_XML));
SimpleUrlHandlerMapping handlerMapping = (SimpleUrlHandlerMapping) this.config.resourceHandlerMapping( SimpleUrlHandlerMapping handlerMapping = (SimpleUrlHandlerMapping) this.config.resourceHandlerMapping(
this.config.mvcContentNegotiationManager(), this.config.mvcConversionService(), this.config.mvcContentNegotiationManager(), this.config.mvcConversionService(),
this.config.mvcResourceUrlProvider()); this.config.mvcResourceUrlProvider());

View File

@ -45,6 +45,7 @@ import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.format.support.FormattingConversionService; import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractJacksonHttpMessageConverter; import org.springframework.http.converter.AbstractJacksonHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter; import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter;
@ -53,6 +54,7 @@ import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher; import org.springframework.util.PathMatcher;
import org.springframework.validation.Validator; import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
@ -213,6 +215,23 @@ class WebMvcConfigurationSupportTests {
assertThat(bodyAdvice.get(3).getClass()).isEqualTo(KotlinResponseBodyAdvice.class); assertThat(bodyAdvice.get(3).getClass()).isEqualTo(KotlinResponseBodyAdvice.class);
} }
@Test
void contentNegotiationManager() {
ApplicationContext context = initContext(WebConfig.class);
ContentNegotiationManager contentNegotiation = context.getBean(ContentNegotiationManager.class);
Map<String, MediaType> mediaTypeMappings = contentNegotiation.getMediaTypeMappings();
assertThat(mediaTypeMappings)
.containsEntry("atom", MediaType.APPLICATION_ATOM_XML)
.containsEntry("rss", MediaType.APPLICATION_RSS_XML)
.containsEntry("rss", MediaType.APPLICATION_RSS_XML)
.containsEntry("xml", MediaType.APPLICATION_XML)
.containsEntry("json", MediaType.APPLICATION_JSON)
.containsEntry("smile", MediaType.valueOf("application/x-jackson-smile"))
.containsEntry("cbor", MediaType.APPLICATION_CBOR)
.containsEntry("yaml", MediaType.APPLICATION_YAML);
}
@Test @Test
void uriComponentsContributor() { void uriComponentsContributor() {
ApplicationContext context = initContext(WebConfig.class); ApplicationContext context = initContext(WebConfig.class);