Skip to content

第50课:JSON 处理

🎯 学习目标

  • 理解 JSON 在 Java Web 项目中的作用,以及它和对象、DTO、HTTP 请求体之间的关系。
  • 掌握 Jackson 的常用序列化、反序列化、泛型处理和字段格式控制。
  • 能区分 Jackson、Gson、Fastjson 的定位和选型建议。
  • 能处理日期时间、枚举、空值、未知字段、泛型集合等常见场景。
  • 能识别 JSON 处理中的安全、兼容性和精度问题。

📖 一、JSON 是什么

JSON 是一种轻量级数据交换格式。Java 后端项目中,JSON 常用于:

text
前端请求体           -> Java DTO
Java 对象            -> HTTP JSON 响应
服务 A 调用服务 B     -> JSON 请求/响应
消息队列消息体        -> JSON 字符串
配置、日志、缓存值     -> JSON 文本

示例:

json
{
  "id": 1001,
  "name": "Alice",
  "age": 20,
  "roles": ["USER", "ADMIN"],
  "enabled": true
}

在 Java 中,它通常会映射为对象:

java
public class UserDTO {
    private Long id;
    private String name;
    private Integer age;
    private List<String> roles;
    private Boolean enabled;
}

JSON 处理的本质是:

text
序列化:Java 对象 -> JSON 字符串
反序列化:JSON 字符串 -> Java 对象

📖 二、主流 JSON 库

特点推荐程度
JacksonSpring Boot 默认,功能完整,生态最好推荐默认使用
GsonGoogle 出品,简单易用,功能相对轻量小工具可用
Fastjson 1.x曾经流行,但历史安全问题较多不建议新项目使用
Fastjson 2.x重写后安全性改进需要明确理由再选

Spring Boot 默认使用 Jackson。除非项目已有强约束,否则不要随意换 JSON 库。


📖 三、Jackson 基本用法

1. 添加依赖

Spring Boot Web 项目通常已经包含 Jackson:

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

如果是普通 Java 项目,可以单独引入:

xml
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.17.2</version>
</dependency>

2. ObjectMapper 示例

java
import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonDemo {
    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();

        UserDTO user = new UserDTO();
        user.setId(1L);
        user.setName("Alice");
        user.setAge(20);

        String json = mapper.writeValueAsString(user);
        System.out.println(json);

        UserDTO parsed = mapper.readValue(json, UserDTO.class);
        System.out.println(parsed.getName());
    }
}

输出类似:

json
{"id":1,"name":"Alice","age":20}

📖 四、Spring MVC 中的 JSON

在 Spring Boot Web 中,通常不需要手动调用 ObjectMapper

java
@RestController
@RequestMapping("/api/users")
public class UserController {

    @PostMapping
    public UserDTO create(@RequestBody UserCreateRequest request) {
        UserDTO result = new UserDTO();
        result.setId(1L);
        result.setName(request.getName());
        return result;
    }
}

这里发生了两次自动转换:

text
HTTP 请求体 JSON -> UserCreateRequest
UserDTO -> HTTP 响应体 JSON

底层由 Spring MVC 的 HttpMessageConverter 调用 Jackson 完成。


📖 五、常用注解

1. 修改字段名

java
import com.fasterxml.jackson.annotation.JsonProperty;

public class UserDTO {
    @JsonProperty("user_name")
    private String userName;
}

Java 字段是 userName,JSON 字段是 user_name

2. 忽略字段

java
import com.fasterxml.jackson.annotation.JsonIgnore;

public class UserDTO {
    private Long id;

    @JsonIgnore
    private String password;
}

密码等敏感字段不要序列化给前端。

3. 忽略未知字段

java
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class UserDTO {
    private Long id;
    private String name;
}

当前端多传了字段,后端不会反序列化失败。这对接口兼容很有用。

4. 日期格式

java
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;

public class OrderDTO {
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Hong_Kong")
    private LocalDateTime createdAt;
}

更推荐在全局配置中统一时间格式,而不是每个字段都写。


📖 六、泛型反序列化

Java 泛型有类型擦除,不能直接这样写:

java
List<UserDTO> users = mapper.readValue(json, List.class);

这会得到 List&lt;LinkedHashMap&gt;,不是 List&lt;UserDTO&gt;

正确写法:

java
import com.fasterxml.jackson.core.type.TypeReference;

List<UserDTO> users = mapper.readValue(json, new TypeReference<List<UserDTO>>() {});

分页响应也类似:

java
ApiResponse<List<UserDTO>> response = mapper.readValue(
    json,
    new TypeReference<ApiResponse<List<UserDTO>>>() {}
);

📖 七、全局配置建议

Spring Boot 中可以通过配置定制 Jackson:

yaml
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Hong_Kong
    default-property-inclusion: non_null
    deserialization:
      fail-on-unknown-properties: false

也可以注册配置类:

java
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        return mapper;
    }
}

注意:Spring Boot 已经自动配置了很多内容,自己声明 ObjectMapper Bean 时不要覆盖掉必要模块。


⚠️ 八、常见陷阱

1. BigDecimal 精度丢失

金额不要用 double

java
public class PaymentDTO {
    private BigDecimal amount;
}

如果前端用 JavaScript 处理超大整数,Long 也可能超过安全整数范围。订单号、雪花 ID 可以考虑以字符串返回。

2. LocalDateTime 不能自动处理

老版本或普通 Java 项目中,如果没有注册 JavaTimeModuleLocalDateTime 可能序列化失败。

3. DTO 直接暴露实体

不要把 JPA Entity 直接返回给前端:

java
@GetMapping("/users/{id}")
public UserEntity getUser(@PathVariable Long id) {
    return userRepository.findById(id).orElseThrow();
}

问题包括:敏感字段泄露、懒加载异常、循环引用、接口被数据库结构绑死。

推荐:

java
@GetMapping("/users/{id}")
public UserDTO getUser(@PathVariable Long id) {
    UserEntity entity = userRepository.findById(id).orElseThrow();
    return UserDTO.from(entity);
}

4. 循环引用

对象互相引用时可能导致无限递归:

text
User -> List<Order>
Order -> User

推荐使用 DTO 切断循环,而不是依赖复杂注解强行序列化实体。


🆚 九、Java vs C 对比

维度C 常见做法Java 常见做法
数据结构struct 手动映射POJO/record/DTO 自动映射
JSON 解析cJSON、Jansson 等手动取字段Jackson 按类型绑定
内存管理需要关注分配和释放JVM 管理对象生命周期
类型安全字段读取常靠字符串 key可映射为强类型对象
Web 集成需要框架或手写胶水代码Spring MVC 自动转换

Java 的优势是对象模型和框架集成强;代价是要理解反射、泛型擦除、注解和对象映射规则。


💡 十、最佳实践

  • Spring Boot 项目默认用 Jackson,不要无理由混用多个 JSON 库。
  • Controller 入参和出参使用 DTO,不直接暴露数据库实体。
  • 金额使用 BigDecimal,超大 ID 面向前端时可转字符串。
  • 密码、Token、内部状态字段使用 @JsonIgnore 或 DTO 隔离。
  • 反序列化泛型集合时使用 TypeReference
  • 时间格式全局统一,避免每个接口返回不同格式。
  • 接口兼容期可以忽略未知字段,但核心接口仍要通过 Validation 校验必填字段。
  • 不要开启危险的自动类型识别功能,避免反序列化安全风险。

🎓 小结

JSON 是 Java 后端和外部世界交换数据的核心格式。掌握 JSON 不只是会把对象转成字符串,更重要的是理解 DTO 边界、类型精度、时间格式、接口兼容和安全风险。

后续学习 Validation、Swagger 和 Spring Boot Web 时,JSON 会贯穿请求体、响应体、接口文档和错误返回设计。


🧭 十一、项目落地清单

一个规范的 Java Web 项目,JSON 相关设计至少要检查:

text
1. Controller 入参是否使用 Request DTO。
2. Controller 出参是否使用 Response DTO。
3. Entity 是否被禁止直接返回给前端。
4. 金额是否使用 BigDecimal。
5. 超大 Long ID 是否需要转成字符串返回给前端。
6. 时间格式是否全局统一。
7. 密码、Token、内部字段是否被隔离或忽略。
8. 泛型反序列化是否使用 TypeReference。
9. 是否允许未知字段,规则是否和接口兼容策略一致。
10. JSON 库是否统一,避免 Jackson/Gson/Fastjson 混用。

一个常见 DTO 分层可以这样设计:

text
UserCreateRequest   接收创建参数,带 Validation
UserUpdateRequest   接收更新参数,字段通常可选
UserQueryRequest    接收查询条件和分页参数
UserResponse        返回给前端,不包含敏感字段
UserEntity          数据库实体,不直接暴露

这种结构会多几个类,但能换来接口边界清晰和后续维护成本下降。


🔍 十二、自测问题

学习完本节后,应该能回答:

text
序列化和反序列化分别是什么?
Spring MVC 为什么能自动把 JSON 转成对象?
为什么不能直接返回 JPA Entity?
TypeReference 解决了什么问题?
为什么金额不能用 double?
为什么 LocalDateTime 有时需要额外配置?
JsonIgnore 和 DTO 隔离分别适合什么场景?
接口字段重命名时如何兼容旧前端?

JSON 是接口层最容易“看起来简单、实际埋坑”的部分。越早建立 DTO 边界,后面的 Validation、Swagger 和前后端联调越顺。


📌 十三、学习建议

建议亲手测试三类 JSON:

text
普通对象:UserDTO
集合泛型:List<UserDTO>
嵌套响应:ApiResponse<List<UserDTO>>

分别观察不使用 TypeReference 和使用 TypeReference 的差异,这能快速理解 Java 泛型擦除对 JSON 反序列化的影响。