From da124a9e8931c10f102e8b493f37b352d8fb093c Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 27 Jun 2025 10:10:40 +0200 Subject: [PATCH] Make HttpMessageConverters classpath detection static Prior to this commit, the classpath detection of various `HttpMessageConverter` types was using an instance `ClassLoader`. The main goal here was to provide the feature and being able to test it with filtered classloaders. It seems this approach fails with GraalVM and we need to ensure that classpath detection is performed at class loading time for our GraalVM feature (inlining such static booleans at build time). As a result, we need to remove the tests for classpath detection. See gh-33894 --- .../DefaultHttpMessageConverters.java | 172 ++++++++---------- .../DefaultHttpMessageConvertersTests.java | 113 ------------ 2 files changed, 80 insertions(+), 205 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/converter/DefaultHttpMessageConverters.java b/spring-web/src/main/java/org/springframework/http/converter/DefaultHttpMessageConverters.java index dd7a077c679..5fbe4ff3c46 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/DefaultHttpMessageConverters.java +++ b/spring-web/src/main/java/org/springframework/http/converter/DefaultHttpMessageConverters.java @@ -92,9 +92,9 @@ class DefaultHttpMessageConverters implements HttpMessageConverters { this.clientMessageConverterConfigurer = new DefaultClientMessageConverterConfigurer(this.commonMessageConverters); this.serverMessageConverterConfigurer = new DefaultServerMessageConverterConfigurer(this.commonMessageConverters); if (registerDefaults) { - this.commonMessageConverters.registerDefaults(classLoader); - this.clientMessageConverterConfigurer.registerDefaults(classLoader); - this.serverMessageConverterConfigurer.registerDefaults(classLoader); + this.commonMessageConverters.registerDefaults(); + this.clientMessageConverterConfigurer.registerDefaults(); + this.serverMessageConverterConfigurer.registerDefaults(); } } @@ -163,6 +163,41 @@ class DefaultHttpMessageConverters implements HttpMessageConverters { static class DefaultMessageConverterConfigurer { + private static final boolean isJacksonPresent; + + private static final boolean isJackson2Present; + + private static final boolean isGsonPresent; + + private static final boolean isJsonbPresent; + + private static final boolean isKotlinSerializationJsonPresent; + + private static final boolean isJacksonXmlPresent; + + private static final boolean isJackson2XmlPresent; + + private static final boolean isJaxb2Present; + + private static final boolean isJacksonSmilePresent; + + private static final boolean isJackson2SmilePresent; + + private static final boolean isJacksonCborPresent; + + private static final boolean isJackson2CborPresent; + + private static final boolean isKotlinSerializationCborPresent; + + private static final boolean isJacksonYamlPresent; + + private static final boolean isJackson2YamlPresent; + + private static final boolean isKotlinSerializationProtobufPresent; + + private static final boolean isRomePresent; + + private final @Nullable DefaultMessageConverterConfigurer inheritedMessageConverters; private @Nullable ByteArrayHttpMessageConverter byteArrayMessageConverter; @@ -183,6 +218,28 @@ class DefaultHttpMessageConverters implements HttpMessageConverters { private final List> additionalMessageConverters = new ArrayList<>(); + static { + ClassLoader classLoader = DefaultClientMessageConverterConfigurer.class.getClassLoader(); + isJacksonPresent = ClassUtils.isPresent("tools.jackson.databind.ObjectMapper", classLoader); + isJackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) && + ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader); + isGsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader); + isJsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader); + isKotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader); + isJacksonSmilePresent = isJacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.smile.SmileMapper", classLoader); + isJackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader); + isJaxb2Present = ClassUtils.isPresent("jakarta.xml.bind.Binder", classLoader); + isJacksonXmlPresent = isJacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.xml.XmlMapper", classLoader); + isJackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); + isJacksonCborPresent = isJacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.cbor.CBORMapper", classLoader); + isJackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader); + isJacksonYamlPresent = isJacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.yaml.YAMLMapper", classLoader); + isJackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader); + isKotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader); + isKotlinSerializationProtobufPresent = ClassUtils.isPresent("kotlinx.serialization.protobuf.ProtoBuf", classLoader); + isRomePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader); + } + DefaultMessageConverterConfigurer() { this(null); } @@ -294,138 +351,69 @@ class DefaultHttpMessageConverters implements HttpMessageConverters { return result; } - void registerDefaults(ClassLoader classLoader) { + void registerDefaults() { this.byteArrayMessageConverter = new ByteArrayHttpMessageConverter(); this.stringMessageConverter = new StringHttpMessageConverter(); - if (isJacksonPresent(classLoader)) { + if (isJacksonPresent) { this.jsonMessageConverter = new JacksonJsonHttpMessageConverter(); } - else if (isJackson2Present(classLoader)) { + else if (isJackson2Present) { this.jsonMessageConverter = new MappingJackson2HttpMessageConverter(); } - else if (isGsonPresent(classLoader)) { + else if (isGsonPresent) { this.jsonMessageConverter = new GsonHttpMessageConverter(); } - else if (isJsonbPresent(classLoader)) { + else if (isJsonbPresent) { this.jsonMessageConverter = new JsonbHttpMessageConverter(); } - else if (isKotlinSerializationJsonPresent(classLoader)) { + else if (isKotlinSerializationJsonPresent) { this.jsonMessageConverter = new KotlinSerializationJsonHttpMessageConverter(); } - if (isJacksonXmlPresent(classLoader)) { + if (isJacksonXmlPresent) { this.xmlMessageConverter = new JacksonXmlHttpMessageConverter(); } - else if (isJackson2XmlPresent(classLoader)) { + else if (isJackson2XmlPresent) { this.xmlMessageConverter = new MappingJackson2XmlHttpMessageConverter(); } - else if (isJaxb2Present(classLoader)) { + else if (isJaxb2Present) { this.xmlMessageConverter = new Jaxb2RootElementHttpMessageConverter(); } - if (isJacksonSmilePresent(classLoader)) { + if (isJacksonSmilePresent) { this.smileMessageConverter = new JacksonSmileHttpMessageConverter(); } - else if (isJackson2SmilePresent(classLoader)) { + else if (isJackson2SmilePresent) { this.smileMessageConverter = new MappingJackson2SmileHttpMessageConverter(); } - if (isJacksonCborPresent(classLoader)) { + if (isJacksonCborPresent) { this.cborMessageConverter = new JacksonCborHttpMessageConverter(); } - else if (isJackson2CborPresent(classLoader)) { + else if (isJackson2CborPresent) { this.cborMessageConverter = new MappingJackson2CborHttpMessageConverter(); } - else if (isKotlinSerializationCborPresent(classLoader)) { + else if (isKotlinSerializationCborPresent) { this.cborMessageConverter = new KotlinSerializationCborHttpMessageConverter(); } - if (isJacksonYamlPresent(classLoader)) { + if (isJacksonYamlPresent) { this.yamlMessageConverter = new JacksonYamlHttpMessageConverter(); } - else if (isJackson2YamlPresent(classLoader)) { + else if (isJackson2YamlPresent) { this.yamlMessageConverter = new MappingJackson2YamlHttpMessageConverter(); } - if (isKotlinSerializationProtobufPresent(classLoader)) { + if (isKotlinSerializationProtobufPresent) { this.additionalMessageConverters.add(new KotlinSerializationProtobufHttpMessageConverter()); } - if (isRomePresent(classLoader)) { + if (isRomePresent) { this.additionalMessageConverters.add(new AtomFeedHttpMessageConverter()); this.additionalMessageConverters.add(new RssChannelHttpMessageConverter()); } } - private static boolean isRomePresent(ClassLoader classLoader) { - return ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader); - } - - private static boolean isJacksonPresent(ClassLoader classLoader) { - return ClassUtils.isPresent("tools.jackson.databind.ObjectMapper", classLoader); - } - - private static boolean isJackson2Present(ClassLoader classLoader) { - return ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) && - ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader); - } - - private static boolean isGsonPresent(ClassLoader classLoader) { - return ClassUtils.isPresent("com.google.gson.Gson", classLoader); - } - - private static boolean isJsonbPresent(ClassLoader classLoader) { - return ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader); - } - - private static boolean isKotlinSerializationJsonPresent(ClassLoader classLoader) { - return ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader); - } - - private static boolean isJacksonSmilePresent(ClassLoader classLoader) { - return isJacksonPresent(classLoader) && ClassUtils.isPresent("tools.jackson.dataformat.smile.SmileMapper", classLoader); - } - - private static boolean isJackson2SmilePresent(ClassLoader classLoader) { - return ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader); - } - - private static boolean isJaxb2Present(ClassLoader classLoader) { - return ClassUtils.isPresent("jakarta.xml.bind.Binder", classLoader); - } - - private static boolean isJacksonXmlPresent(ClassLoader classLoader) { - return isJacksonPresent(classLoader) && ClassUtils.isPresent("tools.jackson.dataformat.xml.XmlMapper", classLoader); - } - - private static boolean isJackson2XmlPresent(ClassLoader classLoader) { - return ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); - } - - private static boolean isJacksonCborPresent(ClassLoader classLoader) { - return isJacksonPresent(classLoader) && ClassUtils.isPresent("tools.jackson.dataformat.cbor.CBORMapper", classLoader); - } - - private static boolean isJackson2CborPresent(ClassLoader classLoader) { - return ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader); - } - - private static boolean isJacksonYamlPresent(ClassLoader classLoader) { - return isJacksonPresent(classLoader) && ClassUtils.isPresent("tools.jackson.dataformat.yaml.YAMLMapper", classLoader); - } - - private static boolean isJackson2YamlPresent(ClassLoader classLoader) { - return ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader); - } - - private static boolean isKotlinSerializationCborPresent(ClassLoader classLoader) { - return ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader); - } - - private static boolean isKotlinSerializationProtobufPresent(ClassLoader classLoader) { - return ClassUtils.isPresent("kotlinx.serialization.protobuf.ProtoBuf", classLoader); - } - } static class DefaultClientMessageConverterConfigurer extends DefaultMessageConverterConfigurer implements ClientMessageConverterConfigurer { @@ -489,7 +477,7 @@ class DefaultHttpMessageConverters implements HttpMessageConverters { } @Override - void registerDefaults(ClassLoader classLoader) { + void registerDefaults() { this.resourceMessageConverters = Collections.singletonList(new ResourceHttpMessageConverter(false)); } @@ -576,7 +564,7 @@ class DefaultHttpMessageConverters implements HttpMessageConverters { } @Override - void registerDefaults(ClassLoader classLoader) { + void registerDefaults() { this.resourceMessageConverters = Arrays.asList(new ResourceHttpMessageConverter(), new ResourceRegionHttpMessageConverter()); } diff --git a/spring-web/src/test/java/org/springframework/http/converter/DefaultHttpMessageConvertersTests.java b/spring-web/src/test/java/org/springframework/http/converter/DefaultHttpMessageConvertersTests.java index dbd3b826e0e..9de7e5123d5 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/DefaultHttpMessageConvertersTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/DefaultHttpMessageConvertersTests.java @@ -26,42 +26,24 @@ import java.util.Set; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import com.google.gson.Gson; -import com.rometools.rome.feed.WireFeed; -import jakarta.json.bind.Jsonb; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.dataformat.cbor.CBORMapper; -import tools.jackson.dataformat.smile.SmileMapper; -import tools.jackson.dataformat.xml.XmlMapper; -import tools.jackson.dataformat.yaml.YAMLMapper; import org.springframework.core.SmartClassLoader; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.converter.cbor.JacksonCborHttpMessageConverter; -import org.springframework.http.converter.cbor.KotlinSerializationCborHttpMessageConverter; -import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter; import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; -import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; -import org.springframework.http.converter.json.JsonbHttpMessageConverter; -import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.protobuf.KotlinSerializationProtobufHttpMessageConverter; import org.springframework.http.converter.smile.JacksonSmileHttpMessageConverter; -import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter; -import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; -import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; import org.springframework.http.converter.yaml.JacksonYamlHttpMessageConverter; -import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -282,101 +264,6 @@ class DefaultHttpMessageConvertersTests { } } - @Nested - class ClasspathDetectionTests { - - @Test - void jsonUsesJackson2WhenJacksonNotPresent() { - var classLoader = new FilteredClassLoader(ObjectMapper.class); - var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); - assertThat(converters.forServer()).hasAtLeastOneElementOfType(MappingJackson2HttpMessageConverter.class) - .doesNotHaveAnyElementsOfTypes(JacksonJsonHttpMessageConverter.class); - } - - @Test - void jsonUsesGsonWhenJacksonNotPresent() { - var classLoader = new FilteredClassLoader(ObjectMapper.class, com.fasterxml.jackson.databind.ObjectMapper.class); - var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); - assertThat(converters.forServer()).hasAtLeastOneElementOfType(GsonHttpMessageConverter.class) - .doesNotHaveAnyElementsOfTypes(JacksonJsonHttpMessageConverter.class, MappingJackson2HttpMessageConverter.class); - } - - @Test - void jsonUsesJsonbWhenJacksonAndGsonNotPresent() { - var classLoader = new FilteredClassLoader(ObjectMapper.class, com.fasterxml.jackson.databind.ObjectMapper.class, Gson.class); - var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); - assertThat(converters.forServer()).hasAtLeastOneElementOfType(JsonbHttpMessageConverter.class) - .doesNotHaveAnyElementsOfTypes(JacksonJsonHttpMessageConverter.class, MappingJackson2HttpMessageConverter.class, - GsonHttpMessageConverter.class); - } - - @Test - void jsonUsesKotlinWhenOthersNotPresent() { - var classLoader = new FilteredClassLoader(ObjectMapper.class, com.fasterxml.jackson.databind.ObjectMapper.class, Gson.class, Jsonb.class); - var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); - assertThat(converters.forServer()).hasAtLeastOneElementOfType(KotlinSerializationJsonHttpMessageConverter.class) - .doesNotHaveAnyElementsOfTypes(JacksonJsonHttpMessageConverter.class, MappingJackson2HttpMessageConverter.class, - GsonHttpMessageConverter.class, JsonbHttpMessageConverter.class); - } - - @Test - void xmlUsesJackson2WhenJacksonNotPresent() { - var classLoader = new FilteredClassLoader(XmlMapper.class); - var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); - assertThat(converters.forServer()).hasAtLeastOneElementOfType(MappingJackson2XmlHttpMessageConverter.class) - .doesNotHaveAnyElementsOfTypes(JacksonXmlHttpMessageConverter.class); - } - - @Test - void xmlUsesJaxbWhenJacksonNotPresent() { - var classLoader = new FilteredClassLoader(XmlMapper.class, com.fasterxml.jackson.dataformat.xml.XmlMapper.class); - var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); - assertThat(converters.forServer()).hasAtLeastOneElementOfType(Jaxb2RootElementHttpMessageConverter.class) - .doesNotHaveAnyElementsOfTypes(JacksonXmlHttpMessageConverter.class, MappingJackson2XmlHttpMessageConverter.class); - } - - @Test - void smileUsesJackson2WhenJacksonNotPresent() { - var classLoader = new FilteredClassLoader(SmileMapper.class); - var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); - assertThat(converters.forServer()).hasAtLeastOneElementOfType(MappingJackson2SmileHttpMessageConverter.class) - .doesNotHaveAnyElementsOfTypes(JacksonSmileHttpMessageConverter.class); - } - - @Test - void cborUsesJackson2WhenJacksonNotPresent() { - var classLoader = new FilteredClassLoader(CBORMapper.class); - var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); - assertThat(converters.forServer()).hasAtLeastOneElementOfType(MappingJackson2CborHttpMessageConverter.class) - .doesNotHaveAnyElementsOfTypes(JacksonCborHttpMessageConverter.class); - } - - @Test - void cborUsesKotlinWhenJacksonNotPresent() { - var classLoader = new FilteredClassLoader(CBORMapper.class, com.fasterxml.jackson.dataformat.cbor.CBORFactory.class); - var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); - assertThat(converters.forServer()).hasAtLeastOneElementOfType(KotlinSerializationCborHttpMessageConverter.class) - .doesNotHaveAnyElementsOfTypes(JacksonCborHttpMessageConverter.class, MappingJackson2CborHttpMessageConverter.class); - } - - @Test - void yamlUsesJackson2WhenJacksonNotPresent() { - var classLoader = new FilteredClassLoader(YAMLMapper.class); - var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); - assertThat(converters.forServer()).hasAtLeastOneElementOfType(MappingJackson2YamlHttpMessageConverter.class) - .doesNotHaveAnyElementsOfTypes(JacksonYamlHttpMessageConverter.class); - } - - @Test - void atomAndRssNotConfiguredWhenRomeNotPresent() { - var classLoader = new FilteredClassLoader(WireFeed.class); - var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); - assertThat(converters.forServer()).doesNotHaveAnyElementsOfTypes(AtomFeedHttpMessageConverter.class, RssChannelHttpMessageConverter.class); - } - - } - - @SuppressWarnings("unchecked") private T findMessageConverter(Class converterType, Iterable> converters) { return (T) StreamSupport