java - 使用 Jackson 自定义反序列化器通过 RestTemplate 解析 Spring-Boot 2.1.0 页面响应

标签 java spring spring-boot jackson

请帮我解析 Spring 响应。我正在尝试使用 Jackson 自定义解串器。 REST 服务器在 Spring-Boot 2.1.0.RELEASE 上实现,它引入了 Spring-Data-Commons 2.1.2 和 Jackson 2.9.7。服务器端点将输出 block 返回为 Page。在 Spring-Boot 版本 1.5.x 中工作的反序列化器现在会抛出异常,因为 2.1 响应非常不同。这是来自 spring-boot 2.1 REST 端点的数据:

{
   "content": [ "3" ],
   "pageable": {
     "sort": {
       "sorted": false,
       "unsorted": true,
       "empty": true
     },
     "offset": 0,
     "pageSize": 20,
     "pageNumber": 0,
     "unpaged": false,
     "paged": true
  },
  "last": true,
  "totalPages": 1,
  "totalElements": 1,
  "size": 20,
  "number": 0,
  "numberOfElements": 1,
  "first": true,
  "sort": {
    "sorted": false,
    "unsorted": true,
    "empty": true
  },
  "empty": false
}

我尝试遵循这些 SO 答案中的建议,只要我忽略 Sort 字段,客户就可以正常工作:

com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of org.springframework.data.domain.Sort out of START_ARRAY token

Spring RestTemplate with paginated API

当我尝试解析排序字段时,下面的代码会产生异常:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 
Cannot construct instance of `org.mypackage.RestPageResponse`, 
problem: argument type mismatch at [Source: (String)
"{ "content":["1","2","3"],"first":true,"last":false,  "number":0,"numberOfElements":3,"size":3,"totalElements":90,"totalPages":30,  "sort":[{"direction":"ASC","property":"name","ignoreCase":false,"nullHandling":"NATIVE","ascending":true,"descending":false}]  }"; 
line: 1, column: 260]

下面是响应的 Java 类(领域模型)。我已将排序数据建模为此类中的一个字段,这可能是错误的,应该有一种方法可以将其在 Pageable 对象中传递给父类(super class)。

import java.util.ArrayList;
import java.util.List;

import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

public class RestPageResponse<T> extends PageImpl<T> {

    private static final long serialVersionUID = 5835593096562217592L;

    private Sort sort;

    public RestPageResponse() {
        super(new ArrayList<T>());
    }

    public RestPageResponse(List<T> content) {
        super(content);
    }

    public RestPageResponse(List<T> content, Pageable pageable, long total) {
        super(content, pageable, total);
    }

    /*
     * https://stackoverflow.com/questions/34647303/spring-resttemplate-with-paginated-api
     */
    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
    public RestPageResponse(//
        @JsonProperty("content") List<T> content, // PageImpl
        @JsonProperty("number") int number, // PageImpl
        @JsonProperty("size") int size, // PageImpl
        @JsonProperty("totalElements") long totalElements, // PageImpl
        @JsonProperty("pageable") JsonNode pageable, //
        @JsonProperty("sort") JsonNode sort, //
        @JsonProperty("totalPages") int totalPages, // computed
        @JsonProperty("first") boolean first, // computed
        @JsonProperty("last") boolean last, // computed
        @JsonProperty("empty") boolean empty, // computed
        @JsonProperty("numberOfElements") int numberOfElements // computed
    ) {
    super(content, PageRequest.of(number, size), totalElements);
    }

    @Override
    public Sort getSort() {
        return sort;
    }

    @JsonDeserialize(using = CustomSortDeserializer.class)
    public void setSort(Sort sort) {
        this.sort = sort;
    }

}

这是自定义反序列化器,归功于https://blog.thecookinkitchen.com/how-to-consume-page-response-from-a-service-in-spring-boot-97293c18ba

import java.io.IOException;

import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;

public class CustomSortDeserializer extends JsonDeserializer<Sort> {
    @Override
    public Sort deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
        ArrayNode node = jp.getCodec().readTree(jp);
        Order[] orders = new Order[node.size()];
        int i = 0;
        for (JsonNode obj : node) {
            orders[i] = new Order(Direction.valueOf(obj.get("direction").asText()), obj.get("property").asText());
            i++;
        }
        return Sort.by(orders);
    }
}

这是一个 JUnit 测试类,其中包含 Spring-boot 1.5 响应和 2.1 响应的用例,它们完全不同:

import java.lang.invoke.MethodHandles;

import org.acumos.cds.transport.RestPageResponse;
import org.junit.Assert;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class DeserializerTest {

    private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

    /*
     * Content emitted by Spring-Boot v1.5.x REST endpoint
     */
    @SuppressWarnings("rawtypes")
    @Test
    public void testRestPageResponse15() {
        String springBoot15 = "{" //
                + " \"content\":[\"1\",\"2\",\"3\"],\"first\":true,\"last\":false, " //
                + " \"number\":0,\"numberOfElements\":3,\"size\":3,\"totalElements\":90,\"totalPages\":30, " //
                + " \"sort\":[{\"direction\":\"ASC\",\"property\":\"name\",\"ignoreCase\":false,\"nullHandling\":\"NATIVE\",\"ascending\":true,\"descending\":false}] "//
                + " }";
        try {
            ObjectMapper mapper = new ObjectMapper();
            JsonNode sb15 = mapper.readTree(springBoot15);
            logger.info("SpringBoot 1.5 JSON {}", sb15.toString());
            RestPageResponse r = mapper.readValue(springBoot15, RestPageResponse.class);
            logger.info("Parsed JSON {}", mapper.writeValueAsString(r));
            Assert.assertNotNull(r.getContent());
            Assert.assertEquals(r.getContent().size(), 3);
            Assert.assertTrue(r.isFirst());
            Assert.assertFalse(r.isLast());
            Assert.assertEquals(r.getNumber(), 0);
            Assert.assertEquals(r.getNumberOfElements(), 3);
            Assert.assertEquals(r.getSize(), 3);
            Assert.assertEquals(r.getTotalPages(), 30);
            Assert.assertEquals(r.getTotalElements(), 90);
        } catch (Exception ex) {
            logger.error("testRestPageResponse15 failed", ex);
        }
    }

    /*
     * Content emitted by Spring-Boot v2.1.0 REST endpoint
     */
    @SuppressWarnings("rawtypes")
    @Test
    public void testRestPageResponse21() {
        String springBoot21 = "{"//
                + " \"content\":[\"4\",\"5\",\"6\"], \"empty\": false, "//
                + " \"pageable\": { \"sort\": {\"sorted\": false, \"unsorted\": true, \"empty\": true },"
                + " \"offset\": 0, \"pageSize\": 20, \"pageNumber\": 0, \"unpaged\": false, \"paged\": true },"
                + " \"last\": true, \"totalPages\": 1, \"totalElements\": 1, \"size\": 20, \"number\": 0, "
                + " \"numberOfElements\": 1,  \"first\": true, \"sort\": { \"sorted\": false, \"unsorted\": true, \"empty\": true }"
                + "}";
        try {
            ObjectMapper mapper = new ObjectMapper();
            JsonNode sb21 = mapper.readTree(springBoot21);
            logger.info("SpringBoot 2.1 JSON {}", sb21.toString());
            RestPageResponse s = mapper.readValue(springBoot21, RestPageResponse.class);
            logger.info("Parsed JSON {}", mapper.writeValueAsString(s));
            Assert.assertNotNull(s.getContent());
            Assert.assertEquals(s.getContent().size(), 3);
            Assert.assertTrue(s.isFirst());
            Assert.assertTrue(s.isLast());
            Assert.assertEquals(s.getNumber(), 0);
            Assert.assertEquals(s.getNumberOfElements(), 3);
            Assert.assertEquals(s.getSize(), 20);
            Assert.assertEquals(s.getTotalPages(), 1);
            Assert.assertEquals(s.getTotalElements(), 3);

        } catch (Exception ex) {
            logger.error("testRestPageResponse21 failed", ex);
        }
    }

}

如果我从响应模型中删除 @JsonDeserialize 注释,那么解析工作正常,但 Sort 字段内容被丢弃。

为什么这么难?春人在https://jira.spring.io/browse/DATACMNS-1061说“使用 HATEOAS”,我根本没有想出。提前致谢。

最佳答案

这就是我为使排序工作所做的。

使用 Rest 模板来使用 API 的类

package br.com.ironforge.springboottutorial.javaclient;

import java.util.Arrays;
import java.util.List;

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import br.com.ironforge.springboottutorial.model.PageableResponse;
import br.com.ironforge.springboottutorial.model.Student;

public class JavaSpringClientTest {

    public static void main(String[] args) {

        RestTemplate restTemplate = new RestTemplateBuilder().rootUri("http://localhost:8080/v1/protected/students")
                .basicAuthentication("username", "password").build();

        ResponseEntity<PageableResponse<Student>> exchange = restTemplate.exchange("/?sort=id,asc", HttpMethod.GET, null,
                new ParameterizedTypeReference<PageableResponse<Student>>() {
                });
        System.out.println(exchange.getBody().getContent());
    }
}

用于 API 调用的分页和排序的类

package br.com.ironforge.springboottutorial.model;

import java.util.ArrayList;
import java.util.List;

import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;


public class PageableResponse<T> extends PageImpl<T> {

    private boolean last;
    private boolean first;
    private int totalPages;

    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
    public PageableResponse(@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("first") boolean first, @JsonProperty("numberOfElements") int numberOfElements) {
        super(content, PageRequest.of(number, size), totalElements);
    }

    public PageableResponse(List<T> content, Pageable pageable, long total) {
        super(content, pageable, total);
    }

    public PageableResponse(List<T> content) {
        super(new ArrayList<T>());
    }

}

关于java - 使用 Jackson 自定义反序列化器通过 RestTemplate 解析 Spring-Boot 2.1.0 页面响应,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53330009/

相关文章:

java - Java中的原始数据类型是线程安全的吗

Java String.format() 语法

java - 为什么方法返回-1?

java - 登录 Spring 初始化

spring-boot - 有没有一种简单的方法可以在 Spring Boot 日志记录中启用堆栈跟踪?

node.js - 跟踪两个不同服务器之间的用户

java - 生活游戏规则无法正常运行

java - Spring注入(inject)模拟Bean

java - 如何使用 mybatis-string 在一个 session 中执行多个 sql 语句

java - Spring Boot RestTemplate 交换 400 错误请求