處理 Spring RestTemplate 中的「沒有合適的 HttpMessageConverter」錯誤
1. 引言
在 Spring 中使用 RESTful 服務時, RestTemplate是進行同步HTTP通訊的可靠工具。然而,開發者面臨的最常見且最令人沮喪的障礙之一是RestClientException異常。具體來說,錯誤訊息如下:
Could not extract response: no suitable HttpMessageConverter found for response type and content type...
當用戶端收到無法反序列化為所需 Java 物件的回應時,通常會發生此錯誤。本文將探討其原因,並介紹如何配置應用程式以有效處理非標準 API 回應,避免此類錯誤。
2. 項目設定
首先,我們需要標準的Spring Boot Starter Web相依性:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>4.0.5</version>
</dependency>
Spring Boot 預設包含Jackson來進行JSON處理。為了更好地理解它,我們需要確保jackson-databind位於類別路徑中,因為它為MappingJackson2HttpMessageConverter提供了底層邏輯。
3. 了解並找出根本原因
為了解決這個問題,我們首先需要了解 Spring 如何將原始 HTTP 回應對應到 Java 物件。問題很少出在數據本身,而是預期通訊協定不符。接下來,我們將研究內部轉換機制,識別常見的易誤導媒體類型,並學習如何檢查實際的回應頭。
3.1. RestTemplate 如何轉換回應
RestTemplate依賴一個HttpMessageConverter bean 清單來轉換 HTTP 請求和回應體。當收到回應時,Spring 會查看伺服器傳送的Content-Type標頭。然後,它會遍歷已註冊的轉換器,找到一個既支援該MediaType又支援目標 Java 類別的轉換器。
3.2. 常見的不符媒體類型
問題通常不在於資料無效,而在於元資料具有誤導性。許多舊版或第三方 API 傳回有效的 JSON 數據,但其Content-Type標頭卻設定為application/json以外的其他類型。常見的類型不符包括text/plain 、 text/javascript,和application/octet-stream.
**預設的MappingJackson2HttpMessageConverter僅支援application/ json和application/*+json.**它會忽略任何其他類型的回應,從而導致「無法提取回應:找不到適用於回應類型和內容類型的合適 HttpMessageConverter」錯誤。
3.3 檢查 API 回應頭
要診斷這個問題,我們必須檢查請求頭。如果無法存取外部日誌,我們可以在application.properties中啟用 Spring Web 的偵錯日誌記錄:
logging.level.org.springframework.web.client.RestTemplate=DEBUG
這將顯示伺服器提供的確切Content-Type ,使我們能夠定位特定的錯誤類型。
4. 解決錯誤:設定策略
在本節中,我們將建立一個名為RestTemplateConverterConfig的配置類別來注入RestTemplate bean,這將有助於我們解決此錯誤。以下部分將詳細介紹:
4.1. 新增對自訂媒體類型的支持
最有效的解決方法是告訴 Jackson 轉換器將這些不常見的媒體類型視為 JSON 。我們可以建立一個MappingJackson2HttpMessageConverter實例並更新其支援的媒體類型:
@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類別中註冊另一個 Jackson 轉換器,並將其MediaType.ALL.這將指示RestTemplate處理伺服器傳回的任何類型的內容:
@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 註冊的所有預設轉換器,例如String和byte[]的轉換器,而只是將我們自訂的 Jackson 轉換器新增到清單末端。
4.3. 使用RestTemplate.exchange()和ParameterizedTypeReference
有時出現錯誤是因為我們嘗試將資料反序列化為泛型集合,例如List<User> 。在本例中, User類別是一個簡單的 POJO,包含id和name字段, Jackson會從JSON回應中映射這些字段。
使用List.class的getForObject()方法會在執行時期因類型擦除而遺失泛型類型資訊。因此,我們應該使用restTemplate.exchange()方法並傳入ParameterizedTypeReference參數。
ResponseEntity<List<User>> response = restTemplate.exchange(
"/users",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<User>>() {}
);
ParameterizedTypeReference的匿名子類別在編譯時捕獲完整的泛型類型List<User> ,從而為 Jackson 提供足夠的信息,以便正確地將數組中的每個元素反序列化為User對象。
5. 測試與驗證
為了驗證我們的配置,我們將使用MockRestServiceServer 。這使我們能夠模擬通常會觸發RestClientException請求頭不匹配場景,而無需使用實際的外部 API。
5.1 手術修復效果測試
在第一個場景中,我們使用@Qualifier注入我們specificMediaTypesRestTemplate 。我們將模擬一個明確標記為text/plain響應。這證明我們精準地包含該特定媒體類型能夠如預期運作:
@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進行反序列化。
@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>通用資訊:
@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 上找到。