当前位置: 首页 > news >正文

前端Long类型精度丢失问题:@JsonFormat与Jackson全局配置的实战对比

1. 问题来了:为什么前端拿到你的Long型ID会“变脸”?

最近在做一个用户中心的后端项目,数据库主键用的是雪花算法生成的ID,标准的19位Long类型。开发的时候一切正常,直到前端同事跑过来问我:“哥,你这用户ID传过来怎么最后三位老是变成000啊?用户A的ID应该是1357924681011121314,我这怎么显示1357924681011120000?”

我一开始还以为是前端解析的问题,结果自己用Postman一测,返回的JSON里,那个Long型的id字段,果然在1314的位置变成了1300。这就是典型的JavaScript数字精度丢失问题。你可能也遇到过,特别是当你的后端用Java,数据库主键用了像雪花算法这种会生成超大Long型数字(超过16位)的时候。

这里简单说下原理,让你彻底明白。JavaScript里,所有数字都是用IEEE 754标准的双精度浮点数来存储的。这种格式能“安全”表示的整数范围是-(2^53 - 1)(2^53 - 1),也就是-90071992547409919007199254740991。你数一下,这个范围的最大值只有16位(9007199254740991是16位数字)。而我们雪花算法生成的ID,长度是19位,妥妥地超出了这个安全范围。

所以,当后端返回一个19位的Long型数字给前端时,JavaScript在解析JSON字符串为对象的过程中,遇到这个超出安全整数范围的数字,就会进行“四舍五入”到最接近的可表示数值,导致最后几位精度丢失。这根本不是网络传输的问题,而是数据到了前端JavaScript运行时,在反序列化(把JSON字符串变成JS对象)这一步出的岔子。

那怎么解决呢?核心思路就一条:不让这个Long型数字以数字的形式出现在JSON里。最直接的办法,就是把它变成字符串。字符串可没有精度限制,传多少位就是多少位。在Spring Boot生态里,最常用的两个武器就是@JsonFormat注解Jackson的全局配置。下面我就结合自己踩过的坑,给你详细掰扯掰扯这两种方案该怎么选、怎么用。

2. 方案一:精准点射——@JsonFormat注解

当你只有少数几个字段需要处理,或者不同字段需要不同的处理规则时,@JsonFormat注解就像一把狙击枪,指哪打哪,非常灵活。

2.1 怎么用?一个注解搞定

用法简单得超乎想象。假设你有一个用户返回的UserDTO,里面有个Long类型的id字段。

import com.fasterxml.jackson.annotation.JsonFormat; public class UserDTO { // 核心就是这行注解 @JsonFormat(shape = JsonFormat.Shape.STRING) private Long id; private String username; // ... 其他字段和getter/setter }

这个@JsonFormat(shape = JsonFormat.Shape.STRING)就是在告诉Jackson库:“兄弟,在把这个对象序列化成JSON的时候,别把这个id当数字处理,直接给我转成字符串。” 同样地,当从JSON反序列化回Java对象时,如果对应字段是字符串形式的数字,Jackson也会尝试把它转换回Long

你可以在Controller里直接返回这个对象:

@GetMapping("/user/{userId}") public Result<UserDTO> getUser(@PathVariable Long userId) { UserDTO user = userService.getById(userId); return Result.success(user); // 返回的JSON中,id字段会是字符串类型 }

这时,返回给前端的JSON就会是这样:

{ "code": 200, "data": { "id": "1357924681011121314", // 注意,这里是带引号的字符串! "username": "张三" } }

前端拿到这个id,就是一个完整的字符串"1357924681011121314",再也不用担心精度丢失了。需要当数字用的时候(比如比较大小),调用一下BigInt(id)或者用第三方库如json-bigint来解析就行。

2.2 进阶玩法:格式化与局部控制

@JsonFormat的功能不止于此,它特别擅长处理各种“格式化”需求。比如你的日期字段:

public class OrderDTO { @JsonFormat(shape = JsonFormat.Shape.STRING) private Long orderId; // 指定日期格式和时区 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime createTime; // 数字格式化,比如金额保留两位小数 @JsonFormat(shape = JsonFormat.Shape.STRING) private BigDecimal amount; }

优点总结一下:

  1. 极其灵活:每个字段可以独立配置,互不影响。A字段转字符串,B字段可以保持数字并指定格式。
  2. 意图清晰:在实体类或DTO上直接标注,代码可读性高,一看就知道这个字段要怎么处理。
  3. 非侵入性:只影响被注解的字段,不会动到其他任何Long类型字段,比如那些不会超过安全范围的普通状态字段。

但是,坑也不少:

  • 容易遗漏:这是最大的问题。项目大了,DTO满天飞,今天在这个类上加一个@JsonFormat,明天可能就忘了在另一个类似的类上加。一旦遗漏,前端就可能收到一个被截断的ID,bug埋得悄无声息。
  • 维护成本高:如果后来决定所有超过16位的ID都用字符串传输,你得把所有相关的DTO、VO都翻出来加一遍注解,工作量不小。
  • 只对序列化有效:这个注解主要管的是对象转JSON(序列化)这个过程。如果你接收入参,前端传过来一个字符串格式的ID,希望自动转成Long,这个注解在反序列化时也有效。但如果你有更复杂的自定义反序列化逻辑,它可能就力不从心了。

所以,@JsonFormat适合局部、精细化的控制。当你的精度丢失问题只出现在几个核心的、确定的实体上时,用它准没错。

3. 方案二:一劳永逸——Jackson全局配置

如果你受够了在无数个DTO上重复打补丁,想要一个“根治”的方案,那么自定义Jackson的ObjectMapper,进行全局配置,就是你的不二之选。这相当于给你的整个应用装上了一套统一的“数据处理规则”。

3.1 打造你的专属ObjectMapper

Spring Boot默认使用Jackson来处理HTTP消息的转换(即@RestController返回对象变JSON,接收JSON变对象)。我们可以通过继承ObjectMapper来定制它的行为。

新建一个类,比如叫JacksonObjectMapper

import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import java.math.BigInteger; /** * 自定义Jackson对象映射器 * 目标:将所有Long类型、BigInteger类型在序列化时转为字符串 */ public class JacksonObjectMapper extends ObjectMapper { public JacksonObjectMapper() { super(); // 1. 创建一个简单模块 SimpleModule simpleModule = new SimpleModule(); // 2. 为Long类型和BigInteger类型注册“转字符串”的序列化器 // ToStringSerializer.instance 会把对象直接调用toString()方法输出 simpleModule.addSerializer(Long.class, ToStringSerializer.instance); simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance); // 处理基本类型long simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); // 3. 注册这个模块到ObjectMapper this.registerModule(simpleModule); // 4. (强烈建议)注册Java8时间模块,方便处理LocalDateTime等 this.registerModule(new JavaTimeModule()); // 5. (可选但推荐)忽略JSON中存在的、但Java对象没有的字段,防止反序列化失败 this.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } }

这段代码干了件什么事呢?它创建了一个新的规则:无论在你的应用哪个角落,只要是Long(包括基本类型long)或者BigInteger类型的字段,在转换成JSON字符串时,统统给我变成字符串格式。这样一来,所有雪花ID、所有可能的大数字,从源头上就被保护起来了。

3.2 让配置全局生效

光有自定义的ObjectMapper还不行,得让Spring Boot用它替换掉默认的那个。我们通过一个配置类来实现:

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; @Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { // 方式一:直接使用我们自定义的JacksonObjectMapper // return new JacksonObjectMapper(); // 方式二(更优雅):利用Builder创建,并混入自定义配置 ObjectMapper objectMapper = builder.createXmlMapper(false).build(); // 在builder创建的基础上,手动添加我们的“Long转String”模块 SimpleModule bigIntegerModule = new SimpleModule(); bigIntegerModule.addSerializer(Long.class, ToStringSerializer.instance); bigIntegerModule.addSerializer(Long.TYPE, ToStringSerializer.instance); bigIntegerModule.addSerializer(BigInteger.class, ToStringSerializer.instance); objectMapper.registerModule(bigIntegerModule); // 同样配置忽略未知字段 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); return objectMapper; } }

我更喜欢第二种方式,因为Jackson2ObjectMapperBuilder已经帮我们预配置了很多Spring Boot的最佳实践(比如日期格式),我们在其基础上“打补丁”,更稳妥。

配置完成后,你的整个Spring Boot应用在序列化任何对象时,都会自动应用这条“Long转String”的规则。你再也不需要为任何一个DTO字段添加@JsonFormat注解了。

3.3 全局配置的威力与注意事项

这个方案的优点非常突出:

  1. 一劳永逸:配置一次,全局生效。新写的DTO、VO,只要字段是Long型,自动享受“字符串保护”,彻底杜绝遗漏。
  2. 维护方便:规则集中在一处管理。哪天你想调整,比如只想对超过18位的Long转字符串,或者想换个格式,改这一个地方就行。
  3. 功能强大:你不仅可以处理Long,还可以在这里统一配置日期格式、处理空值、美化输出等等,是Jackson功能的集大成者。

当然,权力越大,责任越大,有些坑你得留意:

  • “误伤”问题:全局配置是无差别的。你有一个Long类型的status字段,值就1、2、3,本来完全在安全范围内,现在也被转成了字符串。虽然对前端功能可能没影响,但会让API返回的JSON看起来不那么“纯粹”(数字和字符串混在一起),也可能轻微增加传输体积。有些严格的前端框架或类型系统(如TypeScript)可能会对类型变化比较敏感。
  • 可能影响第三方库:如果你的项目引入了其他库,这些库内部也依赖Jackson进行序列化,并且对类型有严格预期,全局修改可能会带来意想不到的影响。不过这种情况在Web层序列化中较少见。
  • 反序列化的考虑:上面的配置主要针对序列化(Java -> JSON)。如果前端传参时,也以字符串形式传递ID,Spring Boot默认也能反序列化回Long。但为了更严谨,你可以在自定义模块中也添加对应的反序列化器,不过通常ToStringSerializer在反序列化时也能工作。

4. 实战对比:我该用哪个?

光讲理论不够,我们放到真实的项目场景里比比看。

场景A:小型工具类项目,实体清晰你正在开发一个内部用的运维管理系统,核心实体就UserServerTask那么几个,而且这些实体的ID都是雪花ID。这种情况下,我强烈推荐使用全局配置。在项目初期花5分钟配好JacksonObjectMapper,之后所有关于ID精度的问题都与你无关了,开发体验极其流畅。不用担心遗漏,代码也干净。

场景B:大型复杂业务系统,字段语义多样你维护的是一个电商平台,有Order(订单)、Product(商品)、Payment(支付)等上百个DTO。里面Long类型的字段五花八门:有雪花算法生成的orderId,有来自外部系统的externalProductId(可能也很大),也有普通的categoryId(数值很小),还有表示状态的status,表示库存的stock。 这时,无差别的全局转换可能就不太合适了。把statusstock这种小数字也变成字符串,会让接口返回值显得很奇怪,也可能影响前端一些数字计算逻辑(虽然可以转换,但增加了心智负担)。 更合适的策略是混合使用

  1. 使用全局配置,处理那些明确会很大的ID字段,比如所有以IdNoSn结尾的字段,我们可以通过更精细的全局配置(例如使用@JsonSerialize配合自定义逻辑)来针对性处理,但这需要更高级的Jackson技巧。
  2. 对于普通的、数值小的Long字段,保持原样
  3. 对于少数特殊的、需要特定格式(如日期、金额)的字段,使用@JsonFormat

实际上,在大型项目中,我见过更常见的做法是:定义明确的规约。比如,在团队内约定,所有在接口中传输的、可能超过JavaScript安全整数范围的标识字段(通常是数据库主键ID),其DTO字段类型直接定义为String。从Service层开始,就使用String类型的ID进行传递和返回。这样,Jackson甚至不需要做任何特殊处理,因为字段本来就是字符串。这虽然需要在业务层做一些类型转换,但语义最清晰,前后端都不会有任何歧义。

5. 避坑指南与最佳实践

踩过几次坑之后,我总结了几条经验,希望能帮你少走弯路。

第一,测试一定要做透。无论是用注解还是全局配置,改完之后,不要只用一个ID测试。准备几个边界值:

  • 一个16位的数字(如9007199254740991),这是安全边界。
  • 一个17位的数字(如12345678901234567)。
  • 一个典型的19位雪花ID。 用Postman或单元测试调用你的接口,查看返回的JSON,确认长的ID是否变成了带引号的字符串,短的ID是否保持了数字形式(如果你希望如此)。同时,也要测试反序列化,让前端同事配合传一个字符串ID过来,看你的@PathVariable@RequestBody能否正确接收为Long

第二,关注序列化与反序列化的对称性。你让Long序列化成String,那么前端传参时,如果也传String,你的Controller能否自动接受到Long?默认情况下,Spring Boot的Jackson是可以做到的。但如果你配置了过于复杂的自定义序列化/反序列化器,最好两边都测试一下。一个简单的测试接口就能搞定。

第三,谨慎处理数据库查询和MyBatis。这里解决的是HTTP层的序列化问题。如果你的数据直接从MyBatis查询出来,其中Long类型的ID在Java内存中还是正确的,经过我们的配置,在返回给前端时才被转成字符串。这本身没问题。 但要小心一种情况:如果你在某些逻辑里,直接拿前端传回来的字符串ID(比如"1357924681011121314")去和数据库查询出来的Long类型ID(1357924681011121314L)进行相等比较==equals),一个String一个Long,肯定是不相等的。所以,在比较时,要么都转成String再比,要么把String转回Long再比。

第四,考虑前端协作。和前端同学定好规矩。用了全局配置后,可以明确告诉他们:“咱们后端所有id字段返回的都是字符串了,你们用的时候注意一下。” 他们可能会用到BigInt或者一些大数处理库来解析。如果前端是强类型语言(如TypeScript),记得让他们更新接口类型定义,把idnumber改成string

最后,我的个人建议是:对于全新的项目,如果确定主要ID生成策略是雪花这类会生成大数的算法,直接在项目脚手架里就配上Jackson全局Long转String的配置。这是成本最低、效果最好的预防措施。对于老项目改造,如果涉及到的实体不多,可以先用@JsonFormat注解逐个击破,快速解决问题。如果实体很多,且确定要统一处理,那么再花点时间上全局配置,长远来看更省心。

技术选型没有绝对的对错,只有适合与否。理解了@JsonFormat的灵活和Jackson全局配置的霸道,你就能根据自己项目的实际情况,做出最顺手的选择。毕竟,解决问题的终极目标,是让代码更清晰,让协作更顺畅,而不是单纯追求某一种技术。

http://www.cnnetsun.cn/news/1281152.html

相关文章:

  • RePKG:突破Wallpaper Engine资源处理瓶颈的全栈解决方案
  • RexUniNLU中文-base教程:NLI任务中三类标签(蕴含/矛盾/中立)Schema写法
  • ARS408毫米波雷达在域控制器上的实战配置与调试
  • RePKG:Wallpaper Engine资源处理的性能突破与技术革新
  • 深度deepin系统安装全攻略:从零开始打造国产Linux工作环境
  • 告别格式焦虑:Paperxie 如何用智能排版让毕业论文一键达标
  • 实战对比:六大LLM可视化工具如何重塑智能代理开发流程
  • STM32H750实战:CUBEMX+FreeRTOS下的串口中断接收与任务通信
  • SAP PP CCAP_ECN_MAINTAIN ECN变更日期冲突的源码分析与解决方案
  • PCB阻焊工艺全解析:从油墨选择到关键工序优化
  • TortoiseGit(小乌龟)分支管理全攻略:从创建到冲突解决
  • Element-UI el-input组件type=“number“的样式优化与隐藏箭头技巧
  • 从xapp1052到LC480T:PCIe加速卡部署实战与驱动开发指南
  • 高效采集小红书无水印方案:开源工具XHS-Downloader技术实践指南
  • Dify自定义节点异步处理全链路优化:5步精准识别隐性开销,避免月度账单暴涨300%
  • 用快马AI十分钟复刻Typora:构建即时渲染的Markdown编辑器原型
  • 【FPGA】基于DS18B20的单总线温度监测系统设计与实现
  • 【嵌入式】树莓派上基于NCNN的YOLOv5模型优化与性能调优
  • 多平台直播效率提升指南:OBS Multi RTMP插件全方位应用
  • 基于GD32VW553的WS2812E彩灯驱动移植与SPI时序控制详解
  • 【ARMv8架构解析】NIC-400:芯片内部的AMBA高速公路
  • Docker 快速部署 CentOS7 开发环境指南
  • Kaggle训练模型不断连的终极配置指南
  • 2025CCPC河北省赛解题思路与实战技巧分享
  • 虚拟串口软件VSPD在串口调试中的实战应用
  • ecoRoute:纳米级ECO布线中的智能DRC修复与分层设计考量
  • ITK-SNAP实战指南:从二维切片到三维重建的医学影像分析
  • Phi-3 Mini开源镜像实操:GPU显存占用动态监控与告警设置
  • Verilog进阶:2001标准下模块端口的ANSI-C风格实践指南
  • 科研绘图自动化:让学术图表创作效率提升十倍的智能解决方案