☰
智能视频推荐小程序后端:JSON工具类设计与实战解析
2026/10/18 9:38:31 网站建设 项目流程

1. 这篇文章真正要解决的问题

如果你正在做一个“SpringBoot + Vue + Uniapp + LangChain4j + SpringAI”的智能视频推荐小程序,前期大概率把精力都放在了模型接入、推荐算法、前后端联调上。但项目一旦进入中后期,你会发现一个很尴尬的问题:系统里到处都在手工拼接 JSON。

举几个真实场景:

  • 前端 Uniapp 请求后端接口,后端返回的结果需要统一包装成{code, message, data}结构。
  • 调用 SpringAI 或 LangChain4j 时,大模型返回的内容是文本,但你可能希望它输出结构化 JSON,比如推荐理由、视频标签、用户兴趣画像。
  • 第三方接口返回的 JSON 字段名是snake_case,而你的 Java 实体类字段是camelCase,不做处理就只能写一堆getString("video_id")。
  • 日志里要打印请求参数,结果一个不小心把密码字段也序列化出去了。

这些问题的本质是:JSON 在 Java 后端里不只是“数据格式”,它是系统对外交互的契约,是前后端协作的边界,也是大模型输出落地的关键环节。如果 JSON 工具类写得不好,项目越往后维护成本越高。

这篇文章不讲空泛的“JSON 是什么”,而是基于智能视频推荐小程序这个具体项目,把 JSON 工具类的设计思路、代码实现、坑点排查讲透。读完你可以直接把这套工具类搬进自己的项目里,或者根据业务场景改造成适合自己的版本。

2. JSON 工具类的基础概念与核心设计思路

2.1 为什么 Java 项目需要自定义 JSON 工具类

很多初学者会问:项目里已经引入了 Fastjson 或 Jackson,直接调用不就行了,为什么还要自己封装一层?

直接调用确实可以,但会带来三个问题:

第一,调用方式不统一。团队里有人用JSONObject.parseObject,有人用ObjectMapper.readValue,还有人用Gson.fromJson。一旦依赖升级或需要全局调整序列化策略,改动会散落到几十个文件里。

第二,异常处理不统一。JSON.parseObject遇到格式错误会抛异常,但不同调用者处理异常的方式完全不同:有人捕获后记录日志,有人直接忽略,有人抛给上层。最终表现是同样的错误在系统里出现多种行为。

第三,安全策略难统一。比如某些字段序列化时不能输出,日期格式要统一成yyyy-MM-dd HH:mm:ss,空值要不要输出,这些配置如果散落各处,很难保证一致。

所以,自定义 JSON 工具类的核心价值不是“重复造轮子”,而是统一门面 + 兜底策略 + 安全控制。你可以把它理解成公司的前台:来访者不需要知道每个部门在哪,只要到前台,前台会统一接待、登记、引导。

2.2 主流 JSON 库选型对比

智能视频推荐小程序的后端是 SpringBoot 3.x,默认自带 Jackson。但在国内团队里,Fastjson2 和 Hutool 的 JSON 工具也很常见。下面用一个简单对比说明:

特性JacksonFastjson2Hutool JSON
是否被 Spring Boot 默认集成是否否
性能良好优秀中等
API 易用性偏底层,需配置简单直接简单直接
对 Java 8 日期时间支持需注册模块内置支持较好支持
社区活跃度高高较高
适合场景作为基础框架默认实现追求性能,简化代码不想额外引入复杂依赖

从项目稳定性角度,我个人更推荐以 Jackson 为基础封装。原因很简单:SpringBoot 全家桶已经依赖 Jackson,你不需要额外引入一个可能引发依赖冲突的大库。但如果你的团队对 Fastjson2 已经很熟,也不排斥,那用它封装同样可行。

本文示例以Jackson为主,但封装的思路完全适用于其他库。

2.3 JSON 工具类的边界与职责

封装工具类之前,先划清边界。工具类只负责以下几件事:

  • Java 对象转 JSON 字符串。
  • JSON 字符串转 Java 对象。
  • JSON 字符串转JsonNode/JSONObject之类的通用结构。
  • 复杂类型转换,比如List<VideoInfo>。
  • 统一处理日期格式、空值策略、异常包装。

工具类不应该做的事:

  • 不负责业务校验,比如视频 ID 是否存在。
  • 不负责日志埋点,那是日志框架的事。
  • 不负责网络传输,那是 Controller 和 Feign 的事。

边界清晰之后,工具类的代码才不会越写越臃肿。

3. 环境准备与项目依赖配置

3.1 项目环境说明

本文示例基于以下环境,版本请以你实际项目为准:

  • JDK 17 或以上。
  • Spring Boot 3.x(自动包含 Jackson 2.x)。
  • Maven 或 Gradle 作为构建工具。
  • 后端工程名示例:smart-video-server。

如果你的项目是 Spring Boot 2.x,依赖坐标略有不同,但 Jackson 的核心 API 一致,示例代码可以无缝迁移。

3.2 添加依赖

如果使用 Spring Boot,请检查pom.xml中已经存在spring-boot-starter-web,它会自动引入 Jackson:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>

如果你需要更精细地控制 Jackson 特性,可以显式添加 Jackson 依赖:

<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>

jackson-datatype-jsr310是 Java 8 日期时间类型(LocalDate、LocalDateTime)序列化的关键依赖。如果你的项目里没有它,LocalDateTime默认会被序列化成数组,而不是字符串。

3.3 配置文件中的 JSON 相关参数

在application.yml里,我们可以统一配置 Jackson 的全局行为:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 default-property-inclusion: non_null serialization: fail-on-empty-beans: false deserialization: fail-on-unknown-properties: false

这些配置的含义:

  • date-format:统一日期格式。
  • time-zone:指定时区,避免服务器和数据库时区不一致。
  • default-property-inclusion: non_null:序列化时忽略null字段,减小 JSON 体积。
  • fail-on-unknown-properties: false:反序列化时忽略未知字段。这个配置很有用,因为前端或第三方接口经常多传字段,如果设置为 true,会导致反序列化直接失败。

不过要注意:spring.jackson.date-format对LocalDateTime并不总是生效。如果发现日期格式没变,需要在工具类里通过Jackson2ObjectMapperBuilder或ObjectMapper手动注册JavaTimeModule,并设置LocalDateTime的序列化格式。这一点在后面的工具类代码中会体现。

4. 核心流程拆解:从 JSON 字符串到 Java 对象

4.1 整体流转

在实际项目中,一次典型的 JSON 处理流程是这样的:

  1. 前端 Uniapp 调用后端接口,传入 JSON 字符串。
  2. SpringMVC 通过 HttpMessageConverter 将 JSON 自动反序列化为 Controller 方法参数对象。
  3. Controller 调用 Service 层处理业务。
  4. Service 层调用 LangChain4j 或 SpringAI 与大模型交互,大模型返回文本或 JSON。
  5. Service 层使用 JSON 工具类将大模型返回内容解析成结构化对象。
  6. Controller 将返回对象交给 SpringMVC 序列化为 JSON 响应给前端。

可以看到,第 2 步和第 6 步由框架完成,但第 5 步必须自己处理。这就是 JSON 工具类的主要使用场景。

4.2 实体类设计示例

假设我们的智能视频推荐小程序里有这样一个视频信息实体:

// 文件路径:src/main/java/com/example/smartvideo/model/VideoInfo.java package com.example.smartvideo.model; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; @Data public class VideoInfo { private Long id; private String title; private String coverUrl; private BigDecimal score; private List<String> tags; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; /** * 内部字段,序列化时忽略,避免泄露给前端 */ @JsonIgnore private String internalRemark; }

这里用到了三个注解:

  • @JsonFormat:指定LocalDateTime的格式。
  • @JsonIgnore:序列化时忽略该字段,适合内部使用、不想暴露给前端的字段。
  • @JsonProperty(后面会用到):显式指定 JSON 字段名,解决命名不一致问题。

4.3 手工拼接 JSON 的错误示范

新手最容易写出这样的代码:

String json = "{\"code\":200,\"message\":\"success\",\"data\":{\"title\":\"" + title + "\"}}";

这种写法至少有四个风险:

第一,title里如果包含双引号、反斜杠、换行符,拼接出来的 JSON 直接非法。

第二,如果title为null,最终 JSON 会变成{"title":null},但如果你拼成"title":" + null + ",就会生成"title":null,虽然勉强合法,但连字符串的引号都不见了,属于"碰巧能用"。

第三,一旦结构嵌套变深,手工拼接简直是一场灾难。

第四,没有转义处理,很容易引入 JSON 注入或 XSS 风险。

切记:能用工具类完成 JSON 序列化的地方,永远不要手工拼接。

4.4 大模型输出 JSON 的特殊处理

在智能视频推荐小程序里,大模型返回的往往不是严格 JSON。常见情况是:

  • 前后有解释性文字,比如"以下是推荐结果:{...}"。
  • 返回 Markdown 代码块包裹的 JSON。
  • 字段名和实体类不一致。
  • 偶尔多出逗号或注释。

因此,解析大模型输出时要先做一次"清洗",提取出 JSON 片段再解析。具体实现会在工具类中提供。

5. 完整示例:JSON 工具类代码实现

5.1 工具类整体结构

这里以一个基于 Jackson 的JsonUtils为例。它既能处理普通 Java 对象转换,也能处理大模型非标准 JSON 输出的提取。

// 文件路径:src/main/java/com/example/smartvideo/util/JsonUtils.java package com.example.smartvideo.util; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import org.springframework.util.StringUtils; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; public class JsonUtils { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final ObjectMapper OBJECT_MAPPER = createObjectMapper(); private JsonUtils() { } private static ObjectMapper createObjectMapper() { ObjectMapper objectMapper = new ObjectMapper(); // 注册 Java 8 时间模块 JavaTimeModule javaTimeModule = new JavaTimeModule(); javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); objectMapper.registerModule(javaTimeModule); // 序列化时忽略 null 字段 objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 反序列化时忽略未知字段 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 允许单个字符串作为 JSON 值传入 objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); // 空对象不报错 objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); return objectMapper; } }

这里的关键点是手动注册JavaTimeModule,因为仅仅在application.yml中配置spring.jackson.date-format不一定能覆盖LocalDateTime。

5.2 核心 API:对象转 JSON

下面添加序列化相关方法:

// 文件路径:src/main/java/com/example/smartvideo/util/JsonUtils.java(接上文) public static String toJson(Object obj) { if (obj == null) { return null; } try { return OBJECT_MAPPER.writeValueAsString(obj); } catch (JsonProcessingException e) { throw new RuntimeException("对象序列化为 JSON 失败: " + obj.getClass().getName(), e); } } public static String toJsonPretty(Object obj) { if (obj == null) { return null; } try { return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj); } catch (JsonProcessingException e) { throw new RuntimeException("对象序列化为格式化 JSON 失败: " + obj.getClass().getName(), e); } }

toJson是最常用的方法。toJsonPretty用于日志打印或调试,生产环境不建议频繁使用,因为它会额外增加输出体积。

5.3 核心 API:JSON 转对象

反序列化方法需要支持三种常见场景:

  • 转单个对象。
  • 转泛型集合,比如List<VideoInfo>。
  • 转Map或JsonNode,便于灵活取值。
// 文件路径:src/main/java/com/example/smartvideo/util/JsonUtils.java(接上文) public static <T> T parseObject(String json, Class<T> clazz) { if (!StringUtils.hasText(json)) { return null; } try { return OBJECT_MAPPER.readValue(json, clazz); } catch (JsonProcessingException e) { throw new RuntimeException("JSON 反序列化为 " + clazz.getName() + " 失败", e); } } public static <T> T parseObject(String json, TypeReference<T> typeReference) { if (!StringUtils.hasText(json)) { return null; } try { return OBJECT_MAPPER.readValue(json, typeReference); } catch (JsonProcessingException e) { throw new RuntimeException("JSON 反序列化失败,目标类型: " + typeReference.getType(), e); } } public static <T> List<T> parseList(String json, Class<T> clazz) { if (!StringUtils.hasText(json)) { return null; } try { return OBJECT_MAPPER.readValue(json, new TypeReference<List<T>>() {}); } catch (JsonProcessingException e) { throw new RuntimeException("JSON 反序列化为 List<" + clazz.getName() + "> 失败", e); } } public static JsonNode parseTree(String json) { if (!StringUtils.hasText(json)) { return null; } try { return OBJECT_MAPPER.readTree(json); } catch (JsonProcessingException e) { throw new RuntimeException("JSON 解析为 JsonNode 失败", e); } } public static Map<String, Object> parseMap(String json) { return parseObject(json, new TypeReference<Map<String, Object>>() {}); }

注意parseList里的new TypeReference<List<T>>() {}写法。你不能直接写List<T>.class,因为 Java 泛型擦除会导致运行时拿不到具体类型。TypeReference的工作原理是保存泛型签名,让 Jackson 在反序列化时知道目标类型。

5.4 核心 API:大模型输出清洗与解析

这是智能视频推荐小程序里最实用的一个方法。LangChain4j 和 SpringAI 返回的文本可能不是纯 JSON,我们需要先提取出 JSON 片段。

// 文件路径:src/main/java/com/example/smartvideo/util/JsonUtils.java(接上文) public static String extractJsonString(String rawText) { if (!StringUtils.hasText(rawText)) { return null; } String text = rawText.trim(); // 去掉 Markdown 代码块标记 if (text.startsWith("```")) { int firstNewLine = text.indexOf('\n'); if (firstNewLine > 0) { text = text.substring(firstNewLine + 1); } if (text.endsWith("```")) { text = text.substring(0, text.length() - 3); } text = text.trim(); } // 从第一个 { 开始截取 int start = text.indexOf('{'); if (start < 0) { throw new RuntimeException("无法从大模型输出中提取 JSON: " + rawText); } // 从最后一个 } 结束截取 int end = text.lastIndexOf('}'); if (end < 0 || end < start) { throw new RuntimeException("无法从大模型输出中提取 JSON: " + rawText); } return text.substring(start, end + 1); } public static <T> T parseAiJson(String rawText, Class<T> clazz) { String json = extractJsonString(rawText); return parseObject(json, clazz); }

这里采用了最稳妥的截取方式:找第一个{和最后一个}。优点是简单可靠,不怕模型在前面加废话、后面加补充说明。缺点是不能处理一个文本里包含多个 JSON 对象的情况,但在推荐理由生成、用户画像解析这些单个 JSON 场景下完全够用。

5.5 业务代码调用示例

假设我们让大模型生成视频推荐理由,返回格式约定为:

{ "videoId": 1001, "reason": "根据你最近观看的科技测评内容,推荐这部人工智能纪录片。", "confidence": 0.92, "tags": ["AI", "纪录片"] }

定义对应的 DTO:

// 文件路径:src/main/java/com/example/smartvideo/model/RecommendReason.java package com.example.smartvideo.model; import lombok.Data; import java.util.List; @Data public class RecommendReason { private Long videoId; private String reason; private Double confidence; private List<String> tags; }

在 Service 层调用:

// 文件路径:src/main/java/com/example/smartvideo/service/RecommendService.java(片段) public RecommendReason parseAiReason(String aiOutput) { RecommendReason reason = JsonUtils.parseAiJson(aiOutput, RecommendReason.class); if (reason == null || reason.getVideoId() == null) { throw new RuntimeException("AI 推荐理由解析失败,输出内容: " + aiOutput); } return reason; }

这样就把大模型文本和 Java 对象之间建立了一个稳定的转换通道。即使模型偶尔输出不规整,工具类也能兜底提取。

5.6 统一响应结构示例

后端接口通常需要统一响应格式。定义一个泛型响应类:

// 文件路径:src/main/java/com/example/smartvideo/common/Result.java package com.example.smartvideo.common; import lombok.Data; @Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }

Controller 中直接返回Result对象,SpringMVC 会自动调用 Jackson 序列化:

@GetMapping("/recommend") public Result<List<RecommendReason>> recommend(@RequestParam Long userId) { List<RecommendReason> reasons = recommendService.getReasonsByUserId(userId); return Result.success(reasons); }

最终前端收到的 JSON 是:

{ "code": 200, "message": "success", "data": [ { "videoId": 1001, "reason": "根据你最近观看的科技测评内容,推荐这部人工智能纪录片。", "confidence": 0.92, "tags": ["AI", "纪录片"] } ] }

6. 运行结果与效果验证

6.1 编写单元测试

不要等接口联调才发现 JSON 解析问题。建议直接为JsonUtils写单元测试。以下测试类可以作为模板:

// 文件路径:src/test/java/com/example/smartvideo/util/JsonUtilsTest.java package com.example.smartvideo.util; import com.example.smartvideo.model.VideoInfo; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; import static org.junit.jupiter.api.Assertions.*; class JsonUtilsTest { @Test void testToJsonAndParseObject() { VideoInfo videoInfo = new VideoInfo(); videoInfo.setId(1L); videoInfo.setTitle("Spring Boot 实战"); videoInfo.setCoverUrl("http://example.com/cover.png"); videoInfo.setScore(new BigDecimal("9.2")); videoInfo.setTags(Arrays.asList("Java", "Spring")); videoInfo.setCreateTime(LocalDateTime.of(2025, 1, 1, 12, 0, 0)); videoInfo.setInternalRemark("内部备注,不应出现在 JSON 里"); String json = JsonUtils.toJson(videoInfo); System.out.println("序列化结果: " + json); // 断言内部字段被忽略 assertFalse(json.contains("internalRemark")); // 断言日期格式正确 assertTrue(json.contains("2025-01-01 12:00:00")); // 反序列化回对象 VideoInfo parsed = JsonUtils.parseObject(json, VideoInfo.class); assertNotNull(parsed); assertEquals(Long.valueOf(1L), parsed.getId()); assertEquals("Spring Boot 实战", parsed.getTitle()); } @Test void testParseJsonArray() { String json = "[{\"id\":1,\"title\":\"视频A\"},{\"id\":2,\"title\":\"视频B\"}]"; List<VideoInfo> list = JsonUtils.parseList(json, VideoInfo.class); assertNotNull(list); assertEquals(2, list.size()); assertEquals("视频A", list.get(0).getTitle()); } @Test void testExtractJsonFromAiOutput() { String aiOutput = "以下是推荐结果:\n```json\n{\"videoId\":1001,\"reason\":\"推荐理由\",\"confidence\":0.92}\n```\n希望对你有帮助!"; String json = JsonUtils.extractJsonString(aiOutput); System.out.println("提取后的 JSON: " + json); assertNotNull(json); assertTrue(json.startsWith("{")); assertTrue(json.endsWith("}")); } }

6.2 预期输出与验证点

运行测试后,控制台会打印类似结果:

序列化结果: {"id":1,"title":"Spring Boot 实战","coverUrl":"http://example.com/cover.png","score":9.2,"tags":["Java","Spring"],"createTime":"2025-01-01 12:00:00"}

验证点有三个:

  • internalRemark字段没有出现在 JSON 中,说明@JsonIgnore生效。
  • createTime输出为"2025-01-01 12:00:00",说明日期格式控制成功。
  • null字段被忽略,因为我们在ObjectMapper中配置了NON_NULL。

如果createTime输出成了数组格式或时间戳,优先检查JavaTimeModule是否注册成功,以及DateTimeFormatter的 pattern 是否正确。

6.3 配合 SpringAI / LangChain4j 的验证方式

调用大模型接口时,建议先打印原始返回内容,再走工具类解析:

String rawOutput = aiChatModel.call(prompt); log.info("AI 原始输出: {}", rawOutput); RecommendReason reason = JsonUtils.parseAiJson(rawOutput, RecommendReason.class); log.info("解析后对象: {}", JsonUtils.toJson(reason));

这样你既能直观看到模型输出长什么样,也能快速定位解析失败是模型问题还是工具类问题。

7. 常见问题与排查思路

JSON 工具类看起来简单,实际使用时问题并不少。下面按经验整理高频问题。

问题现象可能原因排查方式解决方案
LocalDateTime序列化成数组[2025,1,1,12,0]未注册JavaTimeModule或未配置格式打印序列化结果,检查 ObjectMapper 配置注册JavaTimeModule,并添加LocalDateTimeSerializer
反序列化报UnrecognizedPropertyException目标对象缺少多余字段,且未关闭FAIL_ON_UNKNOWN_PROPERTIES查看异常信息中的字段名设置FAIL_ON_UNKNOWN_PROPERTIES为 false
大模型返回内容解析失败输出不是纯 JSON,包含解释文字或 Markdown 标记打印原始输出,观察首尾字符使用extractJsonString提取后再解析
序列化出现循环引用Infinite recursion两个对象互相引用,比如父子结构查看堆栈,确认字段关系在关联字段上加@JsonIgnore或@JsonManagedReference
日期格式固定为yyyy-MM-dd HH:mm:ss但接口返回时间戳application.yml配置对LocalDateTime不生效检查是否使用 Java 8 时间类型在实体字段加@JsonFormat,或手动注册模块
JSON 中包含反斜杠转义字符,导致前端解析失败字符串中包含特殊字符,序列化时被自动转义打印原始 JSON 字符串这是 JSON 正常行为,前端用JSON.parse即可
parseList返回null传入 JSON 为空字符串或空白字符检查StringUtils.hasText判断逻辑工具类中已做空字符串保护,但调用方仍要检查入参
泛型List<T>解析后元素是LinkedHashMap而不是目标类型直接使用List.class反序列化检查代码是否用了TypeReference改用new TypeReference<List<VideoInfo>>() {}
序列化时Boolean字段变成isXxx结构Lombok 的getter生成规则与 Jackson 耦合查看实体类字段和getter命名明确设计字段名,或使用@JsonProperty指定

除了表格里的问题,还有一个隐藏很深的坑:大模型有时会输出NaN或Infinity这类非标准 JSON 数值。Jackson 默认是拒绝反序列化这类值的。如果这种概率很低,没必要在工具类里全局放开,建议在调用大模型时通过 Prompt 约束输出格式,或者在解析前做一次正则替换。

8. 最佳实践与工程建议

8.1 设计统一返回结构,但不滥用

Result<T>统一响应结构非常有价值,因为它让前端 Uniapp 可以用同一套逻辑处理成功和失败。但要注意两点:

第一,分页数据、错误详情等不要塞进data里变成一个复杂的嵌套对象,尽量单独定义字段或 DTO。

第二,不要把Result用在大模型流式输出的场景。流式输出每条消息都包一层Result会增加无谓开销,也容易把简单的问题复杂化。

8.2 字段命名规范:前后端约定优先

在后端实体类里,推荐字段使用驼峰命名,比如coverUrl。但如果第三方接口返回cover_url,不要改实体类字段名,而是在字段上加@JsonProperty("cover_url"):

@JsonProperty("cover_url") private String coverUrl;

这样既能保持 Java 代码风格一致,又能对接外部接口。比手工写一堆get("cover_url")要清晰得多。

8.3 日志打印安全:敏感字段必须脱敏

JSON 序列化经常用于打印日志。这时要特别小心:密码、token、手机号等敏感字段一旦被序列化,就会进日志系统,成为安全隐患。

推荐三种处理方式:

  • 在实体类敏感字段上标注@JsonIgnore,彻底不输出。
  • 单独定义日志专用 DTO,只包含必要字段。
  • 在日志输出前调用脱敏工具,将手机号中间四位替换为****。

8.4 ObjectMapper 尽量复用

ObjectMapper是线程安全的,构建成本较高,不要每次调用都new一个。最佳实践是像本文工具类一样,在静态代码块中初始化一个全局实例并复用。

如果项目里需要不同配置的 ObjectMapper,可以定义多个命名实例,比如objectMapperForThirdParty、objectMapperForLog,但不要随意创建。

8.5 大模型 JSON 输出的二次校验

我们虽然提供了extractJsonString,但它只能解决"格式不规整"的问题,不能解决"内容不合法"的问题。大模型返回的 JSON 即使解析成功,字段值也可能不符合业务预期,比如videoId是负数、confidence大于 1。因此,解析之后一定要做业务校验:

  • 必填字段是否为 null。
  • 枚举值是否在合法范围内。
  • 外键 ID 是否存在。

建议把校验逻辑放在独立的 Service 方法里,而不是塞进JsonUtils。这样工具类保持纯粹,业务规则也更容易测试。

8.6 约定优于配置:让团队统一调用入口

在团队协作中,建议在 README 或代码规范文档里明确约定:

  • 所有 JSON 序列化反序列化只能调用JsonUtils。
  • 禁止在业务代码中直接使用ObjectMapper或JSONObject.parseObject。
  • 禁止手工拼接 JSON 字符串。
  • 新增实体类时,如果字段不想暴露给前端,必须添加@JsonIgnore。

这样长期维护时,改动 JSON 策略只需要改一个类,风险可控。

9. 总结与后续学习方向

本文围绕智能视频推荐小程序的后端开发,把 JSON 工具类的设计、实现、测试和排查讲了一遍。核心要点可以归纳为四条:

第一,自定义 JSON 工具类的真正价值不是封装 API,而是统一调用入口、统一异常处理、统一序列化策略。

第二,基于 Jackson 封装时,重点处理好 Java 8 时间类型、未知字段、空值策略三个问题。这三件事最容易在不同环境出现行为不一致。

第三,SpringAI 和 LangChain4j 场景下,大模型输出可能是"夹带私货"的文本,extractJsonString这种提取方法很有实战价值。

第四,JSON 工具类要放在安全框架内使用:字段脱敏、业务校验、团队规范缺一不可。

下一步建议你按这个顺序继续深入:

  • 把JsonUtils跑进自己的项目,补全单元测试。
  • 对接一个真实的大模型接口,打印原始输出,观察需要清洗的特殊情况。
  • 研究@JsonProperty、@JsonIgnore、@JsonFormat的更多组合用法,因为它们在复杂嵌套对象中非常有用。
  • 如果项目中 JSON 处理性能成为瓶颈,再考虑对比 Fastjson2 的序列化性能,但不要一开始就为了性能牺牲可维护性。

代码永远是为业务服务的。JSON 工具类写得好,系统对接大模型、前端联调都会顺畅很多。建议把这篇文章收藏,等你在智能视频推荐小程序项目里遇到 JSON 解析问题时,再对照着排查一遍,会比自己摸索快不少。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询