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上找到。