Ohhnews

分类导航

$ cd ..
Baeldung原文

解决 Spring RestTemplate 中的“无合适的 HttpMessageConverter”错误

#spring#resttemplate#http 消息转换#api 开发#数据序列化

[LOADING...]

1. 引言

在 Spring 中调用 RESTful 服务时,RestTemplate 是进行同步 HTTP 通信的可靠主力。然而,开发人员最常见且令人沮丧的障碍之一是 RestClientException,特别是以下错误信息:

$ java
Could not extract response: no suitable HttpMessageConverter found for response type and content type...

当客户端收到的响应无法反序列化为所需的 Java 对象时,通常会出现此错误。 在本文中,我们将探讨导致此问题的原因,并学习如何配置应用程序以有效处理非标准 API 响应,从而避免此类错误。

2. 项目设置

首先,我们需要标准的 Spring Boot Starter Web 依赖:

$ xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>4.0.5</version>
</dependency>

对于 JSON 处理,Spring Boot 默认包含 Jackson。为了更好地理解它,我们需要确保 jackson-databind 位于类路径(classpath)中,因为它为 MappingJackson2HttpMessageConverter 提供了底层逻辑。

3. 理解并识别根本原因

要修复此错误,我们首先需要了解 Spring 如何将原始 HTTP 响应映射到 Java 对象。问题通常不在于数据本身,而在于预期通信协议的不匹配。下面我们将检查内部转换机制,识别常见的误导性媒体类型(Media Type),并学习如何检查实际的响应头。

3.1. RestTemplate 如何转换响应

RestTemplate 依赖于一组 HttpMessageConverter Bean 来转换 HTTP 请求和响应体。当响应到达时,Spring 会检查服务器发送的 Content-Type 头信息。然后,它会遍历其注册的转换器,找到一个同时支持该 MediaType 和目标 Java 类的转换器。

3.2. 常见的媒体类型不匹配

问题通常不在于数据无效,而在于元数据具有误导性。许多遗留 API 或第三方 API 返回有效的 JSON,但却将 Content-Type 头设置为 application/json 之外的其他值。常见的类型不匹配包括 text/plaintext/javascriptapplication/octet-stream

默认的 MappingJackson2HttpMessageConverter 仅声明支持 application/jsonapplication/*+json 它会忽略任何其他类型的响应,从而导致“Could not extract response: no suitable HttpMessageConverter found for response type and content type”错误。

3.3. 检查 API 响应头

为了诊断此问题,我们必须检查响应头。如果我们无法访问外部日志,可以在 application.properties 中启用 Spring Web 的调试日志:

$ properties
logging.level.org.springframework.web.client.RestTemplate=DEBUG

这将揭示服务器提供的确切 Content-Type,使我们能够针对特定的不匹配进行处理。

4. 解决错误:配置策略

在本节中,我们将创建一个名为 RestTemplateConverterConfig 的配置类来注入 RestTemplate Bean,这将帮助我们解决此错误。

4.1. 添加对自定义媒体类型的支持

最精确的修复方法是告诉 Jackson 转换器将这些不常见的媒体类型视为 JSON。 我们可以创建一个 MappingJackson2HttpMessageConverter 实例并更新其支持的媒体类型:

$ java
@Configuration
public class RestTemplateConverterConfig {
    @Bean("specificMediaTypesRestTemplate")
    public RestTemplate specificMediaTypesRestTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        List<HttpMessageConverter<?>> converters = restTemplate.getMessageConverters();
        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        jsonConverter.setSupportedMediaTypes(Arrays.asList(
          MediaType.APPLICATION_JSON,
          MediaType.TEXT_PLAIN,
          MediaType.valueOf("text/javascript")
        ));
        converters.add(jsonConverter);
        restTemplate.setMessageConverters(converters);
        return restTemplate;
    }
}

在这里,我们明确列出了我们预期会遇到的媒体类型。这使得转换器的范围保持狭窄且可控。其他意外的内容类型(如 application/octet-stream)仍然会失败,这在受控环境中通常是我们所期望的行为。

4.2. 在 RestTemplate 中手动注册转换器

如果我们想要一个更宽松的设置,可以在同一个 RestTemplateConverterConfig 类中注册另一个支持 MediaType.ALL 的 Jackson 转换器。这会指示 RestTemplate 处理服务器返回的任何内容类型:

$ java
@Bean
public RestTemplate restTemplate() {
    RestTemplate restTemplate = new RestTemplate();
    List<HttpMessageConverter<?>> converters = restTemplate.getMessageConverters();
    MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
    jsonConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.ALL));
    converters.add(jsonConverter);
    restTemplate.setMessageConverters(converters);
    return restTemplate;
}

在这里,我们调用 getMessageConverters() 方法并将其附加到现有列表中,而不是替换它。这样可以保留 Spring 注册的所有默认转换器(例如针对 Stringbyte[] 的转换器),并仅在最后添加我们自定义的 Jackson 转换器。

4.3. 使用 RestTemplate.exchange() 和 ParameterizedTypeReference

有时错误是因为我们试图反序列化为泛型集合(如 List<User>)而产生的。在此示例中,User 类是一个简单的 POJO,包含 Jackson 从 JSON 响应中映射的 idname 字段。

使用 getForObject()List.class 会因为类型擦除(Type Erasure)而在运行时丢失泛型类型信息。相反,我们应该使用带有 ParameterizedTypeReferencerestTemplate.exchange() 方法:

$ java
ResponseEntity<List<User>> response = restTemplate.exchange(
  "/users",
  HttpMethod.GET,
  null,
  new ParameterizedTypeReference<List<User>>() {}
);

ParameterizedTypeReference 的匿名子类在编译时捕获了完整的泛型类型 List<User>,从而为 Jackson 提供了足够的信息,以便将数组中的每个元素正确反序列化为 User 对象。

5. 测试与验证

为了验证我们的配置,我们将使用 MockRestServiceServer。这允许我们在无需实时外部 API 的情况下,模拟触发 RestClientException 的确切不匹配头场景。

5.1. 测试精确修复方案

在第一个场景中,我们使用 @Qualifier 注入我们的 specificMediaTypesRestTemplate。我们将模拟一个明确标记为 text/plain 的响应。这证明了我们针对特定媒体类型的精确包含方案按预期工作:

$ java
@Autowired
@Qualifier("specificMediaTypesRestTemplate")
private RestTemplate specificMediaTypesRestTemplate;

@Test
void givenSpecificMediaTypesRestTemplate_whenTextPlainResponse_thenDeserializeCorrectly() {
    MockRestServiceServer mockServer = MockRestServiceServer.createServer(specificMediaTypesRestTemplate);
    mockServer.expect(requestTo("/user"))
      .andRespond(withSuccess("{\"id\":1,\"name\":\"Sudarshan\"}", MediaType.TEXT_PLAIN));
    User user = specificMediaTypesRestTemplate.getForObject("/user", User.class);
    assertNotNull(user);
    assertEquals("Sudarshan", user.getName());
}

5.2. 测试宽松修复方案

接下来,我们测试配置了 MediaType.ALL 的主要 RestTemplate Bean。 该测试确认,通过使转换器变得宽松,它会忽略误导性的 text/plain 头,并默认使用 Jackson 进行反序列化。

$ java
@Test
void givenMockServer_whenTextPlainResponse_thenDeserializeCorrectly() {
    MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate);
    mockServer.expect(requestTo("/user"))
      .andRespond(withSuccess("{\"id\":1,\"name\":\"Sudarshan\"}", MediaType.TEXT_PLAIN));
    User user = restTemplate.getForObject("/user", User.class);
    assertNotNull(user);
    assertEquals("Sudarshan", user.getName());
}

5.3. 验证泛型类型解析

最后,我们验证 restTemplate.exchange() 策略是否正确处理集合。 即使存在不匹配的 text/plain 头,ParameterizedTypeReference 也确保了泛型信息 List<User> 在转换过程中得以保留:

$ java
@Test
void givenMockServer_whenTextPlainResponseForList_thenDeserializeWithParameterizedTypeReference() {
    MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate);
    mockServer.expect(requestTo("/users"))
      .andRespond(
        withSuccess(
          "[{\"id\":1,\"name\":\"Sudarshan\"},{\"id\":2,\"name\":\"Baeldung\"}]",
          MediaType.TEXT_PLAIN));
    ResponseEntity<List<User>> response = restTemplate.exchange(
      "/users",
      HttpMethod.GET,
      null,
      new ParameterizedTypeReference<List<User>>() {}
    );
    assertNotNull(response.getBody());
    assertEquals(2, response.getBody().size());
    assertEquals("Sudarshan", response.getBody().get(0).getName());
}

6. 结论

在本教程中,我们了解到“No Suitable HttpMessageConverter”错误很少是数据损坏的迹象,它是服务器头信息与客户端预期之间的沟通中断。通过识别返回的 Content-Type 并明确配置我们的 MappingJackson2HttpMessageConverter 以支持它,我们可以弥补这一差距。

无论你是选择通过 MediaType.ALL 支持所有媒体类型,还是严格列出例外情况(如 text/plain),理解转换器的注册过程都是构建稳健 Spring 客户端的关键。

本文中使用的完整代码示例可在 GitHub 上找到。