包阅导读总结
1. `FastJson`、`JDK 11`、`序列化问题`、`业务异常`、`问题排查`
2. 本文主要记录了作者在升级到 JDK 11 后遇到的 FastJSON 序列化问题及排查过程,包括类加载顺序变动、多端差异等,最终通过排除相关 jar 解决问题。
3.
– 升级到 JDK 11 后,FastJSON 序列化出现问题,类加载顺序变动导致同名类加载不一致。
– 出现批量买家反馈订单页面无法发起纠纷报错。
– 排查过程中怀疑是某些代码行或端口设置导致。
– 问题复现存在困难,PC 端无问题,仅 APP 端出现。
– 最初怀疑前端发布问题,后排除。
– 对调用链路逐层打日志排查。
– 最终发现是某个类的问题,排除相关 jar 后解决。
– 排查过程中还对各种可能因素进行了分析和排除。
思维导图:
文章地址:https://mp.weixin.qq.com/s/t3nq03MxL0HWzQFx62P9Cg
文章来源:mp.weixin.qq.com
作者:河影
发布时间:2024/9/8 5:55
语言:中文
总字数:4901字
预计阅读时间:20分钟
评分:88分
标签:FastJSON,JDK 11,序列化问题,类加载顺序,排查过程
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
本文记录作者升级到 JDK 11 后遇到的 FastJSON 序列化问题,以及详细的排查过程。
升级到JDK 11后,类加载顺序有所改动,同名的类在多个jar中,导致实际加载的类不一样,因此序列化的结果不一样。
@客户服务(CCO) 出现批量买家反馈,现在订单页面无法发起纠纷,申请后就报错。辛苦帮忙看下。
问题现象:
排查过程:
new SpringApplicationBuilder(DestinyApplication.class)
.web(WebApplicationType.NONE)
.profiles(DiamondProfiles.load())
.run(args);
-
就这一行代码,看看是不是给 .web(WebApplicationType.NONE) 端口干坏了。
@Controller
public static class OkController {
@ResponseBody
@RequestMapping("/ok.jsp")
public String ok() {
return "success";
}
@GetMapping("/checkpreload.htm")
public @ResponseBody String checkPreload() {
return "success";
}
}
icbusession.authorization.exclude-paths=/favicon.ico,/checkpreload.htm,/status.taobao,/ok.jsp
问题复现
-
因为回滚的太快,测试同学在进行复现的时候,大部分都掉进上面这个优雅上下线的问题去了。
-
抽丝剥茧后,最终还原了事件的真相。
-
PC端没有问题,仅仅在APP端才会出现这个问题。因为在售后域,PC和APP的差别基本上就一层Mtop的hsf接口的问题,底层调用的HSF服务基本都是统一的,所以我们开发同学在自测的过程基本上都是回归下PC看看没问题了,大概率就认为没问题。–这是犯了经验主义错误,后续还是要认认真真下一个beta包,安卓和IOS都走一遍在发布。
-
BETA没有发现的问题,是因为这个问题实际没有产生任何Java的异常,报错的是一个业务异常的校验。
-
{“memo”:”min refund limit”,”resultCode”:{“code”:-27,”message”:”min refund limit”,”success”:false},”success”:false}
-
我们原本认定这种是业务校验异常,无需关心。–后续对业务异常也进行监控,如果有大批量上涨,也需要引起重视。
-
好了,不卖关子了,开始分析这个问题并复现。
-
问题推导,首先多端问题先怀疑前端发布,不过最近没发布,后端回滚后止血,那就应该不是前端的问题。售后的提交的数据的报错。
-
测试在预发复现后,看到对应日志。

{"oldPostIssueRequestDTO":{"authorizedViewCommunication":false,"businessType":"XXX","buyerInfo":{"email":"cxw20180809@126.com"},"destinyTraceId":"XXX-84ba-68ad2b62690c","device":"IOS","issueReasonId":1022,"memo":"qqq","operation":"abortOrder","operatorAccountId":XXX,"operatorAliMemberId":CCC,"operatorType":"buyer","orderId":"XXX","payerList":[{"availableTaxAmount":{"amount":0.00,"cent":0,"centFactor":100,"currency":"USD","currencyCode":"USD"},"cardTailNo":"5164","currency":"USD","finNumber":["XXX"],"forexRefundAmount":{"amount":0.30,"cent":30,"centFactor":100,"currency":"USD","currencyCode":"USD"},"fundAmount"{"amount":0.00,"cent":0,"centFactor":100,"currency":"USD","currencyCode":"USD"},"online":true,"originPayMethod":"CREDIT_CARD_PAY","payContractId":"CCC","payGmtCreate":1724210447000,"payGmtCreateStr":"2024-08-20 20:20","payMethod":"CREDIT_CARD_PAY","payProcessFeeAmount":{"amount":0.01,"cent":1,"centFactor":100,"currency":"USD","currencyCode":"USD"},"payStep":"ADVANCE","payerName":"null null","rate":1,"refundAmount":{"amount":0.30,"cent":30,"centFactor":100,"currency":"USD","currencyCode":"USD"},"taxAmount":{"amount":0.00,"cent":0,"centFactor":100,"currency":"USD","currencyCode":"USD"},"termFundArrived":false}],"refundAmount":{"amount":0.00,"cent":0,"centFactor":100,"currency":"USD","currencyCode":"USD"}}}
-
我们开发的一个好习惯,就是在MTOP请求过来的时候,先打印一行日志,我们MTOP接口入参是一个String,这里面明明是带着 amount的啊。
-
这里面的amount的值是在哪里被抹平的呢?带着这个疑惑,我把每次调用的链路的地方都打了一行日志。
-
从MtopTradeIssueViewService的时候还是带amount值的,在IssueApplicationService的时候就没有了。
-
看着这长长的调用链路,我又陷入了沉思,这么多层调用着实有点离谱,后续流程我没有继续画了。
-
已知MtopTradeIssueViewService的string是对的,然后IssueApplicationService的不对。
-
我于是在每一个service上面都打了一下入参,看看到底是哪里的问题。
-
当我开始思考是不是区域化路由给我的amount的值给抹平了的时候,结论又给我整不明白了。
-
看日志在第一个调用的时候,值就没有了。TradeIssueViewRegionFacade。
-
然后我继续在第一个类里面增加日志,看看具体是哪里的问题。结果再一次给我震惊了。
fun parsePostIssueRequestDTO(request: String): PostIssueRequestDTO? {
var postIssueRequestDTO: PostIssueRequestDTO? = null
try {
postIssueRequestDTO = JSON.parseObject(request, PostIssueRequestDTO::class.java)
} catch (e: Throwable) {
logger.error("MtopIssueViewService.parsePostIssueRequestDTO parse request error.$request", e)
}
if (postIssueRequestDTO == null) {
logger.error("MtopIssueViewService.parsePostIssueRequestDTO provideEvidenceForm is null.$request")
}
return postIssueRequestDTO
}
本地复现
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68.noneautotype</version>
</dependency>
-
看了下线上也是这个版本啊,相关引入fastjson的包也没有变化? -
这怎么查???
改动范围review
依赖二方库的改动
-
反序列化的这个类:PostIssueRequestDTO ,是我们代码一方库的类。线上用的是更低一个版本的。
-
我一开始怀疑是这个二方库的问题,我发布的时候顺手勾了下java11,是不是这个锅。
-
等我重新用java8发布了一个snapshot的二方库,好像也没有影响。
-
这时候我们前端提醒我,为什么PC不报错呢。
-
对啊,因为PC在我们后端的前端应用moirai中,这里的parse好像没问题,那我在这个应用里面,升级到我怀疑的二方库版本,发现还是正常。
-
那只能排除掉是这个二方库升级导致的反序列化异常。
JDK11的改动
-
这个怀疑是有点没道理的,因为我们应用大部分已经升级到JDK11了,也没听说遇到这种问题的。
-
但谨慎起见,我保障了和前端应用Moirai(即PC可以正常反序列化的应用)一样的Java的版本。
-
多次尝试后,发现和JDK的版本没啥关系。而且好像也不能在降级回Java8。
仔细review前端传的字符串
Money类分析
public class Money implements Serializable, Comparable {
/**
* Comment for <code>serialVersionUID</code>
*/
private static final long serialVersionUID = 6009335074727417445L;
/**
* 缺省的币种代码,为CNY(人民币)。
*/
public static final String DEFAULT_CURRENCY_CODE = "CNY";
/**
* 缺省的取整模式,为<code>BigDecimal.ROUND_HALF_EVEN
* (四舍五入,当小数为0.5时,则取最近的偶数)。
*/
public static final int DEFAULT_ROUNDING_MODE = BigDecimal.ROUND_HALF_EVEN;
/**
* 一组可能的元/分换算比例。
* <p>
* 此处,“分”是指货币的最小单位,“元”是货币的最常用单位, 不同的币种有不同的元/分换算比例,如人民币是100,而日元为1。
*/
private static final int[] centFactors = new int[] { 1, 10, 100, 1000 };
/**
* 金额,以分为单位。
*/
private long cent;
/**
* 币种。
*/
private Currency currency;
/**
* 币种代码
*/
private String currencyCode;
}
-
然后我们丢失的amount呢,其实这个并不是一个字段,仅有get和set方法。
// Bean方法 ====================================================
/**
* 获取本货币对象代表的金额数。
*
* @return 金额数,以元为单位。
*/
public BigDecimal getAmount() {
return BigDecimal.valueOf(cent, currency.getDefaultFractionDigits());
}
/**
* 设置本货币对象代表的金额数。
*
* @param amount 金额数,以元为单位。
*/
public void setAmount(BigDecimal amount) {
if (amount != null) {
cent = rounding(amount.movePointRight(2), BigDecimal.ROUND_HALF_EVEN);
}
}
FastJson分析
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.52</version>
</dependency>
FastJson代码探究
-
那没办法了,只能要么让前端加一下cent,要么debug下FastJson。
-
来吧,逃也逃不过去,具体的源码精度我后面放在ParseObject的文章里面。这里记录下关键的几个结论和发现问题的点。
-
首先,我本地是可以反序列化money的类的,aone的机器反序列化money的类,amount值会被抹平。
-
然后慢慢对比这两处,哪里是不一致的,然后一点点排查。
-
aone的机器就是我们部署在服务器的机器,即和正式环境的基本一致。
首先怀疑是ASM的问题
-
一个是ASM的一个是JavaBean的。和高铁老师沟通后,建议关掉我本地的ASM在试下,然后看看能不能在Fastjson2上面复现。
-
搞到现在高铁老师都开始怀疑是不是这个类Java11没兼容好。-_-||
-
fastjson2可以通过参数把asm关了,比如加上JVM启动参数 -Dfastjson2.creator=reflect。
然后怀疑ClassLoader的问题
BeanInfo问题
-
本来以为是这个问题,本地也换成javaBean之后,发现好像没啥变化,本地还是可以反序列化amount,aone机器还是不能反序列化amount。
-
然后在继续跟JavaBeanInfo的时候发现了端倪,我本机是有三个字段的,但是aone机器上只有两个。
-
给个口子,有兴趣的同学可以看一眼。
com.alibaba.fastjson.util.JavaBeanInfo#build(java.lang.Class<?>, java.lang.reflect.Type, com.alibaba.fastjson.PropertyNamingStrategy, boolean, boolean, boolean)
终见端倪
真相大白
-
真的有这个类,然后线上机器是没有的。
-
把这个jar排除掉,问题解决。
PAI Stable Diffusion WebUI 解决方案为企业提供云上快速部署定制化的文生图应用。提供了方便、高效的模型部署产品,并支持根据实际需求,配置不同的服务版本及服务参数。具有分钟级部署上线,方便快捷、开箱即用,多版本部署方案,参数可定制化调整的优势。
点击阅读原文查看详情。