日志打印中断批量付款

83 阅读2分钟

一、前言

背景:线上告警群,收到告警:BigMoney java.lang.ArithmeticException

image.png 业务流程:这块是通用方法,调用中台接口。

核心作用:把“批次单(BatchOrder)在支付中台已冻结的总金额”里,按单笔拆出一部分做“单笔解冻”,为后续该笔付款走“正常单笔付款预下单/支付流程”做资金释放/衔接。

直接通过异常日志,可以知道

  • 序列化 org.joda.money.Money 时 FastJSON 的 ASM 序列化器会调用 getAmountMajorInt(),其内部使用 BigDecimal.intValueExact()
  • 金额部分超过 Integer.MAX_VALUE时就会抛出 ArithmeticException: Overflow image.png

再结合上下文日志发现是在处理 53亿 VND(越南盾)的时候出现了异常:

  • 在 Java 里,Integer.MAX_VALUE 的值是 2147483647(即 2^31 - 1),21亿
  • 53亿 远超了 21亿

小结:

1、org.joda.money.Money内部有定义getAmountMajorInt()方法

2、FastJSON 生成的 ASM 序列化器会按属性列表逐个调用所有公开的 getter(按名称/类型排序),执行到 getAmountMajorInt(),然后整型溢出。

3、这种日志打印万万不能影响支付链路。



二、实验复现

测试代码如下

@Slf4j
public class MockTest extends TestBase {
    public void test() {
        // {"amount":5370000000,"currency":"VND"}
        BigDecimal amount = BigDecimal.valueOf(5370000000L);
        CurrencyUnit currencyUnit = CurrencyUnit.VND;
        Money money = Money.of(currencyUnit, amount);

        log.info("result: "+ JSON.toJSONString(money));
        System.out.println("====> success");
    }
}

运行后就会报错image.png



三、问题解决

解决这个问题可以分为两个方向

1、改org.joda.money.Money,自定义 Money 的序列化 / 忽略问题 getter

@JSONField(serialize = false)
public int getAmountMajorInt() { 
  return getAmountMajor().intValueExact(); 
}

2、改 JSON 输出方式:升级到新版 FastJSON 2.x 并用 @JSONField(serializeUsing=...) 定制 Money 序列化,或切到 Jackson + joda-money module。


🌰举例子如下:以 Jackson 为例

  1. 依赖:
<dependencies>
  <dependency>
    <groupId>org.joda</groupId>
    <artifactId>joda-money</artifactId>
    <version>1.0.5</version>
  </dependency>
  <dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-joda-money</artifactId>
  </dependency>
</dependencies>
  1. 自定义日志打印:
public class LogConverter {
  private static final ObjectMapper objectMapper = newObjectMapper();

  // 配置
  public static ObjectMapper newObjectMapper() {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new JodaMoneyModule());
    
    return objectMapper;
  }

  // 调用这个方法来打日志
  public static String toJsonString(Object original) {
    return objectMapper.writeValueAsString(original);
  }
}