Posted in

我的字段被 FastJson 给干掉了?!_AI阅读总结 — 包阅AI

包阅导读总结

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 解决方案为企业提供云上快速部署定制化的文生图应用。提供了方便、高效的模型部署产品,并支持根据实际需求,配置不同的服务版本及服务参数。具有分钟级部署上线,方便快捷、开箱即用,多版本部署方案,参数可定制化调整的优势。

点击阅读原文查看详情。