第二十章. common-json 工具类:JsonUtils 的静态封装与泛型实战
第二十章. common-json 工具类:JsonUtils 的静态封装与泛型实战
Prorise第二十章. 脱离 MVC 的舒适区:JsonUtils 手动处理与泛型避坑
摘要:在 Controller 层,Spring MVC 帮我们自动完成了 JSON 与对象的转换。但在 Service 层、定时任务、MQ 消费者或工具类中,我们经常需要手动处理 JSON。本章将跳出源码罗列,通过 4 个真实的业务场景,教你如何使用 RVP 提供·JsonUtils 解决手动序列化、数据库 JSON 字段解析、复杂泛型转换以及动态结构处理难题。
本章学习路径
- 认知重构:理解为什么不能直接
new ObjectMapper(),以及JsonUtils如何继承全局配置。 - 业务场景一:在 日志与缓存 场景中,如何安全地进行序列化(Object -> JSON)。
- 业务场景二:在 数据库配置 场景中,如何解析存储在 String 字段里的 JSON(JSON -> Object)。
- 业务场景三:在 第三方 API 对接 场景中,如何解决
List<User>泛型擦除导致的ClassCastException。 - 业务场景四:在 Webhook 回调 场景中,如何处理结构未知的动态 JSON。
20.1. 为什么我们需要 JsonUtils?
在 Spring Boot Web 项目中,我们 99% 的时间都在写 Controller:
1 | // Spring MVC 自动把入参 json 转为 User 对象,把返回值 User 转为 json |
这让我们产生了一种错觉:JSON 处理是全自动的。
但是,当你遇到以下场景时,Spring MVC 的魔法就消失了:
- Redis 缓存:你需要把对象存入 Redis 字符串类型,必须手动转 JSON。
- 消息队列(MQ):发送消息时,对象需要序列化;监听消息时,收到的只是 String 或 byte []。
- 数据库 JSON 字段:MySQL 表里有一个
extra_config字段存了 JSON,实体类里却是String,读取后需要手动转为对象。 - 调用第三方 API:使用
HttpClient请求外部接口,拿到的是一坨 String,你需要把它变成对象。
20.1.1. 严禁手动 new ObjectMapper
当需要手动处理时,新手最容易犯的错误是:
1 | // ❌ 绝对禁止的写法 |
为什么禁止?
因为你手动 new 出来的 mapper 是“原厂设置”,它丢失了我们在第 19 章辛辛苦苦配置的所有全局规则:
- 你的
Long类型 ID 可能会再次出现精度丢失(变 0)。 - 你的
LocalDateTime又会变成奇怪的数组格式。
20.1.2. JsonUtils 的核心价值
RVP 的 JsonUtils 采用了 静态代理模式,它持有的底层 OBJECT_MAPPER 正是 Spring 容器中那个 已经配置完美 的 Bean。
1 |
|
结论:无论在项目的任何角落(哪怕是静态方法里),只要涉及 JSON 转换,必须且只能使用 JsonUtils,以保证全局数据格式的一致性。
20.2. 场景一:日志记录与 Redis 缓存(序列化)
业务需求:我们需要在 Service 层记录一条操作日志,或者将用户信息存入 Redis 缓存。
20.2.1. 痛点:繁琐的异常处理
如果直接使用原生 Jackson:
1 | try { |
20.2.2. JsonUtils 解决方案:toJsonString
JsonUtils 帮我们将受检异常(Checked Exception)转化为运行时异常,并自动处理了 null 值。
1 | // ✅ 优雅写法 |
关键细节:如果传入的对象是 null,toJsonString 会返回 null,而不是字符串 "null",这对于数据库存储和 Redis 判空非常友好。
20.3. 场景二:数据库中的 JSON 配置解析(反序列化)
业务需求:sys_user 表中有一个 config 字段(String 类型),存储了用户的个性化配置(如 {"theme":"dark", "notification":true})。我们需要在 Java 代码中读取并解析它。
20.3.1. 基础解析:parseObject
代码演示:
1 | // 假设从数据库读取到的 String |
常见错误:如果数据库字段为空字符串 "" 或 null,parseObject 会直接返回 null。在调用 config.getTheme() 之前 务必判空。
20.4. 场景三:第三方 API 与泛型擦除(核心难点)
这是二开过程中 最容易翻车 的地方。
业务需求:我们需要调用一个外部的用户中心 API,它返回的数据结构是统一响应体 R<List<UserDto>>。
20.4.1. 灾难现场:ClassCastException
新手通常会这样写:
1 | String responseBody = HttpUtil.get("https://api.example.com/users"); |
为什么?
因为 Java 的泛型是 伪泛型。在运行时,Jackson 根本不知道 R 里面包的是 List,更不知道 List 里面是 UserDto。它只能把 JSON 解析成最通用的 LinkedHashMap。
20.4.2. 解决方案:TypeReference
要解决这个问题,我们需要告诉 Jackson 完整的类型结构。JsonUtils 支持 TypeReference 传参。
正确代码:
1 | // ✅ 正确写法:使用匿名内部类保留泛型信息 |
原理通俗解:new TypeReference<T>() {} 创建了一个 匿名子类。虽然 Java 会擦除对象的泛型,但会把 类的泛型定义 保留在字节码中。Jackson 通过读取这个子类的签名,终于看清了 R<List<UserDto>> 的全貌。
20.5. 场景四:Webhook 回调与动态 Dict
业务需求:我们系统接收 GitHub 或 微信支付 的 Webhook 回调。这些回调的 JSON 结构非常复杂且不固定,我们懒得为每一个回调事件去写对应的 Java Bean(DTO)。
20.5.1. 痛点:DTO 爆炸
如果为每个可能的 JSON 结构都定义一个类,你的 entity 包会迅速膨胀,且难以维护。
20.5.2. 解决方案:parseMap (Hutool Dict)
RVP 集成了 Hutool 的 Dict(字典类),它比原生的 Map<String, Object> 更强大,支持类型自动转换。
代码演示:
1 | // 假设这是 Webhook 收到的 payload |
适用场景:
- 快速原型开发。
- 解析结构未知的 JSON。
- 只需要提取 JSON 中某这一两个字段,不想定义整个 DTO。
20.6. 进阶技巧:从复杂 JSON 中只取一个值
有时我们调用第三方接口,对方返回了几千行的 JSON,但我只需要其中深层嵌套的一个 token 字段。定义 DTO 太浪费,用 Map 太麻烦。
解决方案:使用 JsonNode (树模型)。
JsonUtils 虽然主要是对象映射,但也可以暴露底层的 readTree 能力(通过 getObjectMapper())。
1 | String json = "...(几千行的JSON)..."; |
20.7. 本节小结
在这一章,我们跳出了 MVC 的保护圈,学习了手动“操纵”数据的能力。
- 统一战线:严禁
new ObjectMapper(),必须用JsonUtils以复用全局配置(时区、ID 精度)。 - 泛型救星:遇到
List<User>或R<Data>转换报错,请立刻想起TypeReference和那对大括号{}。 - 动态应对:不想写 DTO 时,
Dict和parseMap是你的偷懒神器。 - 空值安全:
JsonUtils的所有方法都对null做了防御,不会报空指针,但返回值可能为null,业务逻辑中要注意检查。








