1、概览 {#1概览}
本文将带你了解 "HttpMessageNotWritableException: no converter for [class ...] with preset Content-Type" 异常的原因以及解决办法。
2、原因 {#2原因}
异常的堆栈信息说明了一切:Spring 找不到合适的 HttpMessageConverter
,无法将 Java 对象转换为 HTTP 响应。
Spring 依靠客户端的 Accept
Header 来检测它需要响应的媒体类型(Media Type)。
因此,使用未预注册消息转换器(Message Converter)的 Media Type 将导致 Spring 抛出异常。
3、重现异常 {#3重现异常}
通过一个实际例子来重现异常。
创建一个 Handler Method,指定一种媒体 Media Type(用于响应),这种类型没有注册 HttpMessageConverter
。
例如,使用 MediaType.APPLICATION_XML_VALUE
或字符串 application/xml
:
@GetMapping(value = "/student/v3/{id}", produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<Student> getV3(@PathVariable("id") int id) {
return ResponseEntity.ok(new Student(id, "Robert", "Miller", "BB"));
}
接着,使用 cURL 对 http://localhost:8080/api/student/v3/1
发起请求:
curl http://localhost:8080/api/student/v3/1
端点响应如下:
{"timestamp":"2022-02-01T18:23:37.490+00:00","status":500,"error":"Internal Server Error","path":"/api/student/v3/1"}
查看后端日志,Spring 抛出了 HttpMessageNotWritableException
异常:
[org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class com.baeldung.boot.noconverterfound.model.Student] with preset Content-Type 'null']
因此,之所以会出现异常,是因为没有 HttpMessageConverter
能够将 Student
对象与 XML 之间进行序列化和反序列化。
最后,创建一个测试用例,以确认 Spring 会抛出带有指定信息的 HttpMessageNotWritableException
:
@Test
public void whenConverterNotFound_thenThrowException() throws Exception {
String url = "/api/student/v3/1";
this.mockMvc.perform(get(url))
.andExpect(status().isInternalServerError())
.andExpect(result -> assertThat(result.getResolvedException()).isInstanceOf(HttpMessageNotWritableException.class))
.andExpect(result -> assertThat(result.getResolvedException()
.getMessage()).contains("No converter for [class com.baeldung.boot.noconverterfound.model.Student] with preset Content-Type"));
}
4、解决办法 {#4解决办法}
4.1、修改响应类型 {#41修改响应类型}
最简单的办法,那就是使用已注册 Message Converter 的媒体类型。
Spring Boot 依靠自动配置来注册内置 Message Converter。
例如,如果 classpath 中存在 Jackson 2 依赖,它就会自动注册 MappingJackson2HttpMessageConverter
。
既然如此,并且知道 Spring Boot 在 Web Starter 中包含了 Jackson,那么就可以用 APPLICATION_JSON_VALUE
媒体类型创建一个新的端点:
@GetMapping(value = "/student/v2/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Student> getV2(@PathVariable("id") int id) {
return ResponseEntity.ok(new Student(id, "Kevin", "Cruyff", "AA"));
}
创建一个测试用例:
@Test
public void whenJsonConverterIsFound_thenReturnResponse() throws Exception {
String url = "/api/student/v2/1";
this.mockMvc.perform(get(url))
.andExpect(status().isOk())
.andExpect(content().json("{'id':1,'firstName':'Kevin','lastName':'Cruyff', 'grade':'AA'}"));
}
一切 OK。
Spring 没有抛出 HttpMessageNotWritableException
是因为找到了 APPLICATION_JSON_VALUE
媒体类型对应的 Message Converter 实现:MappingJackson2HttpMessageConverter
。
4.2、注册对应的 Message Converter {#42注册对应的-message-converter}
Jackson 对于 application/xml
媒体类型也提供了一个实现:MappingJackson2XmlHttpMessageConverter
。
只需要在 pom.xml
中添加如下依赖,即可自动注册:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
重新启动应用,再次使用 cURL 访问 http://localhost:8080/api/student/v3/1
:
curl http://localhost:8080/api/student/v3/1
<Student><id>1</id><firstName>Robert</firstName><lastName>Miller</lastName><grade>BB</grade></Student>
如你所见,成功地找到了 application/xml
的 Message Converter:MappingJackson2XmlHttpMessageConverter
,并用它把 Student
对象序列化为了 XML。
参考:https://www.baeldung.com/spring-no-converter-with-preset