Spring Boot 将 JSON 中的 Long 值序列化为 String 避免精度丢失

什么是精度丢失?

Java 中长整形 Long (64位)的取值范围是:-9223372036854775808 - 9223372036854775807

在这种情况下,由于 JavaScript 的 Number 类型是 64 位浮点数,它无法精确表示超过 53 位的整数。因此,当将 Java Long 类型的值传递给 JavaScript 时,可能会发生精度丢失。

你可以在浏览器控制台运行如下代码,更直观地感受 “精度丢失” 的问题。

let val = 9223372036854775807;
console.log(val); //9223372036854776000 输出的值丢失了精度

如果我们在业务中使用 Long 作为数据类型,那么就必须要考虑浏览器客户端中 Js 存在精度丢失的问题。解决这个问题最简单的办法就是 把 Java 的 Long 值,序列化为字符串传递

Jackson 的注解支持

Spring Boot 默认使用 Jackson 作为 JSON 的序列化、反序列化框架。Jackson 提供了 @JsonSerialize 注解,该注解的 using 属性可以指定一个 JsonSerializer 的实现类,用于自定义字段的序列化方式。

Jackson 已经预定义了一个实现 ToStringSerializer,用于把指定的字段序列化为字符串。

定义一个简单的 User 对象:

package cn.springdoc.demo.model;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

public class User {

    // 把 Long 类型的 id 序列化为 字符串
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;

    private String name;

    // 忽略 get / set 方法
}

定义一个 Controller 进行测试:

package cn.springdoc.demo.web.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import cn.springdoc.demo.model.User;


@RestController
@RequestMapping("/demo")
public class DemoController {

    @PostMapping
    public User demo (@RequestBody User user) {
        return user; 
    }
}

如上,定义一个简单的端点,接收客户端 POST 的 JSON 字符串,封装为 User 对象后原样返回。

启动应用,使用 curl 进行测试:

$ curl -X POST -H "Content-Type: application/json" -d '{"id": 9223372036854775807, "name": "springdoc.cn"}' http://localhost:8080/demo

{"id":"9223372036854775807","name":"springdoc.cn"}

如你所见,请求体中的 id 字段是一个数值类型,其值为 9223372036854775807,也就是 Long 的最大值。Jackson 准确地把请求体封装为了 User 对象。

并且在响应 JSON 中,id 字段类型是 String,说明注解生效!

id 字段如果以字符串形式传递,Jackson 也能自动解析为 Long 类型:

$ curl -X POST -H "Content-Type: application/json" -d '{"id": "9223372036854775807", "name": "springdoc.cn"}' http://localhost:8080/demo

{"id":"9223372036854775807","name":"springdoc.cn"}

全局设置

@JsonSerialize 注解的弊端在于,需要对所有 Long 类型的字段进行一一设置,这太麻烦。

通过 Spring Boot 的 Jackson2ObjectMapperBuilderCustomizer 配置类,可以对 Jackson 进自定义,从而指定所有 Long 类型的序列化方式为 String

package cn.springdoc.demo.configuration;

import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

@Configuration
public class JacksonConfiguration {

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
        return builder -> {
            // 把 Long 类型序列化为 String
            builder.serializerByType(Long.class, ToStringSerializer.instance);
        };
    }
}

删除 User 对象中的注解:

package cn.springdoc.demo.model;

public class User {

    private Long id;
    private String name;
    // 省略 get / set 方法
}

重启应用,再次使用 curl 进行测试:

$ curl -X POST -H "Content-Type: application/json" -d '{"id": "9223372036854775807", "name": "springdoc.cn"}' http://localhost:8080/demo

{"id":"9223372036854775807","name":"springdoc.cn"}

全局设置成功。系统中所有的 Long 类型,都会被序列化为 String