【Spring】java 单体式应用微服务拆分过程 bug 修复:Json Parse Error

【Spring】java 单体式应用微服务拆分过程 bug 修复:Json Parse Error

Fre5h1nd Lv5

💡简介

  • 将单体式 Spring 应用拆分为微服务式应用过程中,遇到了一系列有关 Spring 框架的问题,将修复过程及相关问题信息分析过程记录下来,便于以后解决同类问题,也顺便提高自己对 Spring 框架的理解。

  • 前期还遇到循环引用导致 JSON 序列化失败、Serialize 类序列化失败等问题,暂未记录下来,未来有机会再回顾并补充相关记录。

🧠问题1️⃣——JSON parse error

  • 共出现三类报错,分别对应 Spring Framework org.springframework.data.domain 包中的 PageablePageRequestSort三个类。

✏️问题描述

  • 上述三个类是用于处理数据分页和排序的关键类。其中:

    • Pageable 接口定义了处理分页信息的通用方法,PageRequest 是它的默认实现之一,提供了一个便捷的方式来构造分页请求。Sort 类用于定义数据的排序规则,可以用于单独的查询,也可以与Pageable 一起使用以实现在分页基础上的排序。在数据库查询中,通常会将Pageable和Sort对象传递给查询方法,以指定分页和排序规则,从而从数据库中检索特定范围的数据,并按指定的顺序排列。

    • 具体而言:

      • org.springframework.data.domain.Pageable

        • Pageable接口是用于定义数据分页信息的接口。它允许用户指定要检索的页码、每页的数据量以及排序规则。通常用于在数据库查询中限制结果集大小并实现分页功能。
      • org.springframework.data.domain.PageRequest

        • PageRequest是Pageable接口的默认实现之一。它提供了一个简便的方法来创建分页请求。用户可以通过构造函数传递页码、每页数据量和排序规则来创建一个PageRequest对象,然后将其用作查询方法的参数。
      • org.springframework.data.domain.Sort

        • Sort类用于指定数据排序规则。它允许用户定义一个或多个排序条件,包括属性名和排序方向(升序或降序)。在数据库查询中,Sort对象可以与Pageable一起使用,以便在分页的基础上对数据进行排序。

🤔问题分析

  • 本项目迁移至微服务过程中,一个变为远程调用的函数使用了 Pageable 作为函数参数。

  • 在远程调用时,先将相应参数转化为 JSON 格式数据,再将 JSON 数据转换为相应对象。

    • 经过简单分析得知,问题源于将 JSON 数据转换为对象时出现错误,具体来说, JSON 解析器无法构造 Pageable 实例,因为找不到适合的构造函数或者创造者方法,或者可能需要添加/启用类型信息。(最常见的报错提示缺乏无参构造函数)
  • 先展示一下报错信息,再介绍我所尝试的几种解决方案。

ℹ️报错信息 1

1
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `org.springframework.data.domain.Pageable` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.springframework.data.domain.Pageable` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information at [Source: java.io.PushbackInputStream@11b5cfbc; line: 1, column: 59] (through reference chain: com.raysmond.blog.common.models.PostParams["pageRequest"])

ℹ️报错信息 2

1
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Can not construct instance of `org.springframework.data.domain.PageRequest``: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of org.springframework.data.domain.PageRequest: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?) at [Source: java.io.PushbackInputStream@72267439; line: 1, column: 60] (through reference chain: com.raysmond.blog.common.models.PostParams[\"pageRequest\"])","path":"/PostRepositoryController/error/general"}

🛠️相关尝试及解决方案

🔨尝试方法1️⃣——封装基础类

  • 根据提示可知,缺乏无参构造函数。而基础包中的代码是只读的无法修改,因此采用封装的方式,参考[1][2]实现一个继承基础类的新类,和基础类相比仅仅多了一个无参构造函数。

  • 以 PageRequest 为例,查看源码后构造新类如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package com.raysmond.blog.common.models;

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


    public class PageRequest extends org.springframework.data.domain.PageRequest {

    public PageRequest() {
    super(1, 1);
    }

    public PageRequest(int page, int size) {
    super(page, size);
    }

    public PageRequest(int page, int size, Direction direction, String... properties) {
    super(page, size, direction, properties);
    }

    public PageRequest(int page, int size, Sort sort) {
    super(page, size, sort);
    }

    }
  • 实现后,将所有地方的使用的基础类替换为新类,一个简单的方法是查找替换:

    1
    2
    import com.raysmond.blog.common.models.PageRequest;
    // import org.springframework.data.domain.PageRequest;
  • 修改后似乎解决了该问题,但出现了第二个错误问题——404。因此尝试使用其他解决方案。

🔨尝试方法2️⃣——启用 Jackson 配置

  • 最开始就看到这种方法[2],但在 application.yaml 中配置时发现对应配置项无法找到,因此搁置。后来检索 Sort 类的问题时再次调研到更细致的配置方法[4]

  • 具体如下:

    • 配置项(在 application 文件中配置):

      1
      feign.autoconfiguration.jackson.enabled=true
    • 导入包:

      1
      2
      # gradle
      compile 'org.springframework.cloud:spring-cloud-openfeign-core:+'
      1
      2
      3
      4
      5
      6
      7
      # maven
      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-openfeign-core</artifactId>
      <version>3.1.3</version> # 根据需要修改
      <scope>compile</scope>
      </dependency>
  • 同样,导入后报错消失,但仍然出现第二个错误问题—— 404 问题。(😵‍💫晕倒)

  • 暂且认为是解决了一个问题的第一阶段,先尝试解决第二阶段。

🧠问题2️⃣——404 Not Found

✏️问题描述

  • 如上所述,当 jackson 不再报错后,开始出现 404 问题,主要信息为Content type 'application/json;charset=UTF-8' not supported

ℹ️报错信息

1
2
3
4
5
6
7
feign.FeignException: status 404 reading PostRepositoryRealClient#findAllByPostTypeAndPostStatusAndDeleted(PostParams); 
content: {
status: 404,
error: Not Found,
exception: org.springframework.web.HttpMediaTypeNotSupportedException,
message:org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/json;charset=UTF-8' not supported
}

🤔问题分析

  • 这个错误表明服务器不支持传入的Content-Type,该Content-Type是application/json;charset=UTF-8。

    • 这可能是因为:
      • 服务器端的接口没有正确配置以处理JSON格式的请求,
      • 或者可能是由于请求的Content-Type与服务器端接口的期望不匹配。
  • 由于报错分析过于简单,有点无从下手,检索了一系列文章也没看出个所以然。因此考虑第一步骤为获取更多信息:

    💾尝试方法1️⃣——获取更多信息

  • 首先分别测试每个被调用者,看到底是哪个接口开始出错:

    • 发现 Edge 的 检查->网络控制器 中有类似于 Postman 的工具,正好用于测试。
    • 进行相应测试后发现问题还是出在接收端反序列化上。
  • 进行一系列检索后,发现一个相关解答。

💭问题原因

  • 来源于[5],对于不支持的类型,会报错Content type 'application/json;charset=UTF-8' not supported,含义实际上是无法解析 json 数据。
    • 理论上我们已经进行了相关配置,不应该出现该问题。只能怀疑是 spring 版本过低导致的 bug。
    • 然而如果要升级 spring 只会消耗更多时间精力,因此暂不考虑该方案。
  • 进一步地,[5]中导入相关包后仍然报错,根据其报错可以更深了解 spring 这部分原理。
    • spring需要使用方法TypeFactory.type(class)进行解析,当jackson mapper中没有实现方法则会报错。

🧠最终方案——曲线救国

  • 之前排查过程中其实看到一套方案,但这种方案无法触及当前问题的本质,出于对问题本质的好奇一直没有采用。
    • 然而现在已经耗费了太多时间在这个问题上,只能先曲线救国一下了。
  • 方案如下:

    使用默认支持的参数格式:
    Spring Data通常会根据请求的参数自动解析分页和排序信息,而无需将它们作为JSON对象传递。例如,在使用Spring MVC时,您可以直接将页码、每页大小和排序参数添加到URL中,Spring Data会自动解析这些参数并应用于查询。这样可以避免直接将分页对象作为JSON传递,从而避免反序列化问题。wd

🔨解决方法1️⃣——传递重要参数

  • 本项目下使用PageRequest作为Pageable的实现类,其创建方式如下:
    1
    new PageRequest(page, pageSize, Sort.Direction.DESC, "createdAt")
  • 显然,只要将各个参数传递后重新创建对象即可,因此实现以下 DTO:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    @Data
    @NoArgsConstructor
    public class PostParams {
    private PostType postType;
    private PostStatus postStatus;
    private boolean isNullPageRequest;
    private int page=0;
    private int size=5;
    private String sortDirection="DESC";
    List<String> properties= new ArrayList<String>();

    private Boolean deleted;

    public PostParams(PostType postType, PostStatus postStatus, Pageable pageRequest, Boolean deleted) {
    this.postType = postType;
    this.postStatus = postStatus;
    setPageRequest(pageRequest);
    this.deleted = deleted;
    }

    public PostParams(PostType postType, PostStatus postStatus, Boolean deleted) {
    this.postType = postType;
    this.postStatus = postStatus;
    setPageRequest(null);
    this.deleted = deleted;
    }

    public void setPageRequest(Pageable pageRequest) {
    if(pageRequest==null) {
    this.isNullPageRequest = true;
    } else {
    this.isNullPageRequest = false;
    this.page = pageRequest.getPageNumber();
    this.size = pageRequest.getPageSize();
    }
    }

    public Pageable getPageRequest() {
    return new PageRequest(this.page, this.size, Sort.Direction.fromString(this.sortDirection), this.properties.toArray(new String[this.properties.size()]));
    }

    }
  • 不得不吐槽一下,Sort 的设计太反人类了,确实是不想让人获取其中的对象(也可能是什么设计模式?)
  • 进一步地,在所有接口上封装一层函数以使用 PostParams 对象作为中介即可。

🧠问题3️⃣——后续JSON parse error(Page)

✏️问题描述

  • 后续调用函数并返回org.springframework.data.domain.Page类时出现反序列化报错,在此进行记录。
  • 上述类是 Spring Data JPA 提供的一个分页对象,可以通过它来方便地实现数据分页操作。

ℹ️报错信息 1

1
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is feign.codec.DecodeException: JSON parse error: Cannot construct instance of `org.springframework.data.domain.Page` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.springframework.data.domain.Page` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information\n at [Source: java.io.PushbackInputStream@3448ee37; line: 1, column: 1]

🤔问题分析

  • 根据报错信息可以了解,在反序列化时先创造空 Page 对象,但由于其是抽象类且没有默认构造函数,因此导致报错。
  • 根据[前文相关信息](### 🔨尝试方法2️⃣——启用 Jackson 配置)进行项目配置后仍然无法解决,暂未尝试进行 Page 实现类的自定义封装,先尝试配置方便的 FeignConfiguration。

🛠️相关尝试及解决方案

🔨尝试方法1️⃣——FeignConfiguration

  • 根据相关资料[6],创建一个新类FeignConfigurationFactory(暂时还不知道原理,先亦步亦趋)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import com.fasterxml.jackson.databind.Module;
    import org.springframework.cloud.openfeign.support.PageJacksonModule;
    import org.springframework.cloud.openfeign.support.SortJacksonModule;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    @Configuration
    public class FeignConfigurationFactory {
    @Bean
    public Module pageJacksonModule() {
    return new PageJacksonModule();
    }
    }

版本报错

  • 运行后报错:

    1
    2
    3
    4
    java: 无法访问org.springframework.cloud.openfeign.support.PageJacksonModule
    错误的类文件: /Users/freshwind/.gradle/caches/modules-2/files-2.1/org.springframework.cloud/spring-cloud-openfeign-core/4.0.4/30883d013fe1586e06e9f995020124ba6202a317/spring-cloud-openfeign-core-4.0.4.jar!/org/springframework/cloud/openfeign/support/PageJacksonModule.class
    类文件具有错误的版本 61.0, 应为 52.0
    请删除该文件或确保该文件位于正确的类路径子目录中。
  • 查询相关资料[7],判断是多个依赖发生冲突。即多个包都引用了org/springframework.cloud:spring-cloud-openfeign-core并且引用的是不同版本。

  • 在使用 Gradle 管理时,执行gradle dependencies查看依赖路径

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    +--- org.springframework.cloud:spring-cloud-openfeign-core:+ -> 4.0.4
    | +--- org.springframework.boot:spring-boot-autoconfigure:3.0.9 -> 1.5.9.RELEASE (*)
    | +--- org.springframework.boot:spring-boot-starter-aop:3.0.9 -> 1.5.9.RELEASE (*)
    | +--- io.github.openfeign.form:feign-form-spring:3.8.0
    | | +--- io.github.openfeign.form:feign-form:3.8.0
    | | | \--- org.slf4j:slf4j-api:1.7.26 -> 1.7.25
    | | +--- org.springframework:spring-web:5.1.5.RELEASE -> 4.3.13.RELEASE (*)
    | | \--- org.slf4j:slf4j-api:1.7.26 -> 1.7.25
    | \--- commons-fileupload:commons-fileupload:1.5
    | \--- commons-io:commons-io:2.11.0
  • 其中spring-cloud-openfeign-core:4.0.4与报错信息对得上,可以看见下面org.springframework.boot:spring-boot-autoconfigureorg.springframework.boot:spring-boot-starter-aop都被强制更新了版本,因此怀疑是spring-cloud-openfeign-core版本问题,尝试降低spring-cloud-openfeign-core版本:

    • 根据版本资料[8]逐个降低版本,直到无冲突产生。(但换到最低支持的版本仍然有冲突)
    • 同时发现前文已有compile('org.springframework.cloud:spring-cloud-starter-openfeign'),可能已包含spring-cloud-openfeign-core,因此考虑删除该处。
  • 后续根据相关资料[9],判断确实是 spring 组件与 java 之间的版本问题,java 1.8 无法与 spring boot 3.0 及以上版本兼容,需换成 java17。(当然此处选择了降低 spring 版本)。

  • 调整版本后最后的报错为:

    1
    2
    3
    4
    Caused by: java.lang.ClassCastException: java.lang.ClassNotFoundException cannot be cast to [Ljava.lang.Object;
    at org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector.selectImports(EnableConfigurationPropertiesImportSelector.java:54)
    at org.springframework.context.annotation.ConfigurationClassParser.processImports(ConfigurationClassParser.java:586)
    ... 21 common frames omitted
  • 经过了一系列尝试后(一晚上的努力)仍然失败,放弃该方案❌

🔨尝试方法2️⃣——Page实现类

  • 根据[前文相关信息](### 🔨尝试方法2️⃣——启用 Jackson 配置)尝试进行 Page 实现类的自定义封装。

    1. 构建以下 RestPage 类继承 Page 的默认实现类 PageImpl,设置相应构造函数及 json 配置。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
      import com.fasterxml.jackson.annotation.JsonProperty;
      import org.springframework.data.domain.Page;
      import org.springframework.data.domain.PageImpl;
      import org.springframework.data.domain.PageRequest;

      import java.util.List;

      @JsonIgnoreProperties(ignoreUnknown = true, value = {"pageable"})
      public class RestPage<T> extends PageImpl<T> {
      @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
      public RestPage(@JsonProperty("content") List<T> content,
      @JsonProperty("number") int page,
      @JsonProperty("size") int size,
      @JsonProperty("totalElements") long total) {
      super(content, new PageRequest(page, size), total);
      }

      public RestPage(Page<T> page) {
      super(page.getContent(), new PageRequest(page.getNumber(), page.getSize()), page.getTotalPages());
      }
      }
    2. 将相应 FeignClient 及 被调用类函数中涉及的 Page 类替换为 RestPage 类,例如:

      • 原代码:
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        // FeignClient
        @RequestMapping(value = "/findByTag", method = RequestMethod.POST)
        @ResponseBody
        Page<Post> findByTag(@RequestBody PostParams postPrams);

        // 被调用类
        @RequestMapping(value = "/findByTag", method = RequestMethod.POST)
        @ResponseBody
        Page<Post> findByTag(@RequestBody PostParams postPrams) {
        String tag = postPrams.getTag();
        Pageable pageable = postPrams.getPageRequest();
        return postRepository.findByTag(tag, pageable);
        }
    • 修改后代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // FeignClient
      @RequestMapping(value = "/findByTag", method = RequestMethod.POST)
      @ResponseBody
      RestPage<Post> findByTag(@RequestBody PostParams postPrams);

      // 被调用类
      @RequestMapping(value = "/findByTag", method = RequestMethod.POST)
      @ResponseBody
      RestPage<Post> findByTag(@RequestBody PostParams postPrams) {
      String tag = postPrams.getTag();
      Pageable pageable = postPrams.getPageRequest();
      return new RestPage<Post>(postRepository.findByTag(tag, pageable));
      }
  • 终于成功···

🏥反思

  • 对于环境配置的问题总是会花很长时间,一方面是因为这类问题往往涉及复杂的框架,有大片知识盲区,另一方面也受我个人思路影响。
    • 第一个“问题”是好奇心,好奇心过重导致我更希望将所有问题的根源都摸清楚。实际上这种状态确实是人类所需要的,但我的问题在于“遇到较大困难时,既磨磨蹭蹭不能往前推进,又拖拖拉拉不肯放弃”。在本次情况中,实际上早就知道可以“曲线救国”,但始终没有把那条路放到面上考虑,而是沉浸在“我在攻克难题”的虚假幻想中。
    • 第二个问题是过于自信、过于浮躁,主要体现在明明梳理不清思路,却也不愿意花费一些时间写下来(当然最后还是写了,否则也没有这篇博客了)。记录的好处实在是有点太多了,既能帮助捋清思路,又能提高工作成就感进而提高专注度,还能将坑分享给大家节省大家时间(一个人一分钟,四十个人是不是四十分钟了?[doge])

🚧ToDoList(也许会补上)

  1. Spring 框架基础原理
  2. Jackson 组件原理及相关注解作用
  3. Post not found: program_language/java-spring-bug-4xx 到底为啥报4xx相关错误
  4. Post not found: program_language/java-spring-bug-json-infinite-recursion json循环引用报错
  5. Serialize Json 报错

  • 希望这篇博客对你有帮助!如果你有任何问题或需要进一步的帮助,请随时提问。(如果你发现了本问题的核心原因,一定要告诉我!)
  • 如果你喜欢这篇文章,欢迎动动小手 给我一个follow或star。

🗺参考文献

[1] JSON parse error: Can not construct instance of org.springframework.data.domain.Page

[2] Error during Deserialization of PageImpl : Cannot construct instance of org.springframework.data.domain.PageImpl

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

[4] Jackson with Feign can’t deserialized Spring’s org.springframework.data.domain.Sort

[5] springmvc接收参数异常application/json not supported

[6] Cannot construct instance of org.springframework.data.domain.Page (no Creators, like default const

[7] Gradle 依赖&解决依赖冲突

[8] Spring Cloud OpenFeign Core

[9] 解决Springboot启动报错:类文件具有错误的版本61.0,应为 52.0

  • 标题: 【Spring】java 单体式应用微服务拆分过程 bug 修复:Json Parse Error
  • 作者: Fre5h1nd
  • 创建于 : 2023-08-25 17:29:39
  • 更新于 : 2023-09-04 15:12:20
  • 链接: https://freshwlnd.github.io/2023/08/25/program_language/java/java-spring-bug-json-parse-error/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论