From 4c1b4f33a818fd22ad95b371dbec2b53a5b749f8 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 24 Mar 2026 17:23:49 +0100 Subject: [PATCH] Skip Jaxb auto-detection in HttpMessageConverters for servers Prior to this commit, `HttpMessageConverters` would consider the JAXB message converters when building `HttpMessageConverters` instances. We noticed that, on the server side, the Jakarta JAXB dependency is very common on the classpath and often brought transitively. At runtime, this converter can use significant CPU resources when checking the `canRead`/`canWrite` methods. This can happen when content types aren't strictly called out on controller endpoints. This commit changes the auto-detection mechanism in `HttpMessageConverters` to not consider the JAXB message converter for server use cases. For client use cases, we keep considering this converter as the runtime cost there is lower. Closes gh-36302 --- .../client/samples/XmlContentTests.java | 5 +- .../XmlContentAssertionTests.java | 2 + .../resultmatches/XpathAssertionTests.java | 2 + .../XmlContentAssertionTests.java | 2 + .../resultmatchers/XpathAssertionTests.java | 2 + .../web/servlet/MockMvcExtensionsTests.kt | 6 +- .../DefaultHttpMessageConverters.java | 65 ++++++++++++++----- 7 files changed, 65 insertions(+), 19 deletions(-) diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/client/samples/XmlContentTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/client/samples/XmlContentTests.java index ee12416dc71..c8b817f6f2a 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/client/samples/XmlContentTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/client/samples/XmlContentTests.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -59,7 +60,9 @@ class XmlContentTests { """; - private final RestTestClient client = RestTestClient.bindToController(new PersonController()).build(); + private final RestTestClient client = RestTestClient.bindToController(new PersonController()) + .configureServer(server -> server.setMessageConverters(new Jaxb2RootElementHttpMessageConverter())) + .build(); @Test diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/XmlContentAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/XmlContentAssertionTests.java index ba3e9aafeca..f67c455a737 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/XmlContentAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/XmlContentAssertionTests.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.stereotype.Controller; import org.springframework.test.web.Person; import org.springframework.test.web.reactive.server.WebTestClient; @@ -58,6 +59,7 @@ class XmlContentAssertionTests { private final WebTestClient testClient = MockMvcWebTestClient.bindToController(new MusicController()) + .messageConverters(new Jaxb2RootElementHttpMessageConverter()) .alwaysExpect(status().isOk()) .alwaysExpect(content().contentType(MediaType.parseMediaType("application/xml;charset=UTF-8"))) .configureClient() diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/XpathAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/XpathAssertionTests.java index ca17a980995..073ef9329bd 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/XpathAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/XpathAssertionTests.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.stereotype.Controller; import org.springframework.test.web.Person; import org.springframework.test.web.reactive.server.WebTestClient; @@ -60,6 +61,7 @@ class XpathAssertionTests { private final WebTestClient testClient = MockMvcWebTestClient.bindToController(new MusicController()) + .messageConverters(new Jaxb2RootElementHttpMessageConverter()) .alwaysExpect(status().isOk()) .alwaysExpect(content().contentType(MediaType.parseMediaType("application/xml;charset=UTF-8"))) .configureClient() diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/XmlContentAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/XmlContentAssertionTests.java index d4bb5c7be9d..677e66bb890 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/XmlContentAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/XmlContentAssertionTests.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.stereotype.Controller; import org.springframework.test.web.Person; import org.springframework.test.web.servlet.MockMvc; @@ -65,6 +66,7 @@ class XmlContentAssertionTests { @BeforeEach void setup() { this.mockMvc = standaloneSetup(new MusicController()) + .setMessageConverters(new Jaxb2RootElementHttpMessageConverter()) .defaultRequest(get("/").accept(MediaType.APPLICATION_XML, MediaType.parseMediaType("application/xml;charset=UTF-8"))) .alwaysExpect(status().isOk()) .alwaysExpect(content().contentType(MediaType.parseMediaType("application/xml;charset=UTF-8"))) diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/XpathAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/XpathAssertionTests.java index 0bcd2f6a456..58e0d336e4c 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/XpathAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/XpathAssertionTests.java @@ -30,6 +30,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.stereotype.Controller; import org.springframework.test.web.Person; import org.springframework.test.web.servlet.MockMvc; @@ -67,6 +68,7 @@ class XpathAssertionTests { @BeforeEach void setup() throws Exception { this.mockMvc = standaloneSetup(new MusicController()) + .setMessageConverters(new Jaxb2RootElementHttpMessageConverter()) .defaultRequest(get("/").accept(MediaType.APPLICATION_XML, MediaType.parseMediaType("application/xml;charset=UTF-8"))) .alwaysExpect(status().isOk()) .alwaysExpect(content().contentType(MediaType.parseMediaType("application/xml;charset=UTF-8"))) diff --git a/spring-test/src/test/kotlin/org/springframework/test/web/servlet/MockMvcExtensionsTests.kt b/spring-test/src/test/kotlin/org/springframework/test/web/servlet/MockMvcExtensionsTests.kt index f2b6fcb1915..56752946625 100644 --- a/spring-test/src/test/kotlin/org/springframework/test/web/servlet/MockMvcExtensionsTests.kt +++ b/spring-test/src/test/kotlin/org/springframework/test/web/servlet/MockMvcExtensionsTests.kt @@ -28,6 +28,8 @@ import org.springframework.http.MediaType.APPLICATION_ATOM_XML import org.springframework.http.MediaType.APPLICATION_JSON import org.springframework.http.MediaType.APPLICATION_XML import org.springframework.http.MediaType.TEXT_PLAIN +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter import org.springframework.test.json.JsonCompareMode import org.springframework.test.web.Person import org.springframework.test.web.servlet.setup.MockMvcBuilders @@ -52,7 +54,9 @@ import java.util.Locale */ class MockMvcExtensionsTests { - private val mockMvc = MockMvcBuilders.standaloneSetup(PersonController()).build() + private val mockMvc = MockMvcBuilders.standaloneSetup(PersonController()) + .setMessageConverters(JacksonJsonHttpMessageConverter(), Jaxb2RootElementHttpMessageConverter()) + .build() @Test fun request() { 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 0c50bb36955..e94e8ff67fe 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 @@ -76,39 +76,39 @@ class DefaultHttpMessageConverters implements HttpMessageConverters { abstract static class DefaultBuilder { - private static final boolean JACKSON_PRESENT; + static final boolean JACKSON_PRESENT; - private static final boolean JACKSON_2_PRESENT; + static final boolean JACKSON_2_PRESENT; - private static final boolean GSON_PRESENT; + static final boolean GSON_PRESENT; - private static final boolean JSONB_PRESENT; + static final boolean JSONB_PRESENT; - private static final boolean KOTLIN_SERIALIZATION_JSON_PRESENT; + static final boolean KOTLIN_SERIALIZATION_JSON_PRESENT; - private static final boolean JACKSON_XML_PRESENT; + static final boolean JACKSON_XML_PRESENT; - private static final boolean JACKSON_2_XML_PRESENT; + static final boolean JACKSON_2_XML_PRESENT; - private static final boolean JAXB_2_PRESENT; + static final boolean JAXB_2_PRESENT; - private static final boolean JACKSON_SMILE_PRESENT; + static final boolean JACKSON_SMILE_PRESENT; - private static final boolean JACKSON_2_SMILE_PRESENT; + static final boolean JACKSON_2_SMILE_PRESENT; - private static final boolean JACKSON_CBOR_PRESENT; + static final boolean JACKSON_CBOR_PRESENT; - private static final boolean JACKSON_2_CBOR_PRESENT; + static final boolean JACKSON_2_CBOR_PRESENT; - private static final boolean KOTLIN_SERIALIZATION_CBOR_PRESENT; + static final boolean KOTLIN_SERIALIZATION_CBOR_PRESENT; - private static final boolean JACKSON_YAML_PRESENT; + static final boolean JACKSON_YAML_PRESENT; - private static final boolean JACKSON_2_YAML_PRESENT; + static final boolean JACKSON_2_YAML_PRESENT; - private static final boolean KOTLIN_SERIALIZATION_PROTOBUF_PRESENT; + static final boolean KOTLIN_SERIALIZATION_PROTOBUF_PRESENT; - private static final boolean ROME_PRESENT; + static final boolean ROME_PRESENT; boolean registerDefaults; @@ -461,6 +461,23 @@ class DefaultHttpMessageConverters implements HttpMessageConverters { return this; } + @Override + void detectMessageConverters() { + super.detectMessageConverters(); + // client-specific detection behavior + if (this.xmlConverter == null) { + if (JACKSON_XML_PRESENT) { + this.xmlConverter = new JacksonXmlHttpMessageConverter(); + } + else if (JACKSON_2_XML_PRESENT) { + this.xmlConverter = new MappingJackson2XmlHttpMessageConverter(); + } + else if (JAXB_2_PRESENT) { + this.xmlConverter = new Jaxb2RootElementHttpMessageConverter(); + } + } + } + @Override public HttpMessageConverters build() { if (this.registerDefaults) { @@ -573,6 +590,20 @@ class DefaultHttpMessageConverters implements HttpMessageConverters { return this; } + @Override + void detectMessageConverters() { + super.detectMessageConverters(); + // server-specific detection behavior + if (this.xmlConverter == null) { + if (JACKSON_XML_PRESENT) { + this.xmlConverter = new JacksonXmlHttpMessageConverter(); + } + else if (JACKSON_2_XML_PRESENT) { + this.xmlConverter = new MappingJackson2XmlHttpMessageConverter(); + } + } + } + @Override public HttpMessageConverters build() { if (this.registerDefaults) {