警惕!Long 类型 ID 精度丢失——从一次诡异的 “00” 尾巴 Bug

3 阅读4分钟

本文由 本人CSDN转码, 原文地址 警惕!Long类型ID精度丢失——从一次诡异的“00”尾巴Bug说起(long类型属性前端展示最后几位都变成0)_long类型精度丢失-CSDN博客

一、问题现场:消失的数字

在一次再普通不过的消息发送功能中,我们遇到了一个bug:

Map<String, Object> Map = new HashMap<>();
Map.put("ecId", ecOrder.getId());        // 源值:123456789012345678
Map.put("doctorId", ecOrder.getDoctorId());  // 源值:987654321098765432
 
// 经过JSON序列化传输后,另一端收到的值变成了:
ecId: 123456789012345000
doctorId: 987654321098765000

数字的末尾几位莫名其妙地变成了 "00",像是被什么东西 "咬掉" 了一样。

二、真相大白:JavaScript 的数字之殇

问题的根源不在 Java,而在 JavaScript

1. JavaScript 的数字精度限制

JavaScript 遵循 IEEE 754 标准,所有数字都以 64 位双精度浮点数存储。这意味着:

  • 安全整数范围-2^53 + 1 到 2^53 - 1(即-90071992547409919007199254740991

  • 超过这个范围的数字将无法精确表示,会导致精度丢失

2. Java Long 类型 vs JavaScript Number

类型范围精度
Java Long-2^63 到 2^63-1精确表示 19 位数字
JavaScript Number±2^53-1精确表示 16 位数字

当 19 位的 Long 类型 ID(如123456789012345678)传到前端时,JavaScript 无法精确表示,于是末几位就变成了 "000"。

三、问题重现:一个简单的测试

public class LongPrecisionTest {
    public static void main(String[] args) {
        Long bigId = 123456789012345678L;
        String json = JSON.toJSONString(Collections.singletonMap("id", bigId));
        System.out.println("序列化前: " + bigId);
        System.out.println("JSON字符串: " + json);
        
        // 模拟前端JavaScript处理
        // 在JS中: JSON.parse('{"id":123456789012345678}') 
        // 会得到: {id: 123456789012345000}
    }
}

四、解决方案:四重防护策略

方案 1:字符串化传输(推荐)

后端处理:

// 在序列化前将Long转为String
Map<String, Object> data = new HashMap<>();
data.put("id", String.valueOf(123456789012345678L));
data.put("name", "会诊订单");
String json = JSON.toJSONString(data);

前端处理:

// 收到数据后,如果需要数值运算,使用BigInt
const data = JSON.parse(response);
const id = BigInt(data.id); // 使用BigInt处理大整数

**方案 2:**使用 @JsonFormat 注解

可以使用@JsonFormat(shape = JsonFormat.Shape.STRING)将字段转换为 String 类型

@Data
@AllArgsConstructor
public class Student {
 
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private long id;
    private String name;
}

方案 3:配置 JSON 序列化器

使用 Jackson:

@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        // 将Long类型序列化为字符串
        SimpleModule module = new SimpleModule();
        module.addSerializer(Long.class, ToStringSerializer.instance);
        module.addSerializer(Long.TYPE, ToStringSerializer.instance);
        objectMapper.registerModule(module);
        return objectMapper;
    }
}

使用 FastJSON:

SerializeConfig config = new SerializeConfig();
config.put(Long.class, ToStringSerializer.instance);
config.put(Long.TYPE, ToStringSerializer.instance);
 
String json = JSON.toJSONString(data, config);

方案 4:自定义序列化方案

public class LongToStringSerializer extends JsonSerializer<Long> {
    @Override
    public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) {
        gen.writeString(String.valueOf(value));
    }
}
 
// 在实体类中使用
public class Order {
    @JsonSerialize(using = LongToStringSerializer.class)
    private Long id;
    // other fields
}

方案 5:统一响应体封装

@Data
public class ApiResponse<T> {
    private boolean success;
    private String message;
    private T data;
    
    // 自动处理Long类型转String
    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setSuccess(true);
        response.setData(processLongTypes(data));
        return response;
    }
    
    private static Object processLongTypes(Object data) {
        if (data instanceof Long) {
            return String.valueOf(data);
        }
        if (data instanceof Map) {
            // 遍历Map处理Long类型
        }
        if (data instanceof Collection) {
            // 遍历集合处理Long类型
        }
        return data;
    }
}

五、实战修复:我们的代码优化

修改前:

Map<String, Object> Map = new HashMap<>();
Map.put("ecId", ecOrder.getId()); // 可能精度丢失

修改后:

Map<String, Object> Map= new HashMap<>();
// 所有Long类型字段都转为String
Map.put("ecId", String.valueOf(ecOrder.getId()));
Map.put("doctorId", String.valueOf(ecOrder.getDoctorId()));
Map.put("toDoctorId", String.valueOf(ecOrder.getToDoctorId()));
// ...其他字段

六、预防措施:建立开发规范

  1. API 设计规范:所有 ID 字段在接口中均以 String 类型传输

  2. 代码审查:重点检查 Long 类型在前后端交互处的处理

  3. 测试用例:添加大整数精度测试用例

  4. 文档注释:在相关代码处添加注释说明精度问题

/**
 * 注意:Long类型ID直接JSON序列化到前端可能会导致精度丢失
 * 必须转换为String类型传输
 */
public static final String convertLongToString(Long value) {
    return value != null ? String.valueOf(value) : null;
}

七、总结

这次 "00 尾巴"Bug 给我们上了深刻的一课:在分布式系统和前后端分离架构中,数据类型的一致性至关重要

  • 根本原因:JavaScript 的数字精度限制与 Java Long 类型的范围不匹配

  • 解决方案:字符串化传输 + 序列化配置

  • 核心建议:在系统设计初期就考虑数据精度问题,建立统一的数据传输规范

记住这个数字:2^53 = 9007199254740992。超过这个值的整数,在前端都会面临精度丢失的风险!