1. 概述
在本教程中,我們將研究 RestTemplate 以調用 RESTful 端點並讀取類型為 Page<Entity> 的響應。 我們將快速瞭解 Jackson 如何反序列化 RestTemplate 接收到的 JSON 響應。 我們將使用員工數據設置一個簡單的 RESTful 端點。
稍後,我們將設置一個客户端類,該類將使用 RestTemplate 從端點消耗數據,首先導致異常。 然後,我們將採取必要的步驟來使 RestTemplate 客户端能夠成功讀取 JSON 響應。 最後,我們將編寫一個集成測試以驗證正確行為。
2. RestTemplate 和 Jackson 序列化解
RestTemplate 是一個廣泛使用的客户端 HTTP 通信庫,它簡化了發出 HTTP 請求和處理響應的過程。當我們使用 RestTemplate 向服務器發出 HTTP 調用時,服務器的響應通常是 JSON 格式。Jackson 負責將此 JSON 響應反序列化為 Java 對象。
當 Jackson 遇到 JSON 對象並需要創建相應的 Java 類實例時,它會查找合適的構造函數或工廠方法來調用。 默認情況下,Jackson 使用默認構造函數進行實例化。 但是,在某些情況下,默認構造函數可能不可用或可能無法充分初始化對象。
為了解決此類情況,可以使用 @JsonCreator 註解來標記 Jackson 應該用於實例化,構造函數或工廠方法。 這允許我們定義在反序列化過程中自定義對象創建邏輯。
此外,當我們需要使用 Jackson 反序列化 JSON 並捕獲泛型類型時,我們可以提供 ParameterizedTypeReference 實例。 此類的目的是啓用捕獲和傳遞泛型類型。
為了捕獲泛型類型並在運行時保留它,我們需要創建一個子類,通常使用 inline 的方式,即 new ParameterizedTypeReference<List<String>>() {}。 結果實例可用於獲取 Type 實例,該實例在運行時攜帶捕獲的參數化類型信息。
接下來,讓我們設置一個包含 RESTful 端點和調用端點的客户端類的員工數據示例。
3. 定義 REST 控制器
讓我們創建一個簡單的員工數據示例。 我們將創建一個 GET /employee/data 端點,它將以分頁響應返回 EmployeeDto 數據:
@GetMapping("/data")
public ResponseEntity<Page<EmployeeDto>> getData(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
List<EmployeeDto> empList = listImplementation();
int totalSize = empList.size();
int startIndex = page * size;
int endIndex = Math.min(startIndex + size, totalSize);
List<EmployeeDto> pageContent = empList.subList(startIndex, endIndex);
Page<EmployeeDto> employeeDtos = new PageImpl<>(pageContent, PageRequest.of(page, size), totalSize);
return ResponseEntity.ok().body(employeeDtos);
}
顯然,getData() 方法返回 Page<EmplyeeDto> 作為響應,內容為 List<EmployeeDto>。
4. 定義客户端,使用 RestTemplate
讓我們考慮一個典型的場景,即從另一個外部服務通過 HTTP 調用 GET /organization/data 端點。 我們將使用 RestTemplate 定義用於調用端點的客户端。 它將嘗試將 JSON 序列化為 Page<EmployeeDto>。
為了讓 Jackson 將 JSON 數據序列化為 Page<EmployeeDto>,我們將提供具體的實現類 PageImpl,它是抽象類 Page 接口的:
@Component
public class EmployeeClient {
private final RestTemplate restTemplate;
public EmployeeClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public Page<EmployeeDto> getEmployeeDataFromExternalAPI(Pageable pageable) {
String url = "http://localhost:8080/employee";
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(url)
.queryParam("page", pageable.getPageNumber())
.queryParam("size", pageable.getPageSize());
ResponseEntity<PageImpl<EmployeeDto>> responseEntity = restTemplate.exchange(uriBuilder.toUriString(),
HttpMethod.GET, null, new ParameterizedTypeReference<PageImpl<EmployeeDto>>() {
});
return responseEntity.getBody();
}
}
但是,嘗試向 Jackson 提供 ParameterizedType<Page<EmployeeDto>> 或 ParameterizedType<PageImpl<EmployeeDto>> 將導致錯誤:
org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class org.springframework.data.domain.Pageable];
nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.springframework.data.domain.Pageable` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 160] (through reference chain: org.springframework.data.domain.PageImpl["pageable"])
5. 如何解決 HttpMessageConversionException
我們知道當 RestTemplate 調用返回 Page<EmployeeDto> 的端點時,響應無法成功讀取到 PageImpl<EmployeeDto>。 這是因為 PageImpl 類沒有默認構造函數。 此外,在現有的構造函數中沒有任何 @JsonCreator 註解。
為了解決反序列化問題,讓我們定義一個繼承 PageImpl<T>並具有默認構造函數以及 @JsonCreator 註解的自定義類:
public class CustomPageImpl<T> extends PageImpl<T> {
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public CustomPageImpl(@JsonProperty("content") List<T> content, @JsonProperty("number") int number,
@JsonProperty("size") int size, @JsonProperty("totalElements") Long totalElements,
@JsonProperty("pageable") JsonNode pageable, @JsonProperty("last") boolean last,
@JsonProperty("totalPages") int totalPages, @JsonProperty("sort") JsonNode sort,
@JsonProperty("numberOfElements") int numberOfElements) {
super(content, PageRequest.of(number, 1), 10);
}
public CustomPageImpl(List<T> content, Pageable pageable, long total) {
super(content, pageable, total);
}
public CustomPageImpl(List<T> content) {
super(content);
}
public CustomPageImpl() {
super(new ArrayList<>());
}
}
本質上,CustomPageImpl 類提供用於將 JSON 響應反序列化為類實例的自定義構造函數。 它擴展了 PageImpl 類,該類通常用於表示分頁數據。 此外,我們添加了 @JsonCreator(JsonCreator.Mode.PROPERTIES) 註解,以指定隨後的構造函數應用於反序列化。
接下來,讓我們重構客户端,以便 restTemplate.exchange() 將 JSON 響應轉換為 CustomPageImpl:
ResponseEntity<CustomPageImpl<EmployeeDto>> responseEntity = restTemplate.exchange(
uriBuilder.toUriString(),
HttpMethod.GET,
null,
new ParameterizedTypeReference<CustomPageImpl<EmployeeDto>>() {}
);
在這裏,調用 restTemplate.exchange() 方法以發送 HTTP GET 請求。 它期望接收類型為 ResponseEntity<CustomPageImpl<EmployeeDto>> 的響應。
ParameterizedTypeReference<CustomPageImpl<EmployeeDto>> 處理響應類型,允許將響應主體反序列化為包含 EmployeeDto 對象的 CustomPageImpl。 這在 Java 的類型擦除由於運行時丟失泛型類型信息而必須這樣做。
6. 集成測試
最後,我們使用 CustomPageImpl測試客户端是否按預期工作:
@Test
void givenGetData_whenRestTemplateExchange_thenReturnsPageOfEmployee() {
ResponseEntity<CustomPageImpl<EmployeeDto>> responseEntity = restTemplate.exchange(
"http://localhost:" + port + "/organization/data",
HttpMethod.GET,
null,
new ParameterizedTypeReference<CustomPageImpl<EmployeeDto>>() {}
);
assertEquals(200, responseEntity.getStatusCodeValue());
PageImpl<EmployeeDto> restPage = responseEntity.getBody();
assertNotNull(restPage);
assertEquals(10, restPage.getTotalElements());
List<EmployeeDto> content = restPage.getContent();
assertNotNull(content);
}
在此,測試通過 restTemplate.exchange調用端點返回成功的響應。它包含類型為 PageImpl<EmployeeDto>的體,內容類型為 List<EmployeeDto>,以及分頁信息。
7. 結論
在本教程中,我們探討了使用 RestTemplate 進行 HTTP 請求和處理響應的問題。我們特別關注了將響應反序列化到 Page<Entity> 過程中涉及的問題。最後,我們演示了使用 CustomPageImpl 類與 ParameterizedTypeReference 成功地將 JSON 讀取到 Page<EmployeeDto>。