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;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.servlet.ServletContext;
import org.jspecify.annotations.Nullable;
@ -177,63 +180,19 @@ import org.springframework.web.util.pattern.PathPatternParser;
*/
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 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 kotlinSerializationCborPresent;
private static final boolean kotlinSerializationJsonPresent;
private static final boolean kotlinSerializationProtobufPresent;
static {
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);
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", 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);
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() {
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);
}
if (supportedMediaTypes.contains(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);
}
if (jacksonPresent || jackson2Present || gsonPresent || jsonbPresent || kotlinSerializationJsonPresent) {
if (supportedMediaTypes.contains(MediaType.APPLICATION_JSON)) {
map.put("json", MediaType.APPLICATION_JSON);
}
if (jacksonSmilePresent || jackson2SmilePresent) {
map.put("smile", MediaType.valueOf("application/x-jackson-smile"));
MediaType smileMediaType = new MediaType("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);
}
if (jacksonYamlPresent || jackson2YamlPresent) {
if (supportedMediaTypes.contains(MediaType.APPLICATION_ATOM_XML)) {
map.put("yaml", MediaType.APPLICATION_YAML);
}
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.mockito.Mockito.mock;
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
@ -276,9 +275,6 @@ class WebMvcConfigurationSupportExtensionTests {
ContentNegotiationManager manager = mapping.getContentNegotiationManager();
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(
this.config.mvcContentNegotiationManager(), this.config.mvcConversionService(),
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.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractJacksonHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter;
@ -53,6 +54,7 @@ import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.validation.Validator;
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.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
@ -213,6 +215,23 @@ class WebMvcConfigurationSupportTests {
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
void uriComponentsContributor() {
ApplicationContext context = initContext(WebConfig.class);