1. 概述

在这篇短文中,我们将深入探讨Spring中的一种异常:HttpMessageNotWritableException: no converter for [class ...] with preset Content-Type。首先,我们会揭示这个异常的根本原因,然后通过一个实际案例来演示如何复现它,并最后讲解如何解决这个问题。

2. 原因

在深入了解之前,我们先尝试理解这个异常的含义。异常堆栈跟踪提供了关键信息:它告诉我们Spring 未能找到能够将Java对象转换为HTTP响应的合适 HttpMessageConverter

简单来说,Spring依赖于Accept头来检测需要响应的媒体类型。因此,使用没有预注册消息转换器的媒体类型会导致Spring抛出这个异常。

3. 复现异常

现在我们知道引发异常的原因,让我们通过一个实际例子来演示如何复现它。

创建一个处理方法,并假设我们指定一个没有注册HttpMessageConverter的媒体类型(例如,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"));
}

接下来,发送一个请求到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由于找不到将Student对象转换为XML的HttpMessageConverter而抛出了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. 解决方案

解决这个问题的唯一方法是使用有预注册消息转换器的媒体类型。

Spring Boot依赖自动配置来注册内置消息转换器,如默认消息转换器。例如,如果类路径中存在jackson 2依赖,Spring Boot会自动注册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'}"));
}

正如我们所见,由于MappingJackson2HttpMessageConverter在后台处理了Student对象到JSON的转换,Spring没有抛出HttpMessageNotWritableException

5. 总结

在这篇教程中,我们详细讨论了Spring抛出"HttpMessageNotWritableException No converter for [class ...] with preset Content-Type"异常的原因。我们展示了如何产生这个异常以及在实践中如何解决。如往常一样,所有示例的完整源代码可以在GitHub上找到这里