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
的实例。这个类的作用是使我们能够在运行时捕获并传递泛型类型。
为了在运行时捕获并保留泛型类型,我们需要创建一个子类,通常使用 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<PageImpl<EmployeeDto>> responseEntity = restTemplate.exchange(uriBuilder.toUriString(),
HttpMethod.GET, null, new ParameterizedTypeReference<PageImpl<EmployeeDto>>() {
});
return responseEntity.getBody();
}
}
然而,如果尝试提供ParameterizedType<Page<EmployeeDto>>
或ParameterizedType<PageImpl<EmployeeDto>>
给Jackson,会导致错误:
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的类型擦除(/java-type-erasure),运行时的泛型信息丢失了。
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>
。
如往常一样,示例代码可以在GitHub上找到。