Spring Boot 异常:HttpMessageNotWritableException: No Converter for [class ...] With Preset Content-Type

1、概览

本文将带你了解 “HttpMessageNotWritableException: no converter for [class …] with preset Content-Type” 异常的原因以及解决办法。

2、原因

异常的堆栈信息说明了一切:Spring 找不到合适的 HttpMessageConverter,无法将 Java 对象转换为 HTTP 响应。

Spring 依靠客户端的 Accept Header 来检测它需要响应的媒体类型(Media Type)。

因此,使用未预注册消息转换器(Message Converter)的 Media Type 将导致 Spring 抛出异常。

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.1、修改响应类型

最简单的办法,那就是使用已注册 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

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