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);
}
}