资讯详情 前后端统一JSON工具类设计:从Jackson封装到AI输出解析实战
📅 2026/10/18 9:39:03
这一讲我们来处理一个在前后端项目里最不起眼、但几乎所有模块都会依赖的基础能力JSON 工具类。项目背景是 SpringBoot Vue Uniapp LangChain4j SpringAI 组合的智能视频推荐小程序前几讲已经把工程骨架、用户登录、推荐流程搭起来了这一讲专门把 JSON 的序列化、反序列化、字段提取、异常兜底这些公共逻辑整理成统一工具后端不再到处 try/catch 解析数据前端也不用每次手写 JSON.parse 容错。先给结论JSON 工具类的核心价值不是写几个静态方法而是定一套项目内的统一规范。后端用 Jackson 作为默认序列化框架封装静态工具类处理对象与 JSON 互转、树节点读取、类型转换并定义统一返回结构 Result前端在 Vue 和 Uniapp 侧封装安全解析、请求拦截、缓存读写同时针对 LangChain4j 和 SpringAI 这类大模型返回结果专门处理模型输出中的 JSON 字符串解析与容错。本文会给出可直接复制的 Java 和 Javascript 代码、单元测试示例、接口联调注意事项以及一份常见问题排查清单。适合正在做小程序后端、或者要把 AI 推荐结果稳定接入业务的读者直接收藏。1. 本讲定位与核心能力速览能力项说明项目组成SpringBoot 后端 Vue 管理端 Uniapp 小程序端 LangChain4j/SpringAI 接入本讲主题统一 JSON 工具类设计、前后端共用规范、AI 返回结果解析后端序列化框架JacksonSpringBoot 默认集成按项目实际引入版本为准后端工具类JsonUtils对象转 JSON、JSON 转对象、JsonNode 节点读取、类型转换统一返回结构Result 泛型类规范 code / message / data前端工具safeParse、safeStringify、请求响应拦截、缓存读写AI 场景LangChain4j / SpringAI 返回 JSON 字符串后的结构化解析测试方式JUnit 单元测试 前后端联调验证适用读者小程序全栈开发、Java 后端开发、对接大模型 API 的开发者这一讲不会引入新框架也不动推荐算法本身而是把散落在各业务代码里的 JSON 处理逻辑收敛起来。后面几讲涉及视频列表、用户行为上报、推荐结果渲染时都会复用这一讲的 JsonUtils 和 Result所以这篇内容值得先跑通再往下走。2. 智能视频推荐系统中的 JSON 流转链路先看清楚 JSON 在整套系统里经过哪些环节。智能视频推荐小程序最基础的交互链路是用户在 Uniapp 端浏览视频、点击播放、提交搜索关键词这些请求由 SpringBoot 后端接收后端根据需要调用 SpringAI 或 LangChain4j 封装的模型接口让大模型根据用户行为返回推荐候选后端再把候选视频列表、推荐理由、关联标签组装成统一结构返回给前端前端解析 JSON 后渲染推荐卡片。整个链路中任何一次数据交换都依赖 JSON。具体到业务模块JSON 至少出现在四个位置一是 HTTP 请求和响应的 body前端提交的行为数据、后端返回的推荐列表都是 JSON二是本地缓存Uniapp 端把用户信息和历史记录写入 storage 时需要序列化读取时需要反序列化三是 AI 模型接口的输入输出让模型返回结构化 JSON 比返回纯文本更稳定四是服务端日志排查推荐结果异常时需要把关键对象序列化后输出。如果每个模块各自实现一套解析逻辑字段命名风格、异常处理方式、时间日期格式都会不一致联调时最容易出现后端给的是 camelCase前端读的是 snake_case这类问题。因此在设计 JSON 工具类之前要先定三个约定字段命名统一采用驼峰 camelCase时间格式统一为字符串 yyyy-MM-dd HH:mm:ss所有接口响应统一包裹 Result 结构。项目使用 SpringBoot 时Jackson 是默认的 JSON 处理框架所以工具类直接基于 Jackson 封装不额外引入依赖。需要说明的是这里给出的代码是通用工程实践具体依赖版本和包名会跟随实际项目环境接入时按自身目录调整。3. SpringBoot 后端 JSON 工具类实现3.1 全局序列化配置先解决全局配置避免每个字段单独加注解。SpringBoot 项目中可以通过 application.yml 配置 Jackson 的序列化行为包括空值处理、时间格式、允许属性命名策略等。下面是一个通用配置模板spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 default-property-inclusion: non_null deserialization: fail-on-unknown-properties: false说明一下这几项的作用date-format 统一时间格式time-zone 解决服务器时区问题default-property-inclusion: non_null 表示序列化时忽略 null 字段响应体不会出现一堆空值fail-on-unknown-properties: false 表示反序列化时遇到未知字段不报错。实际项目中是否忽略 null 需要和后端协商如果前端需要固定字段结构建议保留 null 字段而不是直接隐藏否则前端判空逻辑会受影响。这里给的是较常见的小程序接口风格响应尽量精简。3.2 JsonUtils 工具类核心方法在项目中新建 JsonUtils 工具类集中管理 ObjectMapper 实例。ObjectMapper 是线程安全的可以定义为静态常量复用不需要每次序列化都 new 一个。工具类至少应该包含对象转 JSON 字符串、JSON 字符串转对象、读取 JSON 树节点、节点转对象、格式化 JSON、容错转换等能力。public class JsonUtils { private static final ObjectMapper MAPPER new ObjectMapper(); static { MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); MAPPER.setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss)); MAPPER.setTimeZone(TimeZone.getTimeZone(GMT8)); } private JsonUtils() {} public static String toJson(Object obj) { if (obj null) { return null; } try { return MAPPER.writeValueAsString(obj); } catch (JsonProcessingException e) { throw new RuntimeException(对象序列化失败: obj.getClass(), e); } } public static T T parse(String json, ClassT clazz) { if (json null || json.isEmpty()) { return null; } try { return MAPPER.readValue(json, clazz); } catch (JsonProcessingException e) { throw new RuntimeException(JSON解析失败: e.getMessage(), e); } } public static JsonNode readTree(String json) { if (json null || json.isEmpty()) { return null; } try { return MAPPER.readTree(json); } catch (JsonProcessingException e) { throw new RuntimeException(JSON树解析失败: e.getMessage(), e); } } public static T T nodeToObject(JsonNode node, ClassT clazz) { if (node null || node.isNull()) { return null; } try { return MAPPER.treeToValue(node, clazz); } catch (JsonProcessingException e) { throw new RuntimeException(JSON节点转换失败: e.getMessage(), e); } } public static T ListT parseList(String json, ClassT clazz) { if (json null || json.isEmpty()) { return new ArrayList(); } try { JavaType type MAPPER.getTypeFactory() .constructCollectionType(List.class, clazz); return MAPPER.readValue(json, type); } catch (JsonProcessingException e) { throw new RuntimeException(JSON列表解析失败: e.getMessage(), e); } } }这个工具类把最常见的四个场景覆盖了对象转 JSON、字符串转对象、读树节点、解析 List。需要特别注意 parseList直接使用MAPPER.readValue(json, List.class)只能得到 List内部元素会变成 LinkedHashMap所以必须通过 JavaType 构造泛型类型。3.3 统一返回结构 Result接口返回结构如果不统一前端每个页面都要单独处理错误码排查问题也很痛苦。定义 Result 泛型类所有 Controller 方法统一返回这个结构。public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.code code; result.message message; return result; } // getter / setter 略 }Controller 中的写法变成PostMapping(/recommend) public ResultRecommendVO recommend(RequestBody RecommendRequest request) { RecommendVO vo recommendService.recommend(request); return Result.success(vo); }这里有几个容易忽略的细节Result 类不建议直接继承 Map 或者把 data 做成 Object 后强转泛型能保留类型信息前端可以根据 data 的实际结构生成类型定义error 方法要分业务错误和系统错误业务错误码由各模块定义系统错误码统一用 500响应中的 message 不要直接透传异常栈前后端约定一种通用提示文案即可。4. LangChain4j 与 SpringAI 场景下的 JSON 解析4.1 AI 返回结果的 JSON 设计接入大模型时一个常见痛点是模型返回格式不稳定可能多解释一句也可能把 JSON 包在 markdown 代码块里。因此在调用 LangChain4j 或 SpringAI 之前就要在提示词中明确要求输出纯 JSON并约定字段结构。以视频推荐为例期望模型返回如下结构{ videos: [ { videoId: v1001, title: SpringBoot 入门实战, reason: 用户近期多次搜索 java 相关视频, score: 0.95 } ], summary: 根据用户行为推荐以后端开发为主的学习视频 }后端拿到该 JSON 后用 JsonUtils.readTree 读取根节点再通过 nodes(videos) 获取数组逐条转换为 VideoItem 对象summary 字段用于前端展示推荐理由。这里的关键是不要把响应直接映射成某个强类型对象因为模型可能多返回字段、少了字段、或者嵌套结构变化借助 JsonNode 先读取再转换容错空间更大。一种可行的做法是先定义推荐结果的 Java 记录类型public record RecommendResult(ListVideoItem videos, String summary) {} public record VideoItem(String videoId, String title, String reason, Double score) {}然后写一个统一的解析方法public static RecommendResult parseRecommend(String modelOutput) { ModelOutput cleaned cleanModelOutput(modelOutput); if (cleaned.isJson()) { JsonNode root readTree(cleaned.getContent()); ListVideoItem videos new ArrayList(); JsonNode videoArray root.path(videos); if (videoArray.isArray()) { for (JsonNode node : videoArray) { videos.add(nodeToObject(node, VideoItem.class)); } } String summary root.path(summary).asText(); return new RecommendResult(videos, summary); } return new RecommendResult(new ArrayList(), cleaned.getContent()); }4.2 模型输出清洗与容错模型输出清洗是 AI 接口对接中必要的环节。常见的脏数据有两种模型把 JSON 放置在json代码块中模型在 JSON 前后附加说明文字。清洗策略可以这样做先判断字符串是否以{或[开头如果不是则查找第一个{和最后一个}截取中间内容如果字符串包含json标记直接按代码块边界提取。更稳妥的方式是提示词层面约束输出格式同时在业务侧保留解析兜底即使解析失败也返回空列表不让异常直接打到前端。SpringAI 和 LangChain4j 的调用方式虽然不同但返回结果本质上都是字符串或结构化输出对象。如果项目配置了模型输出解析器可以把 JSON 映射到指定的 POJO如果没有就统一走 JsonUtils.parse。这里建议对所有 AI 输出都做一层日志记录记录原始输出、清洗后内容和解析结果方便排查推荐质量问题时还原现场。5. Vue 与 Uniapp 前端 JSON 工具封装5.1 安全解析与序列化Uniapp 端处理 JSON 最常见的坑是接口返回的数据已经是对象前端又做了一次 JSON.parse结果直接抛异常以及本地缓存写入对象前忘记序列化读取时得到 [object Object] 这样的字符串。因此前端也需要一个轻量工具模块专门处理安全转换。// utils/json.js export function safeParse(data, fallback null) { if (data null || data undefined) return fallback; if (typeof data object) return data; if (typeof data ! string) return fallback; try { return JSON.parse(data); } catch (e) { console.warn([JsonUtils] parse error:, data, e); return fallback; } } export function safeStringify(data, fallback ) { if (typeof data string) return data; try { return JSON.stringify(data); } catch (e) { console.warn([JsonUtils] stringify error:, data, e); return fallback; } } export function getField(data, path, fallback null) { const obj safeParse(data); if (!obj) return fallback; const keys path.split(.); let current obj; for (const key of keys) { if (current null || current undefined) return fallback; current current[key]; } return current undefined ? fallback : current; }safeParse 解决重复解析问题getField 解决深层字段读取问题。小程序渲染推荐列表时经常要取data.list[0].title这类嵌套字段用 getField 可以避免每一步都做空值判断。json.js 这个工具文件应该放在项目的 utils 目录配合 request 封装一起使用。5.2 请求拦截与缓存读写Uniapp 的 uni.request 返回的数据结构包含 statusCode 和 data 两层业务代码里频繁解构容易出错。可以在请求封装层统一处理当 statusCode 为 200 时进一步解析 Result 结构中的 code如果不是 200 或业务 code 不为 200统一弹 toast 并抛出异常。推荐列表接口的响应是ResultListVideoItem前端只需要读取res.data.data这个取值过程很容易把两层 data 搞混封装后业务侧就不需要关心了。缓存读写同样依赖安全解析。把用户信息写入 storage 之前调用 safeStringify读取时调用 safeParse同时加上过期时间判断。视频推荐小程序中用户的历史浏览记录、搜索关键词、推荐结果快照都可以缓存缓存 JSON 的 key 建议加版本号比如recommend_v1_cache这样后端数据结构升级时前端可以一键失效旧缓存。6. 接口联调与批量任务中的 JSON 规范6.1 请求与响应字段规范前后端联调时JSON 规范比工具类本身更重要。下面是一份建议 checklist字段命名统一使用 camelCase不混用 snake_case。时间字段统一为字符串前端不做时区换算。分页接口返回{ list, total, page, size }固定结构。业务失败时code 使用自定义错误码不要复用 HTTP 状态码做业务判断。大字段列表如视频标签数组允许为 null但要保证 data 外层不被 null。举个例子如果后端返回{ code: 200, message: success, data: { list: [ {videoId: v1, title: JSON 工具类实战} ], total: 1, page: 1, size: 10 } }前端读取时直接safeParse(res.data)?.list即可不需要判断 data 是否为 null。6.2 批量任务中的 JSON 处理视频推荐中批量任务通常指两种场景一是运营后台批量导入/更新视频元数据二是离线批量调用大模型生成推荐候选。针对第一种后端需要接收一个 JSON 数组逐条校验并落库针对第二种建议把批量输入写成 JSON Lines 格式每一行是一个独立的推荐文章{userId: u1001, keyword: springboot, topN: 5} {userId: u1002, keyword: python, topN: 8}逐行读取的好处是某一行 JSON 解析失败不影响整批任务可定位到具体行号。后端处理批量文件时避免一次性把大文件读入内存然后整体解析建议使用流式逐行处理每处理完一批记录进度方便断点续跑。7. 单元测试与效果验证后端工具类写完必须补单元测试这是保证后续业务开发不返工的关键。下面是一个针对 JsonUtils 的 JUnit 测试骨架public class JsonUtilsTest { Test public void testObjectToJsonAndBack() { VideoItem item new VideoItem(v1, JSON测试, 测试理由, 0.9); String json JsonUtils.toJson(item); assertNotNull(json); VideoItem parsed JsonUtils.parse(json, VideoItem.class); assertEquals(v1, parsed.videoId()); } Test public void testParseList() { String json [{\videoId\:\v1\},{\videoId\:\v2\}]; ListVideoItem list JsonUtils.parseList(json, VideoItem.class); assertEquals(2, list.size()); assertEquals(v1, list.get(0).videoId()); } Test public void testReadTreePath() { String json {\data\:{\list\:[{\videoId\:\v1\}]}}; JsonNode node JsonUtils.readTree(json); assertEquals(v1, node.path(data).path(list).get(0).path(videoId).asText()); } Test public void testParseInvalidJson() { assertThrows(RuntimeException.class, () - JsonUtils.parse(not-json, VideoItem.class)); } }测试时要重点关注时间格式化是否正确、null 字段是否按预期输出、未知字段反序列化是否会抛异常、List 泛型解析是否得到真实对象而不仅仅是 LinkedHashMap。这些是最容易出问题的地方。前端验证相对简单在 Uniapp 的开发者工具控制台执行const json {code:200,data:{list:[]}}; const obj safeParse(json); console.log(obj.data.list); // 期望输出 []再把请求封装接上真实后端接口查看返回的推荐列表是否正常渲染。如果 AI 接口还没完全调试好可以先 mock 一个固定 JSON 响应前端链路先行验证。8. 常见问题与排查方法问题现象可能原因排查方式解决方案前端读取 data 为 undefined响应 data 为 null 或嵌套层级不对控制台打印整个响应对象统一走 safeParse 和 getField后端解析 JSON 抛异常字符串前后有 BOM 或不可见字符打印 json 的字节码解析前 trim 并检查编码JSON.parse 二次解析报错数据已是对象typeof 判断后返回原对象使用 safeParse时间显示为时间戳Jackson 未配置 date-format检查 yml 中的 jackson 配置设置日期格式并重启服务List 元素是 LinkedHashMap反序列化未指定泛型类型检查是否用了 JavaType改用 parseList 方法AI 返回 JSON 被反引号包裹模型按 markdown 输出打印原始模型输出增加模型输出清洗逻辑缓存读取为 [object Object]写入时未序列化检查缓存写入处写入前调用 safeStringify这里面最有排查价值的是 AI 返回格式不稳定问题。LangChain4j 和 SpringAI 的客户端通常提供输出解析器但不同模型的指令遵循能力有差异不能完全依赖提示词后端解析必须兼顾容错。推荐做法是模型输出先按代码块提取再尝试从首个{截取最后才交给 JsonNode 解析解析异常时记录原始输出到日志并返回空推荐列表避免推荐接口直接 500。9. 最佳实践与使用建议第一ObjectMapper 全局单例。项目中多个模块共用一个实例不要在每个方法里 new避免创建开销和配置不一致。第二null 字段策略前后端要协商一致。响应里保留 null 字段会让前端判空更简单但传输体积略大忽略 null 字段会让响应精简但前端需要处理字段缺失。小程序场景流量敏感建议忽略 null前端统一用 getField 兜底。第三所有 AI 输出必须做格式清洗和日志留存方便后面做推荐效果复盘。第四本地缓存要设计版本号和过期时间避免后端字段升级后前端仍读取旧缓存。第五涉及用户行为数据和偏好数据时JSON 日志中不要打印完整用户信息推荐服务在记录日志前对 userId 等字段做脱敏处理调用大模型接口时也应在请求参数中只传递完成推荐任务所需的最小字段集合避免不必要地把用户隐私数据发送给外部模型服务。第六若推荐内容来自模型生成或外部素材发布前需要做内容审核确认版权和授权边界。10. 总结与下一步这一讲从后端 JsonUtils、统一 Result、AI 输出清洗到前端 safeParse 和缓存读写把智能视频推荐小程序里的 JSON 处理链路整理完了。最值得先跑通的是 JsonUtils 的单元测试它能同时验证序列化配置、泛型解析和时间格式最容易踩的坑是 AI 返回内容被 markdown 包裹以及前端对 data 的二次 JSON.parse。建议按文中的排查表提前做好防护。下一讲会进入推荐模块的核心业务逻辑把本讲的 JSON 工具真正用起来接收用户行为数据、组装推荐请求、解析模型结果并返回视频列表。建议先把本讲的 JsonUtils、Result 和前端 utils/json.js 在自己的项目里落地下一讲的代码会直接复用它。