RestTemplate 不使用 ParameterizedTypeReference 反序列化 HAL 内容

RestTemplate not deserializing HAL content using ParameterizedTypeReference

我正在尝试从 REST 服务中检索资源集合。我请求此集合的代码如下:

public CollectionModel<EntityModel<Doctor>> getDoctors() {
    log.info("Request to get all doctor resources");
    ParameterizedTypeReference<CollectionModel<EntityModel<Doctor>>> doctorTypeReference = 
            new ParameterizedTypeReference<CollectionModel<EntityModel<Doctor>>>() {};
    CollectionModel<EntityModel<Doctor>> doctorResources = traverson
            .follow(doctorsLink)
            .toObject(doctorTypeReference);
    log.info("The received resources are: " + doctorResources);

    return doctorResources;
}

其中 Doctor 只是一个 POJO:

@Data // Lombok annotation for setters, getter, equals, ...
@Relation(value = "doctor", collectionRelation = "doctors")
public class Doctor {
    private Long id;
    private String firstName;
    private String lastNames;
    private String email;
    private String status;
    @JsonIgnore
    private Map<String, String> links;
}

并且traverson注入了@Autowired注解:

@Autowired
private Traverson traverson;

其配置如下:

@Configuration
@Slf4j
public class RestConfiguration {
    @Value("${api.uri}")
    private String restUri;
    @Value("${auth.rest.username}")
    private String username;
    @Value("${auth.rest.password}")
    private String password;
    @Value("${ssl.trust-store}")
    private Resource trustStore;
    @Value("${ssl.trust-store.password}")
    private String trustStorePassword;

    @Bean
    public RestTemplateBuilder restTemplateBuilder() {
        return new RestTemplateBuilder(restTemplate -> {
            // Configure SSL to accept self-signed certificates
            SSLContext sslContext = null;
            try {
                sslContext = SSLContexts.custom()
                        .loadTrustMaterial(null, new TrustSelfSignedStrategy()).build();
            } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
                log.error("Unexpected exception while creating SSLContext: " + e);
            }
            SSLConnectionSocketFactory socketFactory = 
                    new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
            CloseableHttpClient httpClient = HttpClients.custom()
                .setSSLSocketFactory(socketFactory)
                .build();
            HttpComponentsClientHttpRequestFactory requestFactory = 
                    new HttpComponentsClientHttpRequestFactory();
            requestFactory.setHttpClient(httpClient);
            restTemplate.setRequestFactory(requestFactory);
            // Configure basic authentication
            restTemplate.getInterceptors().add(
                    new BasicAuthenticationInterceptor(username, password));
        });
    }

    @Bean
    public Traverson traverson() {
        Traverson traverson = new Traverson(URI.create(restUri), MediaTypes.HAL_JSON);
        // Configure the RestTemplates used by the Traverson
        traverson.setRestOperations(restTemplateBuilder().build());
        return traverson;
    }
}

我只是在使用自签名 SSL 证书和基本 HTTP 身份验证。

也就是说,调用 getDoctors 方法后记录的消息是:

The received resources are: Resources { content: [], links: [] }

我试过直接使用RestTemplate:

public CollectionModel<EntityModel<Doctor>> getDoctors() {
    log.info("Request to get all doctor resources");
    ParameterizedTypeReference<CollectionModel<EntityModel<Doctor>>> doctorTypeReference = 
            new ParameterizedTypeReference<CollectionModel<EntityModel<Doctor>>>() {};
    // builder is the @Autowired RestTemplateBuilder
    CollectionModel<EntityModel<Doctor>> doctorResources =  builder.build()
            .exchange("https://127.0.0.1:8080/guardians/api/doctors",
                HttpMethod.GET, null, doctorTypeReference).getBody();
    log.info("The received resources are: " + doctorResources);
    ResponseEntity<Object> resp = builder.build()
            .exchange("https://127.0.0.1:8080/guardians/api/doctors",
                HttpMethod.GET, null, Object.class);
    log.info("The resources using rest template are: " + resp);

    return doctorResources;
}

使用ParameterizedTypeReference作为响应类型,收到的集合仍然是空的。但是,使用 Object.class 作为响应类型,ResponseEntity 正文实际上包含预期的信息:

The resources using rest template are: <200,{_embedded={doctors=[{id=1, firstName=1, lastNames=1, email=1@guardians.com, status=AVAILABLE, absence=null, _links={self={href=https://127.0.0.1:8080/guardians/api/doctors/1}, doctors={href=https://127.0.0.1:8080/guardians/api/doctors/{?email}, templated=true}, shiftConfig={href=https://127.0.0.1:8080/guardians/api/doctors/shift-configs/1}, updateDoctor={href=https://127.0.0.1:8080/guardians/api/doctors/1}}}, {id=2, firstName=2, lastNames=2, email=2@guardians.com, status=AVAILABLE, absence=null, _links={self={href=https://127.0.0.1:8080/guardians/api/doctors/2}, doctors={href=https://127.0.0.1:8080/guardians/api/doctors/{?email}, templated=true}, shiftConfig={href=https://127.0.0.1:8080/guardians/api/doctors/shift-configs/2}, updateDoctor={href=https://127.0.0.1:8080/guardians/api/doctors/2}}}, {id=3, firstName=3, lastNames=3, email=3@guardians.com, status=AVAILABLE, absence=null, _links={self={href=https://127.0.0.1:8080/guardians/api/doctors/3}, doctors={href=https://127.0.0.1:8080/guardians/api/doctors/{?email}, templated=true}, shiftConfig={href=https://127.0.0.1:8080/guardians/api/doctors/shift-configs/3}, updateDoctor={href=https://127.0.0.1:8080/guardians/api/doctors/3}}}, {id=4, firstName=4, lastNames=4, email=4@guardians.com, status=AVAILABLE, absence=null, _links={self={href=https://127.0.0.1:8080/guardians/api/doctors/4}, doctors={href=https://127.0.0.1:8080/guardians/api/doctors/{?email}, templated=true}, shiftConfig={href=https://127.0.0.1:8080/guardians/api/doctors/shift-configs/4}, updateDoctor={href=https://127.0.0.1:8080/guardians/api/doctors/4}}},..., {id=22, firstName=22, lastNames=22, email=22@guardians.com, status=AVAILABLE, absence=null, _links={self={href=https://127.0.0.1:8080/guardians/api/doctors/22}, doctors={href=https://127.0.0.1:8080/guardians/api/doctors/{?email}, templated=true}, shiftConfig={href=https://127.0.0.1:8080/guardians/api/doctors/shift-configs/22}, updateDoctor={href=https://127.0.0.1:8080/guardians/api/doctors/22}}}]}, _links={self={href=https://127.0.0.1:8080/guardians/api/doctors/{?email}, templated=true}, newDoctor={href=https://127.0.0.1:8080/guardians/api/doctors/?startDate={startDate}, templated=true}, root={href=https://127.0.0.1:8080/guardians/api/}}},[Set-Cookie:"JSESSIONID=6F797BDA2827ACD4590708F5E354122A; Path=/guardians/api; Secure; HttpOnly", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", Strict-Transport-Security:"max-age=31536000 ; includeSubDomains", X-Frame-Options:"DENY", Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Tue, 09 Jun 2020 11:21:01 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]>

那么,如果我只请求一个资源:

public EntityModel<Doctor> getDoctor(Long doctorId) {
    log.info("Request to get doctor " + doctorId);
    ParameterizedTypeReference<EntityModel<Doctor>> doctorTypeReference = 
            new ParameterizedTypeReference<EntityModel<Doctor>>() {};
    EntityModel<Doctor> doctorEntity;
    try {
        doctorEntity = traverson
                .follow(Hop.rel(doctorLink).withParameter("doctorId", doctorId))
                .toObject(doctorTypeReference);
    } catch (NotFound e) {
        log.info("The doctor was not found");
        throw e;
    }
    log.info("The received resource is: " + doctorEntity);
    return doctorEntity;
}

内容反序列化正确,但链接不是:

The received resource is: Resource { content: Doctor(id=1, firstName=1, lastNames=1, email=1@guardians.com, status=AVAILABLE, links=null), links: [] }

编辑

我正在使用 Spring 框架来为 Web 应用程序提供服务。此应用程序使用 Spring-Hateoas.

使用 REST 服务

我发现了问题:我没有将 RestTemplate 的消息转换器配置为使用 HAL。

为了修复它,我只是在 RestTemplate 配置的末尾添加了以下内容:

restTemplate.setMessageConverters(
    Traverson.getDefaultMessageConverters(MediaTypes.HAL_JSON));

现在,提供 RestTemplateBuilderBean 看起来像这样:

@Bean
public RestTemplateBuilder restTemplateBuilder() {
    return new RestTemplateBuilder(restTemplate -> {
        // Configure SSL to accept self-signed certificates
        SSLContext sslContext = null;
        try {
            sslContext = SSLContexts.custom()
                    .loadTrustMaterial(null, new TrustSelfSignedStrategy()).build();
        } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
            log.error("Unexpected exception while creating SSLContext: " + e);
        }
        SSLConnectionSocketFactory socketFactory = 
                new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
        CloseableHttpClient httpClient = HttpClients.custom()
            .setSSLSocketFactory(socketFactory)
            .build();
        HttpComponentsClientHttpRequestFactory requestFactory = 
                new HttpComponentsClientHttpRequestFactory();
        requestFactory.setHttpClient(httpClient);
        restTemplate.setRequestFactory(requestFactory);
        // Configure basic authentication
        restTemplate.getInterceptors().add(
                new BasicAuthenticationInterceptor(username, password));
        // Configure the restTemplate to use the default HAL message converter
        restTemplate.setMessageConverters(
                Traverson.getDefaultMessageConverters(MediaTypes.HAL_JSON));
    });
}