有效避免Long类型数据向前端传递时的精度丢失问题

1. 问题背景

JavaScript的Number类型遵循IEEE 754标准,只能精确表示到2^53 - 1(即9007199254740991)的整数,超过这个范围的整数会丢失精度。因此,后端的Long类型数据在未经处理直接传给前端时,可能会因JavaScript的这一限制而失真。

2. 场景模拟

2.1 创建数据表

DROP TABLE IF EXISTS student;
CREATE TABLE student
(
    id BIGINT NOT NULL COMMENT 'id',
    name VARCHAR(30)  COMMENT '姓名',
    age INT COMMENT '年龄',
    PRIMARY KEY (id)
);
insert into student(id,name,age) values(1, 'zhangsan',23);
insert into student(id,name,age) values(2, 'lisi',45);
insert into student(id,name,age) values(3, 'wangwu',12);
insert into student(id,name,age) values(1805422911868125186, 'zhaoliu',34);

2.2 编写后端接口

1. 实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("student")
public class Student {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer age;
}

2. Controller层

@Slf4j
@RestController
@RequiredArgsConstructor
public class StudentController {
    private final StudentService studentService;

    @GetMapping("/getAllStudents")
    public List<Student> getAllStudents() {
        return studentService.getAllStudents();
    }
}

3. service层

public interface StudentService extends IService<Student> {
    List<Student> getAllStudents();

}
@Service
@RequiredArgsConstructor
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements StudentService {
    private final StudentMapper studentMapper;

    @Override
    public List<Student> getAllStudents() {
        return studentMapper.selectList(null);
    }
}

4. mapper层

@Mapper
public interface StudentMapper extends BaseMapper<Student> {
}

5. 跨域处理

@Configuration
public class CORSConfig {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOriginPatterns("*")
                        .allowedMethods("*")
                        .allowedHeaders("*")
                        .allowCredentials(true);
            }
        };
    }
}

2.3 编写前端代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>学生信息查询</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            margin: 0;
        }

        h1 {
            text-align: center;
            margin-bottom: 20px;
        }

        button {
            padding: 10px 20px;
            font-size: 16px;
            cursor: pointer;
        }

        table {
            width: 100%;
            max-width: 800px;
            border-collapse: collapse;
            margin-top: 20px;
        }

        th,
        td {
            border: 1px solid #ddd;
            padding: 12px;
            text-align: left;
        }

        th {
            background-color: #f2f2f2;
            font-weight: bold;
        }

        tr:nth-child(even) {
            background-color: #f2f2f2;
        }
    </style>
    <script>
        async function refreshData() {
            try {
                const response = await fetch('http://localhost:8080/getAllStudents', {
                    method: 'GET',
                    headers: {
                        'Content-Type': 'application/json',
                    }
                });
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                const data = await response.json();
                populateTable(data); // 假设API直接返回学生数组,而不是data属性下的数组
            } catch (error) {
                console.error('There was a problem with the fetch operation:', error);
            }
        }

        window.onload = refreshData; // 页面加载时自动调用以加载数据

        function populateTable(students) {
            const tbody = document.querySelector('#students tbody');
            tbody.innerHTML = ''; // 清空之前的表格内容
            students.forEach(student => {
                const row = document.createElement('tr');
                row.innerHTML = `                    <td>${student.id || '-'}</td>
                <td>${student.name || '-'}</td>
                <td>${student.age || '-'}</td>
            `;
                tbody.appendChild(row);
            });
        }
    </script>
</head>

<body>
    <h1 style="text-align: center;">学生信息查询</h1>
    <button onclick="refreshData()">刷新</button>
    <table id="students" style="margin-top: 20px;">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th>Age</th>
            </tr>
        </thead>
        <tbody></tbody>
    </table>
</body>

</html>

2.4 访问测试

通过浏览器访问发现,前端接收到的数据为

而实际的数据库数据为

这里,会发现zhaoliu这个学生的id前后端不一致,而后端查询出来的日志却没有异常,该情况为Long类型数据向前端传递时的精度丢失问题

2024-06-25 16:13:41.932  INFO 29552 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@784be587] was not registered for synchronization because synchronization is not active
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1906983a] will not be managed by Spring
==>  Preparing: SELECT id,name,age FROM student
==> Parameters: 
<==    Columns: ID, NAME, AGE
<==        Row: 1, zhangsan, 23
<==        Row: 2, lisi, 45
<==        Row: 3, wangwu, 12
<==        Row: 1805422911868125186, zhaoliu, 8
<==      Total: 4
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@784be587]

3. 解决方案

3.1 局部处理

3.1.1 使用注解解决

在需要处理的属性上添加 @JsonFormat(shape = JsonFormat.Shape.STRING) 或 @JsonSerialize(using = ToStringSerializer.class) 。基于该方案,上述案例可修改为:

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("student")
public class Student {
    @TableId(type = IdType.AUTO)
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    // 或 @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
    private String name;
    private Integer age;
}

3.2 全局处理

3.2.1 创建SimpleModule子类

注意:

1. 要避免其他地方的Jackson配置被覆盖的问题

2. 容易被下面方案覆盖

@Configuration
public class JsonModuleConfig extends SimpleModule {
    public JsonModuleConfig() {
        //super(JsonModuleConfig.class.getName());
        this.addSerializer(Long.class, ToStringSerializer.instance);
        this.addSerializer(Long.TYPE, ToStringSerializer.instance);
    }
}

3.2.2 创建WebMvcConfigurer实现类

注意:

容易被下面方案覆盖

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.math.BigInteger;
import java.util.List;

@EnableWebMvc
@Configuration
public class WebDataConvertConfig implements WebMvcConfigurer {
    /**
     * 配置消息转换器,用于序列化和反序列化HTTP请求和响应的Java对象为JSON格式。
     * 这个方法的目的是定制JSON转换过程,特别是处理Long类型序列化的问题,以避免JavaScript中数值精度丢失的问题。
     *
     * @param converters 一个HttpMessageConverter的列表,用于处理HTTP消息的转换。
     */
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 创建一个MappingJackson2HttpMessageConverter实例,它负责将Java对象转换为JSON格式。
        MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();

        // 创建一个ObjectMapper实例,用于配置JSON的序列化和反序列化。
        ObjectMapper objectMapper = new ObjectMapper();

        // 创建一个SimpleModule实例,用于注册自定义的序列化器。
        SimpleModule simpleModule = new SimpleModule();

        // 注册自定义的序列化器,将Long、Long类型和BigInteger类型序列化为字符串,以避免精度丢失问题。
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
        simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);

        // 配置ObjectMapper,使其在反序列化时忽略未知的属性,而不是抛出异常。
        // 反序列化时忽略多余字段
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        // 注册刚刚配置的SimpleModule到ObjectMapper中。
        // 注册
        objectMapper.registerModule(simpleModule);

        // 将配置好的ObjectMapper设置到MappingJackson2HttpMessageConverter中。
        jackson2HttpMessageConverter.setObjectMapper(objectMapper);

        // 将这个自定义配置的MappingJackson2HttpMessageConverter添加到转换器列表中。
        converters.add(jackson2HttpMessageConverter);
    }
}

 3.2.3 创建ObjectMapper的子类

1. 第一种写法:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.springframework.context.annotation.Configuration;

import java.math.BigInteger;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
 * JacksonObjectMapper配置类,继承自ObjectMapper,用于全局定制Jackson的序列化与反序列化行为。
 * 主要包括处理Long类型数据的序列化为字符串,以及在反序列化时忽略未知属性。
 */
@Configuration
public class JacksonObjectMapper extends ObjectMapper {

    /**
     * 构造函数,用于初始化ObjectMapper并进行特定配置。
     * 配置包括:
     * - 反序列化时遇到未知属性不抛出异常。
     * - 注册自定义模块,使得Long和BigInteger类型的值在序列化时被转换为字符串形式,以兼容JavaScript的数字精度问题。
     */
    public JacksonObjectMapper() {
        super();
        
        // 配置反序列化设置,当遇到JSON中不存在的Java对象属性时,不抛出异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        // 创建一个SimpleModule用于扩展Jackson的功能,比如添加自定义的序列化器
        SimpleModule simpleModule = new SimpleModule();
        
        // 为SimpleModule注册三个序列化器,将Long、Long类型和BigInteger类型转换为字符串形式序列化
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance)
                   .addSerializer(Long.TYPE, ToStringSerializer.instance)
                   .addSerializer(BigInteger.class, ToStringSerializer.instance);
        
        // 将包含自定义序列化逻辑的模块注册到ObjectMapper中
        this.registerModule(simpleModule);
    }
}

2. 第二种写法:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.springframework.context.annotation.Configuration;

import java.math.BigInteger;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
 * JacksonObjectMapper配置类,继承自ObjectMapper,用于全局定制Jackson的序列化与反序列化行为。
 * 主要包括处理Long类型数据的序列化为字符串,以及在反序列化时忽略未知属性。
 */
public class JacksonObjectMapper extends ObjectMapper {

    /**
     * 构造函数,用于初始化ObjectMapper并进行特定配置。
     * 配置包括:
     * - 反序列化时遇到未知属性不抛出异常。
     * - 注册自定义模块,使得Long和BigInteger类型的值在序列化时被转换为字符串形式,以兼容JavaScript的数字精度问题。
     */
    public JacksonObjectMapper() {
        super();
        
        // 配置反序列化设置,当遇到JSON中不存在的Java对象属性时,不抛出异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        // 创建一个SimpleModule用于扩展Jackson的功能,比如添加自定义的序列化器
        SimpleModule simpleModule = new SimpleModule();
        
        // 为SimpleModule注册三个序列化器,将Long、Long类型和BigInteger类型转换为字符串形式序列化
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance)
                   .addSerializer(Long.TYPE, ToStringSerializer.instance)
                   .addSerializer(BigInteger.class, ToStringSerializer.instance);
        
        // 将包含自定义序列化逻辑的模块注册到ObjectMapper中
        this.registerModule(simpleModule);
    }
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.util.Arrays;
import java.util.List;

@Slf4j
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    /**
     * 扩展Spring MVC框架的消息转化器
     *
     * @param converters 转换器
     */
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        // 创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        // 需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
        converter.setObjectMapper(new JacksonObjectMapper());
        // 将自己的消息转化器加入容器中
        converters.add(0, converter);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

可儿·四系桜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值