解决Long精度丢失问题

前言

记录一下之前在公司遇到的一个问题,后端ID使用了Long类型的雪花算法,ID在返回给前端发现显示的 ID 和 数据库中的 ID 不一致,例如数据库中存储的 ID 是 112232531915911412,显示出来却是 112232531915911400,后面2位变成了0,精度丢失了

原因

这是因为JavaScript中的数字的精度是有限的, Java 的 Long 类型的数字超过 JavaScript 的处理范围。 在JavaScript 中只有一种数字类型 Number,所有数字都是采用IEEE 754 标准定义的双精度64位格式存储,即使整数也是如此。也就是说在JavaScript的底层根本没有整数,所有的数字都是小数(64位浮点数)。结构如下:

s
1
e
11
f
52

各位的含义如下:

  • 1位(s) 用来表示符号位,0表示正数,1表示负数
  • 11位(e) 用来表示指数部分
  • 52位(f) 表示小数部分(即有效数字)

双精度浮点数(double)并不是能够精确表示范围内的所有数, 虽然双精度浮点型的范围看上去很大。可以表示的最大整数可以很大,但能够精确表示、使用算数运算的并没有这么大。因为小数部分最大是 52 位,因此 JavaScript 中能精准表示的最大整数是 ,十进制为 9007199254740991

所以只要Java传给JavaScript的Long类型的值超过9007199254740991,就有可能产生精度丢失,从而导致数据和逻辑出错。

解决方法

配置参数

在 SpringBoot 中默认使用的是 Jackson 序列化, 而 Jackson 有个配置参数WRITE_NUMBERS_AS_STRINGS,可以强制将所有数字全部转成字符串输出。其功能介绍为:Feature that forces all Java numbers to be written as JSON strings.。使用方法很简单,只需要配置参数即可:

spring:
  jackson:
    generator:
      write_numbers_as_strings: true

注解

另一个方式是使用注解@JsonSerialize。使用官方提供的Serializer

@JsonSerialize(using=ToStringSerializer.class)
private Long id;

指定了ToStringSerializer进行序列化,将数字编码成字符串格式。这种方式的优点是颗粒度可以很精细;缺点同样是太精细,如果需要调整的字段比较多会比较麻烦

使用JsonComponent序列化配置

/**
 * Spring MVC Json 配置
 */
@Slf4j
@JsonComponent
public class JsonConfig {

    /**
     * 添加 Long 转换 json 精度丢失的配置
     *
     * @param builder 构造器
     * @return ObjectMapper
     */
    @Bean
    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
        log.info("jacksonObjectMapper 进行Long转换成String");
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        SimpleModule simpleModule = new SimpleModule();
        //Long类型转String类型
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        //long类型转String类型
        simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
        objectMapper.registerModule(simpleModule);
        return objectMapper;
    }
}

扩展

配置了全局配置,会把所有的 Long 类型都改成 String类型,如果想要有些字段不想 Long转换成 String 类型,有以下两种方法

  1. 使用 @JsonSerialize 注解,在需要保持数字的字段上添加注解,单独指定序列化方式:
public class PageQuery {
    // 普通 Long 字段自动转 String(解决精度丢失)
    private Long id;
    
    // 特殊字段保持数字类型
    @JsonSerialize(using = JsonSerializer.None.class)
    // 禁用全局配置
    private Long total;
    
    @JsonSerialize(using = JsonSerializer.None.class)
    // Integer 不受影响,但显式声明更清晰
    private Integer size;
    
    @JsonSerialize(using = JsonSerializer.None.class)
    private Integer page;
}
  1. 自定义序列化器(按字段名过滤)
public class SmartLongSerializer extends JsonSerializer<Long> {
    private static final Set<String> NUMERIC_FIELDS = Set.of("total", "size", "page");

    @Override
    public void serialize(Long value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        // 检查字段名:如果是特殊字段则输出数字,否则转字符串
        String fieldName = gen.getOutputContext().getCurrentName();
        if (fieldName != null && NUMERIC_FIELDS.contains(fieldName)) {
            gen.writeNumber(value);
        } else {
            gen.writeString(value.toString());
        }
    }
}
posted @ 2025-08-01 23:20  ゐ叶う枫ゆ  阅读(82)  评论(0)    收藏  举报