1. 为什么 JSON 处理是 Java 开发者的必修课做 Java 后端开发十来年如果让我挑一个几乎每天都要打交道的技术点JSON 的组装和解析绝对排得进前三。不管你是写 REST 接口、对接第三方服务、处理配置文件还是做数据同步、日志结构化JSON 几乎无处不在。很多刚入行的同学觉得“JSON 不就是拼字符串吗”结果真到项目里被转义字符、嵌套结构、日期格式、大数字精度这些问题折腾得够呛。这篇文章我想从一个一线开发者的视角把 Java 里 JSON 组装和解析这件事彻底讲透。不是那种 API 手册式的罗列而是把我在实际项目里踩过的坑、做过的选型、总结出来的套路原原本本分享出来。内容会覆盖主流 JSON 库的对比选型、手工组装与对象映射两种思路、嵌套与集合的处理、日期和数字的坑、性能优化以及一整套排查问题的实战技巧。适合谁看如果你是会写 Java 但 JSON 处理总是靠搜索引擎拼凑的初中级开发者这篇能帮你建立完整的心智模型如果你是有一定经验的工程师里面关于选型权衡、性能对比、边界处理的细节应该也能给你一些新的参考。全文我会尽量用大白话加实际代码来说明保证你能直接抄作业。2. JSON 处理的核心思路与方案选型2.1 先搞清楚组装和解析到底在做什么在动手写代码之前得先把概念理清楚。JSON 的组装本质上是把 Java 里的数据结构对象、Map、List、基本类型转换成符合 JSON 语法规范的字符串而解析则是反过来把一段 JSON 字符串还原成 Java 里可以操作的对象或数据结构。这两个方向合起来叫序列化serialize和反序列化deserialize。听起来简单但真正麻烦的地方在于“映射关系”。Java 是强类型的有 int、long、double、String、Date、枚举、泛型集合而 JSON 只有字符串、数字、布尔、null、数组、对象这几种类型。两边的类型系统不是一一对应的所以中间必然存在转换规则和取舍。比如 Java 的 long 到了 JSON 里就是一个数字但 JavaScript 的数字精度只有 53 位超过就可能丢精度再比如 Date 类型JSON 标准里根本没有日期类型只能约定用字符串表示那格式怎么定、时区怎么处理全是坑。理解了这一层你就明白为什么选一个靠谱的 JSON 库、定一套统一的转换规范比随手拼字符串重要得多。2.2 主流 JSON 库怎么选一场务实的对比Java 生态里处理 JSON 的库不少我这些年主要用过这几种Jackson、Gson、Fastjson以及它的后续版本 Fastjson2、还有 org.json 这种轻量级的。每个都有自己的脾气选错了后期维护会很痛苦。库名称上手难度性能表现功能完整度我的使用场景建议Jackson中等优秀非常完整Spring 项目首选功能全社区活跃Gson简单良好较完整小型工具、Android、快速原型Fastjson2简单优秀完整对性能敏感的高并发场景org.json很简单一般基础临时脚本、极简需求我个人的选型逻辑是这样的如果是 Spring Boot 项目直接用 Jackson因为 Spring 默认就集成它省得引入额外依赖而且 Jackson 对注解、泛型、多态的支持非常成熟。如果是写一个独立的小工具或者对依赖体积敏感Gson 更轻便API 也更直观。如果项目对序列化性能有极致要求比如每秒要处理几十万次转换那可以重点评估 Fastjson2但一定要做好安全配置和版本管理。这里要特别提醒一句无论选哪个库都不要用“字符串拼接”的方式去手工组装 JSON。我见过太多因为拼接导致的 bug——某个字段里带了双引号或者换行符整个 JSON 就废了排查起来还特别费劲。让专业的库去处理转义是底线。2.3 两种组装思路手工构建 vs 对象映射组装 JSON 有两条路。第一条是“手工构建”用库提供的树模型比如 Jackson 的 ObjectNode、Gson 的 JsonObject一个字段一个字段地往里塞。第二条是“对象映射”直接定义一个 Java 类把对象丢给库去序列化。手工构建适合什么场景字段是动态的、不确定的比如你要根据用户权限决定返回哪些字段或者字段名本身是运行时才确定的。这种时候用树模型灵活改起来方便。对象映射适合什么场景结构固定、字段明确的业务对象比如订单、用户、商品。定义一个 POJO加几个注解一行代码就能转成 JSON可读性和可维护性都高得多。我的经验是能用对象映射就用对象映射实在动态得厉害再退回树模型。因为对象映射有编译期检查字段名写错了 IDE 会提示重构的时候也安全而树模型里字段名是字符串写错了只有运行时才报错属于“埋雷”。3. 核心细节解析与实操要点3.1 对象映射的注解配置把规则说清楚用对象映射注解是绕不开的。以 Jackson 为例几个最常用的注解必须掌握。JsonProperty用来指定字段在 JSON 里的名字比如 Java 里叫userName但接口约定要返回user_name就靠它。JsonIgnore用来排除某些字段比如密码、内部状态绝对不能序列化出去。JsonFormat专门处理日期格式这个后面单独讲。JsonInclude控制 null 值要不要输出接口对接时经常要求“null 字段不返回”就靠它。public class UserDTO { JsonProperty(user_name) private String userName; JsonIgnore private String password; JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private Date createTime; JsonInclude(JsonInclude.Include.NON_NULL) private String email; }这里有个实操心得注解不要滥用。我见过一个类上挂了十几个注解字段名、格式、顺序全在注解里配结果新人接手完全看不懂。注解只用来处理“Java 命名和 JSON 约定不一致”的地方如果两边本来就一致就别加保持代码干净。3.2 嵌套对象和集合的处理层级再深也不慌实际业务里的 JSON 很少是扁平的基本都是嵌套的。比如一个订单里有用户信息、商品列表、收货地址商品列表里每个商品又有自己的属性。这种结构用对象映射处理起来其实很自然因为 Java 类本身就可以嵌套。public class OrderDTO { private String orderId; private UserDTO user; private ListItemDTO items; private AddressDTO address; }序列化的时候Jackson 会自动递归处理嵌套对象和集合你不需要写任何额外代码。反序列化的时候要注意一点泛型集合的类型信息在运行时会被擦除所以如果你拿到的是一个ListItemDTO直接强转是不行的。正确做法是用TypeReferenceListItemDTO items objectMapper.readValue(json, new TypeReferenceListItemDTO() {});这个TypeReference的匿名子类写法看起来有点怪但它是解决泛型反序列化的标准手段记住就行。Gson 里对应的写法是TypeToken思路一样。注意嵌套层级不要超过五层。我见过有人把整个业务模型塞进一个 JSON嵌套七八层结果前端解析起来痛苦后端维护起来也痛苦。如果发现层级太深考虑拆分成多个接口或者用扁平化设计。3.3 日期和时间的坑一个字段能让你加班到半夜日期处理是 JSON 里最容易出问题的地方没有之一。JSON 标准没有日期类型所以日期只能用字符串或数字表示。字符串的话格式五花八门yyyy-MM-dd、yyyy-MM-dd HH:mm:ss、ISO 8601 的2024-01-01T12:00:00Z还有带时区的、带毫秒的。数字的话有的是秒级时间戳有的是毫秒级。我的建议是接口层面统一用 ISO 8601 格式并且明确时区。ISO 8601 是国际标准各种语言和库都支持可读性也好。如果对方系统只认时间戳那就在文档里写清楚是秒还是毫秒别让对方猜。Jackson 里配置全局日期格式可以在 ObjectMapper 上设置ObjectMapper mapper new ObjectMapper(); mapper.setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss)); mapper.setTimeZone(TimeZone.getTimeZone(GMT8));但更推荐用 Java 8 的LocalDateTime、LocalDate配合JavaTimeModule因为SimpleDateFormat是线程不安全的多线程环境下会出诡异问题。这个坑我踩过当时一个接口偶发返回错误日期查了两天才定位到是SimpleDateFormat共享实例导致的。3.4 数字精度long 和 BigDecimal 的取舍数字精度问题在涉及金额、ID 的场景特别关键。Java 的 long 是 64 位能表示很大的整数但 JSON 数字在 JavaScript 里是双精度浮点安全整数范围只有 2 的 53 次方。也就是说如果你的订单 ID 超过了 9007199254740991前端解析就会丢精度ID 直接变了。解决办法有两个一是把大数字序列化成字符串前端按字符串处理二是用 BigDecimal 并配置序列化策略。Jackson 里可以这样mapper.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true);或者对特定字段加JsonSerialize(using ToStringSerializer.class)强制转成字符串。金额字段我强烈建议用 BigDecimal并且序列化成字符串避免浮点误差。用 double 存金额迟早会出问题这是血泪教训。4. 实操过程与核心环节实现4.1 从零搭建一个 JSON 处理工具类光讲理论不够我带你从头写一个实用的 JSON 工具类把常用的组装和解析都封装进去。这样你在项目里可以直接用不用每次都重复写样板代码。首先引入依赖。以 Maven 为例Jackson 的依赖是这样的dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.2/version /dependency dependency groupIdcom.fasterxml.jackson.datatype/groupId artifactIdjackson-datatype-jsr310/artifactId version2.15.2/version /dependency第二个依赖是处理 Java 8 时间类型的不加的话LocalDateTime序列化会报错。版本号建议用较新的稳定版老版本有安全漏洞。然后写工具类public class JsonUtils { private static final ObjectMapper MAPPER new ObjectMapper(); static { MAPPER.registerModule(new JavaTimeModule()); MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); } public static String toJson(Object obj) { try { return MAPPER.writeValueAsString(obj); } catch (JsonProcessingException e) { throw new RuntimeException(JSON序列化失败, e); } } public static T T fromJson(String json, ClassT clazz) { try { return MAPPER.readValue(json, clazz); } catch (JsonProcessingException e) { throw new RuntimeException(JSON反序列化失败, e); } } public static T T fromJson(String json, TypeReferenceT typeRef) { try { return MAPPER.readValue(json, typeRef); } catch (JsonProcessingException e) { throw new RuntimeException(JSON反序列化失败, e); } } }这里有几个关键配置要解释。FAIL_ON_UNKNOWN_PROPERTIES设为 false意思是 JSON 里有 Java 类没有的字段时不要报错直接忽略。这个在对接第三方接口时特别有用因为对方可能随时加字段你不可能每次都改代码。NON_NULL表示 null 字段不参与序列化能减小传输体积。4.2 手工构建复杂 JSON 的完整示例有时候结构太动态对象映射搞不定就得手工构建。用 Jackson 的 ObjectNode 和 ArrayNodeObjectMapper mapper new ObjectMapper(); ObjectNode root mapper.createObjectNode(); root.put(code, 200); root.put(message, success); ObjectNode data root.putObject(data); data.put(total, 2); ArrayNode items data.putArray(items); ObjectNode item1 items.addObject(); item1.put(id, 1001); item1.put(name, 商品A); item1.put(price, new BigDecimal(19.90)); ObjectNode item2 items.addObject(); item2.put(id, 1002); item2.put(name, 商品B); item2.put(price, new BigDecimal(29.90)); String json mapper.writerWithDefaultPrettyPrinter() .writeValueAsString(root); System.out.println(json);输出会是格式化好的 JSON字段顺序也保持插入顺序。这种写法适合构建接口响应体尤其是当响应结构需要根据业务逻辑动态调整时。注意putObject和putArray会返回子节点直接在上面继续操作就行不用手动 new。4.3 解析复杂 JSON 并提取数据解析这边如果结构固定优先用对象映射。但如果只是想从一大坨 JSON 里取几个字段定义一个完整的类就有点重了。这时候可以用树模型String json {\code\:200,\data\:{\user\:{\name\:\张三\}}}; JsonNode root mapper.readTree(json); int code root.path(code).asInt(); String name root.path(data).path(user).path(name).asText();path方法的好处是即使中间某个节点不存在也不会抛异常而是返回一个 MissingNodeasText会返回空字符串。这在处理不确定结构的 JSON 时非常安全。相比之下get方法返回 null你得自己做空判断容易漏。如果 JSON 是一个数组比如[{id:1},{id:2}]可以这样遍历JsonNode array mapper.readTree(json); if (array.isArray()) { for (JsonNode node : array) { int id node.path(id).asInt(); } }4.4 性能优化批量处理时的几个关键点单次 JSON 转换的性能差异其实不大但如果是在循环里处理几万条数据优化就很有必要了。第一ObjectMapper 是线程安全的一定要复用不要每次转换都 new 一个创建 ObjectMapper 的开销不小。第二如果只是读取少数字段用流式解析JsonParser比树模型更省内存因为它不会把整个 JSON 加载到内存里。第三序列化时如果不需要格式化输出就别用 pretty printer那个会额外做很多字符串拼接。我做过一个简单的压测处理十万条中等复杂度的对象复用 ObjectMapper 比每次新建快了将近三倍。这个差距在高并发场景下会被放大所以养成复用的习惯很重要。5. 常见问题与排查技巧实录5.1 反序列化报错速查表JSON 处理出的问题十有八九是反序列化阶段。我把常见的报错和排查思路整理成一张表遇到问题直接对照。报错信息关键词常见原因解决思路Unrecognized fieldJSON 有 Java 类没有的字段加 JsonIgnoreProperties(ignoreUnknowntrue)Cannot deserialize value of type类型不匹配如字符串给了数字字段检查字段类型或用自定义反序列化器No suitable constructor类没有无参构造方法补一个无参构造或加 JsonCreatorInvalidFormatException日期格式不匹配检查 JsonFormat 的 patternStackOverflowError对象之间有循环引用用 JsonIgnore 打断循环或 JsonIdentityInfo这张表里的每一条我都在项目里真实遇到过。尤其是循环引用那个两个实体互相持有对方引用序列化时无限递归直接栈溢出。解决办法是在其中一方加JsonIgnore或者用JsonManagedReference和JsonBackReference配对使用。5.2 那些文档里不会写的避坑经验第一个坑字段名大小写。JSON 是大小写敏感的userName和username是两个不同的字段。有些第三方接口文档写得不严谨实际返回的字段名和文档不一致这时候别急着改代码先用抓包工具确认实际返回的 JSON 长什么样。第二个坑空字符串和 null 的区别。和null在 JSON 里是两回事但很多业务代码里混着用。序列化时如果配了 NON_NULLnull 字段会消失但空字符串会保留。前端拿到“字段不存在”和“字段为空字符串”处理逻辑可能完全不同这个约定要在接口文档里写清楚。第三个坑中文乱码。这个通常不是 JSON 库的问题而是 HTTP 传输时字符集没设对。确保请求和响应的 Content-Type 都带上charsetUTF-8基本就能解决。第四个坑大 JSON 的内存占用。我处理过一个几十兆的 JSON 文件用树模型直接 OOM。后来改成流式解析边读边处理内存占用降到了几十兆。如果你的场景涉及大文件一定要考虑流式方案。5.3 安全方面的注意事项JSON 反序列化有一个经典的安全风险如果反序列化的目标类型是攻击者可控的可能被利用来执行恶意代码。所以永远不要反序列化不可信来源的、类型不确定的数据。Jackson 默认已经关闭了多态类型的自动处理但如果你手动开启了enableDefaultTyping一定要清楚自己在做什么。另外解析超大 JSON 时要设置大小限制防止恶意请求撑爆内存。Jackson 可以通过StreamReadConstraints来限制字符串长度、嵌套深度等参数。这些配置在对外接口里尤其重要。6. 我个人的一些实战体会写了这么多最后分享几个我这些年形成的习惯。第一项目里永远只用一个 JSON 库不要 Jackson 和 Gson 混着用依赖冲突和风格不统一会带来很多隐性成本。第二接口的 JSON 结构一定要有文档最好能用 OpenAPI 之类的工具自动生成人写的文档迟早会过期。第三单元测试里一定要覆盖 JSON 转换的边界情况比如 null 字段、空集合、超长字符串、特殊字符这些地方最容易出问题。还有一点遇到 JSON 相关的 bug第一反应应该是把原始 JSON 字符串打印出来看而不是盯着 Java 代码猜。很多时候问题就出在 JSON 本身的结构和你以为的不一样。这个习惯帮我省下了大量排查时间。JSON 处理这件事说难不难说简单也不简单。把选型、注解、日期、数字、嵌套这几个关键点吃透再配上几个实用的工具方法日常开发基本就够用了。剩下的就是在实际项目里不断积累经验遇到问题多总结慢慢就形成自己的套路了。