测试反馈,在UAT环境请求正常的接口,在Test环境报错。
直接执行Test环境的cURL,确实可以复现问题:

但是只有报错信息:
json
{
"code": 400,
"message": "非法参数 缺少请求参数: JSON parse error: create objectReader error, objectType com.xxxx.yyyy.request.PracticalTrainingRecordRequest; nested exception is com.alibaba.fastjson2.JSONException: create objectReader error, objectType com.xxxx.yyyy.request.PracticalTrainingRecordRequest"
}
首先第一直觉去看看待POJO类:
java
@Data
@ApiModel(value = "实训监控列表请求")
public class PracticalTrainingRecordRequest {
@ApiModelProperty("班级ID")
private Long classId;
/**
* 状态 NOT_STARTED("未开始"), IN_PROGRESS("进行中")
*/
private String status;
private List<String> statusList;
}
非常普通,根本不可能出现JSON序列号或反序列化问题。
无法快速定位问题,于是试图本地复现问题,复现失败。
再加上所在公司的基础设施实在是一坨屎,用docker logs -f <container_id> | grep "ERROR"方式来查看日志,不便之处有很多:
- 其他无关报错日志过多,会造成信息干扰;
- 可结合代码,使用更精确的搜索,如
docker logs -f <container_id> | grep "findByMonitorPage"来查看日志,其中findByMonitorPage就是上面报错提到的controller层接口方法名; - 但同样有问题,报错日志会过滤只显示
ERROR等关键词,看不到上下文,即看不到具体的Stacktrace
报错信息
终于看到具体报错堆栈日志:
参数校验失败: 非法参数 缺少请求参数: JSON parse error: create objectReader error, objectType com.aieshanghai.aisurgical.request.PracticalTrainingRecordRequest; nested exception is com.alibaba.fastjson2.JSONException: create objectReader error, objectType com.aieshanghai.aisurgical.request.PracticalTrainingRecordRequest
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: create objectReader error, objectType com.aieshanghai.aisurgical.request.PracticalTrainingRecordRequest; nested exception is com.alibaba.fastjson2.JSONException: create objectReader error, objectType com.aieshanghai.aisurgical.request.PracticalTrainingRecordRequest
at com.alibaba.fastjson2.support.spring.http.converter.FastJsonHttpMessageConverter.readType(FastJsonHttpMessageConverter.java:113)
at com.alibaba.fastjson2.support.spring.http.converter.FastJsonHttpMessageConverter.read(FastJsonHttpMessageConverter.java:80)
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:185)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:160)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:133)
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122)
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:179)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:146)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:681)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
Caused by: com.alibaba.fastjson2.JSONException: create objectReader error, objectType com.aieshanghai.aisurgical.request.PracticalTrainingRecordRequest
at com.alibaba.fastjson2.reader.ObjectReaderCreatorASM.jitObjectReader(ObjectReaderCreatorASM.java:610)
at com.alibaba.fastjson2.reader.ObjectReaderCreatorASM.createObjectReader(ObjectReaderCreatorASM.java:345)
at com.alibaba.fastjson2.reader.ObjectReaderProvider.getObjectReaderInternal(ObjectReaderProvider.java:856)
at com.alibaba.fastjson2.reader.ObjectReaderProvider.getObjectReader(ObjectReaderProvider.java:748)
at com.alibaba.fastjson2.JSONReader.getObjectReader(JSONReader.java:401)
at com.alibaba.fastjson2.reader.FieldReaderObject.getObjectReader(FieldReaderObject.java:70)
at com.alibaba.fastjson2.reader.ORG_78_5_PageQueryDTO.readObject(Unknown Source)
at com.alibaba.fastjson2.JSON.parseObject(JSON.java:1546)
at com.alibaba.fastjson2.support.spring.http.converter.FastJsonHttpMessageConverter.readType(FastJsonHttpMessageConverter.java:111)
Caused by: java.lang.OutOfMemoryError: Metaspace
看到很关键的报错信息:java.lang.OutOfMemoryError: Metaspace,带着问题,问Cursor。
GPT解读
异常发生在FastJSON2为PracticalTrainingRecordRequest生成反序列化器(ObjectReader)失败,Spring把它包装成非法参数/JSON parse error。
FastJSON2默认用ObjectReaderCreatorASM(JIT生成字节码)来加速Bean反序列化。若这一步失败,就会抛出你看到的:
JSONException: create objectReader error, objectType com.aieshanghai.aisurgical.request.PracticalTrainingRecordRequest
发生在ObjectReaderCreatorASM.jitObjectReader里,FastJSON2默认用ASM动态生成并加载很多专用Reader类;生成/加载类时会占用Metaspace。测试环境里Metaspace上限偏小或已经用得差不多了,再JIT新类型(如PracticalTrainingRecordRequest)时就失败,于是先有create objectReader error,本质是Metaspace OOM触发这次解析失败。
所以你遇到的现象很典型:本地JVM Metaspace宽裕→正常;测试Pod/容器限制紧→同样请求反而炸。
总结
| 表象 | 真实原因 |
|---|---|
| createobjectReadererror+PracticalTrainingRecordRequest | FastJSON2ASM在为该类JITReader时需要加载新类 |
| 本地OK、测试失败 | 测试环境Metaspace更小或已耗尽 |
| 栈底OutOfMemoryError:Metaspace | 直接证据 |
处理思路(按优先级)
-
调大测试环境Metaspace(立刻可做),在测试JVM增加类似参数(数值可按监控调整):
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=384m -
关掉FastJSON2的ASM Reader,减轻Metaspace压力。全局改用反射实现,不再为每种Bean JIT一堆类:
-Dfastjson2.creator=reflect,或在classpath下放fastjson2.properties:fastjson2.creator=reflect。这样会少走ASM动态类路径,有利于Metaspace,解析会稍慢一点,一般Web场景可接受。
本地复现
本地基于IDEA开发,增加JVM参数:
java
-XX:MetaspaceSize=32m
-XX:MaxMetaspaceSize=64m
显然是低估项目的代码量,应用直接启动失败:

其实到这一步,基本上就算是已经在本地复现问题;毕竟报错日志都是java.lang.OutOfMemoryError: Metaspace。
但是我偏要犟,认死理,程序员就要有追根究底的倔劲儿:我就想看到Postman请求接口时,PracticalTrainingRecordRequest发生上述报错。
于是调大上述参数,应用启动正常,但是没法复现问题;然后就花费不少时间,调整如上参数。限于时间有限,同时尝试下面章节里的修复方案。
与此同时,应用保持启动状态;由于项目里有诸多定时任务、线程池任务啥的,不过半小时,看到其他类报错信息同上:

符合GPT给出的分析:
本地IDEA冷启动、随便点几下接口,已加载的Class数量往往远少于线上长期跑的实例;Metaspace占用没到临界,同一个接口就不会走到JIT Reader→再加载一批动态类→Metaspace OOM那条路径。测试环境是先运行很久、加载过很多类,再在临界点解析某个Body,才炸。
触发条件往往是「累计」,不是「单次请求一模一样」。线上/本地常见差别包括:
- 进程生命周期:几天不关 vs 本地几分钟会话。
- 请求量与并发:多线程同时踩FastJSON2 ASM,更容易在短时间内推高Metaspace。
- 已加载类型数量:更多Controller、DTO、Feign、动态代理,先把Metaspace吃掉大半。
- 可能的泄漏:ClassLoader、Hot Reload、某些组件反复生成代理类,只有长跑才显现。
所以:同样的JSON、同样JVM上限,「内存水位」可以完全不同。
直接修复
GPT提到JVM参数,那就检查一下Test环境的参数,还真被我找到。test环境docker-compose.yml文件:

修改日期是20天前,内容片段:

此外,相同接口,Dev和Uat环境没有问题,作为对比,查看Uat环境的JVM参数:

看到问题了吧:
- 不仅仅是上面提到的
docker命令查看日志这种垃圾基础设施,没有上云,没有搭建K8s,没有部署Helm; - 各个应用各个环境,最关键的配置文件,没有纳入版本控制系统(VCS),没有统一维护,修改者难以追溯,发生问题排查浪费时间。
既然知道参数有误,调高参数,之前通过cURL请求时稳定复现的问题确实不再复现,理论上确实可以证明问题已修复。个人观点:不能直接证明,所以前面才花费不少时间纠结本地复现。
yaml
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=384m