摘要正在生成中···
爱谦のAI摘要
qwen-turbo

为什么会出现前端精度丢失?

当我们后端 (Java 为例) 使用 Long/long 类型输出数据到前端,且保存的数据是超过 16 位的数字,那么前端就会出现精度丢失现象。

通俗易懂的说就是前端 JavaScript 的数字类型是 Number,其支持的最大范围就是 16 位数字;而如果我们后端使用雪花算法生成 ID 的话则是 19 位,前端的精度没有后端的大,后端传递到前端时就会出现精度丢失现象。

比如:后端传递的 ID 是 xxxxxxxxxxxxxxx5678 ,而前端接收到的却是 xxxxxxxxxxxxxxx5700 ,这就是所谓的精度丢失现象。

解决方案

对于精度问题造成的数据丢失现象,我们的最佳思路就是 将后端传递的 Long/long 类型的数据以字符串的形式进行传递,这样就能够有效地避免精度丢失问题。

主要有两种解决方案:局部解决、自定义序列化器全局解决。

以 Spring Boot 项目 + MyBatis-Plus 持久层框架为例。

局部解决

在对应的主键 ID 字段上添加 @JsonSerialize(using = ToStringSerializer.class) 注解即可,这样就能把 Long 类型的 ID 转化为 String。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 用户表
*
* @TableName user
*/
@TableName(value = "user")
@Data
public class User implements Serializable {
/**
* id
*/
@TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;

// ...
}

不过该方案存在的问题就是:如果项目中有成百上千个类都需要传递 ID 字段,那我们就需要一个一个的去添加注解,非常的不方便,因此大型项目并不推荐此种方式。

全局解决

全局解决又分为两种方式。

⚠️ 注意:这样做之后,前端接收到的所有 Long/long 类型字段都是字符串,前端需要按照字符串处理,不能直接当作数字进行运算。

第一种方式:使用 ObjectMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Spring MVC Json 配置
*/
@JsonComponent
public class JsonConfig {

@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {

// 使用构建器创建一个基本的 ObjectMapper 实例, 不启用 XML 映射功能
ObjectMapper objectMapper = builder.createXmlMapper(false).build();

// 创建一个简单的 Jackson 模块, 用于添加自定义的序列化器
SimpleModule module = new SimpleModule();

// 为 Long 类型注册 ToStringSerializer, 这样在序列化时会直接将 Long 值转为字符串形式, 避免精度丢失
module.addSerializer(Long.class, ToStringSerializer.instance);

// 为 long 基本类型(Long.TYPE)注册同样的序列化器
module.addSerializer(Long.TYPE, ToStringSerializer.instance);

// 将配置好的模块注册到 ObjectMapper 中
objectMapper.registerModule(module);

// 返回配置完成的 ObjectMapper 实例
return objectMapper;
}
}

第二种方式:使用 Jackson2ObjectMapperBuilder。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 避免前端精度丢失配置
*/
@Configuration
public class JacksonConfiguration {

/**
* 配置精度丢失
*
* @return Jackson2ObjectMapperBuilderCustomizer
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
builder.serializerByType(Long.class, ToStringSerializer.instance) // Long --> String
.serializerByType(Long.TYPE, ToStringSerializer.instance); // long --> String
};
}
}

总结

方式二使用 Jackson2ObjectMapperBuilderCustomizer 提供的链式 API,较为简单、高层抽象,编码更简洁,适用于常规配置,且与 Spring Boot 高度集成。

方式一是手动创建 ObjectMapper,通过 registerModule() 等 API 或直接设置属性,更灵活也更底层。适用于复杂定制 (如多个自定义序列化器、混合配置等),但是编码也相对要复杂一些。

拓展

我们也可以通过上述的方式自定义统一的时间类型进行返回。

通常情况下我们是这样格式化时间的:

1
2
3
4
5
6
7
8
9
10
11
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;

/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;

这往往需要我们对每个进行传递的时间执行格式化操作,而现在我们可以全局配置时间的格式了:

1
2
3
4
5
6
7
8
9
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
builder.serializerByType(Long.class, ToStringSerializer.instance) // Long --> String
.serializerByType(Long.TYPE, ToStringSerializer.instance) // long --> String
.timeZone(TimeZone.getTimeZone("Asia/Shanghai"))
.dateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
};
}