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
This commit is contained in:
Brian Clozel
2026-03-24 17:23:49 +01:00
parent f1a60f1664
commit 4c1b4f33a8
7 changed files with 65 additions and 19 deletions
@@ -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
@@ -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()
@@ -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()
@@ -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")))
@@ -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")))
@@ -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() {
@@ -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) {