【Spring】java 单体式应用微服务拆分过程 bug 修复:Json Parse Error
💡简介
将单体式 Spring 应用拆分为微服务式应用过程中,遇到了一系列有关 Spring 框架的问题,将修复过程及相关问题信息分析过程记录下来,便于以后解决同类问题,也顺便提高自己对 Spring 框架的理解。
前期还遇到循环引用导致 JSON 序列化失败、Serialize 类序列化失败等问题,暂未记录下来,未来有机会再回顾并补充相关记录。
🧠问题1️⃣——JSON parse error
- 共出现三类报错,分别对应 Spring Framework
org.springframework.data.domain
包中的Pageable
、PageRequest
、Sort
三个类。
✏️问题描述
上述三个类是用于处理数据分页和排序的关键类。其中:
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
实例,因为找不到适合的构造函数或者创造者方法,或者可能需要添加/启用类型信息。(最常见的报错提示缺乏无参构造函数)
- 经过简单分析得知,问题源于将 JSON 数据转换为对象时出现错误,具体来说, JSON 解析器无法构造
先展示一下报错信息,再介绍我所尝试的几种解决方案。
ℹ️报错信息 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
25package 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
2import 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 | feign.FeignException: status 404 reading PostRepositoryRealClient#findAllByPostTypeAndPostStatusAndDeleted(PostParams); |
🤔问题分析
这个错误表明服务器不支持传入的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
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
13import 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;
public class FeignConfigurationFactory {
public Module pageJacksonModule() {
return new PageJacksonModule();
}
}
版本报错
运行后报错:
1
2
3
4java: 无法访问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-autoconfigure
和org.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
4Caused 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 实现类的自定义封装。
构建以下
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
23import 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;
public class RestPage<T> extends PageImpl<T> {
public RestPage( List<T> content,
int page,
int size,
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());
}
}将相应 FeignClient 及 被调用类函数中涉及的
Page
类替换为RestPage
类,例如:- 原代码:
1
2
3
4
5
6
7
8
9
10
11
12
13// FeignClient
Page<Post> findByTag(; PostParams postPrams)
// 被调用类
Page<Post> findByTag( { 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
RestPage<Post> findByTag(; PostParams postPrams)
// 被调用类
RestPage<Post> findByTag( { PostParams postPrams)
String tag = postPrams.getTag();
Pageable pageable = postPrams.getPageRequest();
return new RestPage<Post>(postRepository.findByTag(tag, pageable));
}
终于成功···
🏥反思
- 对于环境配置的问题总是会花很长时间,一方面是因为这类问题往往涉及复杂的框架,有大片知识盲区,另一方面也受我个人思路影响。
- 第一个“问题”是好奇心,好奇心过重导致我更希望将所有问题的根源都摸清楚。实际上这种状态确实是人类所需要的,但我的问题在于“遇到较大困难时,既磨磨蹭蹭不能往前推进,又拖拖拉拉不肯放弃”。在本次情况中,实际上早就知道可以“曲线救国”,但始终没有把那条路放到面上考虑,而是沉浸在“我在攻克难题”的虚假幻想中。
- 第二个问题是过于自信、过于浮躁,主要体现在明明梳理不清思路,却也不愿意花费一些时间写下来(当然最后还是写了,否则也没有这篇博客了)。记录的好处实在是有点太多了,既能帮助捋清思路,又能提高工作成就感进而提高专注度,还能将坑分享给大家节省大家时间(一个人一分钟,四十个人是不是四十分钟了?[doge])
🚧ToDoList(也许会补上)
- Spring 框架基础原理
- Jackson 组件原理及相关注解作用
- Post not found: program_language/java-spring-bug-4xx 到底为啥报4xx相关错误
- Post not found: program_language/java-spring-bug-json-infinite-recursion json循环引用报错
- Serialize Json 报错
- 希望这篇博客对你有帮助!如果你有任何问题或需要进一步的帮助,请随时提问。(如果你发现了本问题的核心原因,一定要告诉我!)
- 如果你喜欢这篇文章,欢迎动动小手 给我一个follow或star。
🗺参考文献
[1] JSON parse error: Can not construct instance of org.springframework.data.domain.Page
[4] Jackson with Feign can’t deserialized Spring’s org.springframework.data.domain.Sort
[5] springmvc接收参数异常application/json not supported
- 标题: 【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 进行许可。