使用來自 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 上獲取。