分类:后端开发 · 作者:管理员 · 发布于 2026-10-24 · 阅读 0 · 标签:Jackson / JSON序列化 / Spring Boot / LocalDateTime / 精度丢失

一、起因:一个看似"玄学"的订单号

某天联调时前端同事截图过来:订单详情返回的 orderId 明明是 18056123456789012345,小程序里打印出来却是 18056123456789012000。查数据库、查日志都对,唯独响应不对。最后定位到根因:雪花 ID 是 Long 类型,经 Jackson 默认序列化成 JSON 数字后,超过了 JavaScript 的 Number.MAX_SAFE_INTEGER(2^53 - 1,约 9007199254740991),精度被静默截断。这正是 Spring Boot 项目里最高频的序列化坑之一。

本文把 Long 精度、LocalDateTime 格式、时区偏移、循环引用、BigDecimal 这五个实战高频坑一次讲透,并给出可落地的全局配置方案。

二、坑 1:Long 类型精度丢失

分布式 ID(雪花、号段)普遍是 64 位 Long,序列化为 JSON 数字后,Java 与 JavaScript 的数值精度差异直接导致末位失真。修复思路:传给前端的 Long 统一序列化为字符串。

// 方案一:字段级,只对 ID 生效
public class OrderVO {
    @JsonSerialize(using = ToStringSerializer.class)
    private Long orderId; // 输出为 "18056123456789012345"
    // 其余字段略
}

// 方案二:全局级,所有 Long 都转字符串(推荐)
@Configuration
public class JacksonConfig {
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer longToStringCustomizer() {
        return builder -> builder
            .serializerByType(Long.class, ToStringSerializer.instance);
    }
}

⚠️ 注意:全局转字符串会影响所有 Long 字段(如统计数值),若部分字段需要保留数字含义,改用字段级注解更精准。线上建议「默认全局转字符串 + 个别字段 @JsonSerialize 覆盖」。

三、坑 2:LocalDateTime 序列化成数组 [2026,10,24,14,30]

Spring Boot 默认注册了 JavaTimeModule,但默认策略下 LocalDateTime 会输出成 [2026,10,24,14,30,5] 这样的数组,前端拿到的不是字符串,直接导致时间渲染失败。需要在配置里关闭时间戳模式:

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai
    serialization:
      write-dates-as-timestamps: false

加上 write-dates-as-timestamps: false 后,LocalDateTime 输出为 "2026-10-24T14:30:05"。若想输出带空格的格式,配合 date-format 与 @JsonFormat 双保险:

public class OrderVO {
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
}

四、坑 3:时区偏移八小时

服务端部署在 UTC 时区、数据库存 UTC、前端在 GMT+8 时,经常出现"晚上八点提交的订单,接口返回凌晨四点"。根因是 Instant/Date 序列化时未统一时区。修法三件套:

  • JVM 启动参数加 -Duser.timezone=Asia/Shanghai
  • Spring 配置 spring.jackson.time-zone: Asia/Shanghai
  • 数据库连接串统一时区:MySQL 加 serverTimezone=Asia/Shanghai,PostgreSQL 用 TimeZone=Asia/Shanghai

⚠️ 真正的坑在于:LocalDateTime 不带时区,偏移问题通常来自 Date/Instant 字段。排查时先分清字段类型,别把 LocalDateTime 的锅甩给时区。

五、坑 4:双向关联导致 StackOverflowError

实体里 Order 持有 User、User 又持有 List<Order>,Jackson 递归序列化到栈溢出,接口直接 500。三种解法按推荐度排序:

  1. VO/DTO 隔离(最佳):响应对象只放需要的字段,彻底避免实体循环引用;
  2. @JsonIgnoreProperties:在反向字段上声明忽略,保留单向序列化;
  3. @JsonIgnore:直接忽略某个字段,简单但不够精细。
public class User {
    private Long id;
    private String nickname;

    @JsonIgnoreProperties("user") // 序列化 orders 时不再展开 user
    private List<Order> orders;
}

还有一个隐藏坑:Redis 缓存实体时也会触发同样的递归(Jackson 序列化到 Redis),所以实体设计上尽量避免双向强引用,必要时用 @JsonIgnore 兜底。

六、坑 5:BigDecimal 变科学计数法 / Double 精度丢失

金额字段用 Double 接收 JSON,0.1 + 0.2 ≠ 0.3 是经典事故。正确姿势:金额一律 BigDecimal,且 setScale 固定小数位。另外大数 BigDecimal(如 1E+10)默认可能输出科学计数法,前端解析易错:

spring:
  jackson:
    generator:
      write-bigdecimal-as-plain: true  # 大数输出普通数字而非科学计数法
// Java 侧:金额计算务必带 scale
BigDecimal amount = new BigDecimal("19.90");
BigDecimal discounted = amount.multiply(new BigDecimal("0.88"))
        .setScale(2, RoundingMode.HALF_UP);
// 输出 17.51,而不是 17.512

七、收口:一份全局配置解决 80% 序列化问题

与其在每个 VO 上加注解,不如在配置层统一收口。推荐用 Jackson2ObjectMapperBuilderCustomizer(Spring Boot 官方推荐方式,不会覆盖自动配置):

@Configuration
public class JacksonConfig {

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer globalJacksonCustomizer() {
        return builder -> builder
            // Long 转字符串防精度丢失
            .serializerByType(Long.class, ToStringSerializer.instance)
            // Date 统一格式
            .simpleDateFormat("yyyy-MM-dd HH:mm:ss")
            .timeZone(TimeZone.getTimeZone("Asia/Shanghai"))
            // 忽略 null 字段,减小响应体积
            .serializationInclusion(JsonInclude.Include.NON_NULL);
    }
}
spring:
  jackson:
    serialization:
      write-dates-as-timestamps: false
    generator:
      write-bigdecimal-as-plain: true

八、踩坑记录与最佳实践清单

  • 别直接 new ObjectMapper():手动 new 的 Mapper 不会带 Spring Boot 的自动配置,必须用注入的 ObjectMapper 或经过 Customizer;
  • @JsonFormat 对 LocalDateTime 有效,对 Java8 时间需配合 jackson-datatype-jsr310(Spring Boot 2.x 默认已带,3.x 也内置);
  • 给前端的所有 ID 类字段,统一走「Long→String」策略,前端联调少一半问题;
  • 实体类与 VO 分离:实体做 ORM 映射,VO 控制对外字段与格式,序列化问题在 VO 层一次解决;
  • 上线前用 curl 抓一次真实响应,肉眼核对 ID、时间、金额三类字段,比单元测试更快发现格式化问题。

序列化问题看着小,一旦线上爆雷往往是 P0:订单号对不上、时间全错、500 刷屏。把这五个坑的配置沉淀成一个 web-common 起步依赖,新服务开箱即用,是性价比最高的做法。