发布过程中,消防群客户反馈出现批量买家反馈订单页面无法发起纠纷,报错显示为FastJSON解析问题。
首先进行回滚操作,然后在预发环境复现问题,通过排查发现FastJSON在反序列化时出现问题,最后通过升级FastJson版本和排除某个二方库解决问题。
作者通过此次问题排查,积累了经验,并通过与高铁老师的交流,对FastJson有了更深入的了解。同时,通过排除二方库解决了问题,也提醒了开发者在依赖管理和版本控制方面需要更加谨慎。
本文记录作者升级到 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":"[email protected]"},"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
}
本地复现
com.alibaba
fastjson
1.2.68.noneautotype
-
看了下线上也是这个版本啊,相关引入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 serialVersionUID
*/
private static final long serialVersionUID = 6009335074727417445L;
/**
* 缺省的币种代码,为CNY(人民币)。
*/
public static final String DEFAULT_CURRENCY_CODE = "CNY";
/**
* 缺省的取整模式,为BigDecimal.ROUND_HALF_EVEN
* (四舍五入,当小数为0.5时,则取最近的偶数)。
*/
public static final int DEFAULT_ROUNDING_MODE = BigDecimal.ROUND_HALF_EVEN;
/**
* 一组可能的元/分换算比例。
*
* 此处,“分”是指货币的最小单位,“元”是货币的最常用单位, 不同的币种有不同的元/分换算比例,如人民币是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分析
com.alibaba.fastjson2
fastjson2
2.0.52
FastJson代码探究
-
那没办法了,只能要么让前端加一下cent,要么debug下FastJson。
-
来吧,逃也逃不过去,具体的源码精度我后面放在ParseObject的文章里面。这里记录下关键的几个结论和发现问题的点。
-
首先,我本地是可以反序列化money的类的,aone的机器反序列化money的类,amount值会被抹平。
-
然后慢慢对比这两处,哪里是不一致的,然后一点点排查。
-
aone的机器就是我们部署在服务器的机器,即和正式环境的基本一致。
首先怀疑是ASM的问题