使用來自 RestTemplate 的頁面實體響應
1. 概述
在本教程中,我們將研究RestTemplate以調用 RESTful 端點並讀取Page<Entity>類型的響應。我們將快速了解 Jackson 如何反序列化RestTemplate收到的 JSON 響應。我們將使用員工數據設置一個簡單的 RESTful 端點。
接下來,我們將設置一個客戶端類,該類將使用RestTemplate來使用來自端點的數據,首先會導致異常。然後,我們將採取必要的步驟,使RestTemplate客戶端能夠成功讀取 JSON 響應。最後,我們將編寫一個集成測試來驗證正確的行為。
RestTemplate和Jackson反序列化
RestTemplate是一個廣泛使用的客戶端 HTTP 通信庫,它簡化了發出 HTTP 請求和處理響應的過程。當我們使用RestTemplate對服務器進行 HTTP 調用時,服務器的響應通常採用 JSON 格式。 Jackson 負責將此 JSON 響應反序列化為 Java 對象。
當 Jackson 遇到 JSON 對象並需要創建相應的 Java 類實例時,它會尋找合適的構造函數或工廠方法來調用。默認情況下,Jackson 使用默認構造函數進行實例化。但是,在某些情況下,默認構造函數可能不可用或不足以正確初始化對象。
為了解決這種情況,可以使用@JsonCreator註釋來標記 Jackson 應該用於實例化的構造函數或工廠方法。這允許我們在反序列化期間定義對象創建的自定義邏輯。
此外,當我們希望 Jackson 在捕獲泛型類型時反序列化 JSON,我們可以提供ParameterizedTypeReference的實例。此類的目的是捕獲和傳遞泛型類型。
為了捕獲泛型類型並在運行時保留它,我們需要創建一個子類,主要使用**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> ,我們將提供抽象Page接口的具體實現類PageImpl :
@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<CustomPageImpl<EmployeeDto>> responseEntity = restTemplate.exchange(uriBuilder.toUriString(),
HttpMethod.GET, null, new ParameterizedTypeReference<CustomPageImpl<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自定義類,並具有默認構造函數以及@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>類型的內容和分頁信息。
七、結論
在本教程中,我們了解瞭如何使用RestTemplate來發出 HTTP 請求和處理響應。我們特別關注將響應反序列化到Page<Entity>中所涉及的問題。最後,我們展示瞭如何使用CustomPageImpl類以及ParameterizedTypeReference來成功將 JSON 讀取到Page<EmployeeDto>中。
與往常一樣,示例代碼可在 GitHub 上獲取。