浅谈 Java 后端对象映射:从 JSON → VO → Entity 的原理与实践

在日常后端开发中,我们经常会遇到这些问题:

  • 前端没有传某些字段,后端 VO 能接收吗?
  • VO 和 Entity 字段不一致,能否直接 copy?
  • 为什么有时候字段会变成 null?
  • BeanUtils 到底是怎么工作的?
  • JSON 是怎么变成 Java 对象的?

这些问题看似零散,其实背后是一个完整链路:

JSON → VO(反序列化)→ DTO/Entity(对象拷贝)

本文将从 原理 + 实战 两个层面,帮大家打通这条链路。


一、整体流程

在 Spring Boot 中,一个请求的完整流程如下:

text 复制代码
前端 JSON
   ↓
HTTP 请求 Body
   ↓
Spring MVC
   ↓
HttpMessageConverter
   ↓
Jackson(反序列化)
   ↓
VO 对象
   ↓
BeanUtils / MapStruct
   ↓
Entity(数据库对象)

👉 核心就两个动作:

  1. 反序列化(JSON → VO)
  2. 对象拷贝(VO → Entity)

二、JSON → VO:为什么字段可以不传?

1️⃣ 示例

前端请求

json 复制代码
{
  "name": "张三"
}

后端 VO

java 复制代码
@Data
public class UserVO {
    private String name;
    private Integer age;
}

最终结果

java 复制代码
UserVO {
    name = "张三",
    age = null
}

完全正常 ✅


2️⃣ 底层原理(Jackson 在干什么)

Spring 默认使用:

java 复制代码
MappingJackson2HttpMessageConverter

而真正做转换的是 Jackson


Jackson 的核心逻辑:

text 复制代码
1. 创建对象(反射)
2. 遍历 JSON 字段
3. 找同名属性
4. 调 setter 赋值

关键点:

情况 行为
JSON有字段 赋值
JSON没有字段 忽略
VO多字段 保持默认值

3️⃣ 为什么设计成这样?

兼容性(API演进)

后端新增字段,老前端不传也不会报错

前后端解耦

前端无需完全同步后端结构

支持部分更新(Patch)

只传需要修改的字段


4️⃣ 注意事项(坑点)

(1)基本类型问题

java 复制代码
private int age;  // ❌

前端不传:

java 复制代码
age = 0  // 无法区分"没传"还是"就是0"

✅ 推荐:

java 复制代码
private Integer age;

(2)校验注解

java 复制代码
@NotNull
private Integer age;

前端不传:直接报错(400)


(3)JSON 多字段

json 复制代码
{
  "name": "张三",
  "xxx": "未知字段"
}

默认:忽略。可配置为报错:

yaml 复制代码
fail-on-unknown-properties: true

三、VO ↔ Entity:对象拷贝的本质

1️⃣ 示例

VO

java 复制代码
public class AgentVO {
    private String name;
    private String address;
    private String displayName; // VO特有
}

Entity

java 复制代码
public class AgentEntity {
    private String name;
    private String address;
}

2️⃣ VO → Entity

java 复制代码
BeanUtils.copyProperties(vo, entity);

结果:

java 复制代码
entity.name = vo.name;
entity.address = vo.address;

displayName 被忽略 ✅


3️⃣ Entity → VO

java 复制代码
BeanUtils.copyProperties(entity, vo);

结果:

java 复制代码
vo.displayName = null;

4️⃣ 原理

和 Jackson 一样:

按"字段名 + 类型"匹配,有就拷贝,没有就跳过


四、最危险的坑:null 覆盖问题

示例

java 复制代码
vo.name = null;
entity.name = "原值";

BeanUtils.copyProperties(vo, entity);

结果:

java 复制代码
entity.name = null ❌

正确做法

忽略 null 字段:

java 复制代码
BeanUtils.copyProperties(vo, entity, getNullPropertyNames(vo));

工具方法:

java 复制代码
public static String[] getNullPropertyNames(Object source) {
    BeanWrapper src = new BeanWrapperImpl(source);
    return Arrays.stream(src.getPropertyDescriptors())
        .map(PropertyDescriptor::getName)
        .filter(name -> src.getPropertyValue(name) == null)
        .toArray(String[]::new);
}

五、反序列化 vs 对象拷贝

场景 本质
JSON → VO Jackson 按字段填充
VO → Entity BeanUtils 按字段拷贝

本质完全一致:

只处理"匹配的字段"


六、实践中的常用方式

1. 分层设计

对象
Controller VO
Service DTO
DAO Entity

2. 所有字段用包装类型

java 复制代码
Integer / Long / Boolean

3. 更新接口必须防 null 覆盖

java 复制代码
if (vo.getName() != null) {
    entity.setName(vo.getName());
}

4. 明确三种状态

含义
null 前端没传
有值 前端传了
默认值 系统赋值

总之,Jackson & BeanUtils 的核心思想是 "按已有数据填充对象,而不是校验对象完整性" ,也就是说, 字段可以不传,对象可以不完全一致,系统只会处理"匹配上的部分",但我们必须控制 null 和业务语义

相关推荐
qqxhb10 小时前
11|结构化输出:为什么 JSON 能让系统更稳定
json·ai编程·结构化·规范模板
大阿明10 小时前
Spring Boot(快速上手)
java·spring boot·后端
bearpping10 小时前
Java进阶,时间与日期,包装类,正则表达式
java
邵奈一10 小时前
清明纪念·时光信笺——项目运行指南
java·实战·项目
sunwenjian88611 小时前
Java进阶——IO 流
java·开发语言·python
sinat_2554878111 小时前
读者、作家 Java集合学习笔记
java·笔记·学习
皮皮林55111 小时前
如何画出一张优秀的架构图?(老鸟必备)
java
百锦再11 小时前
Java 并发编程进阶,从线程池、锁、AQS 到并发容器与性能调优全解析
java·开发语言·jvm·spring·kafka·tomcat·maven
森林猿11 小时前
java-modbus-读取-modbus4j
java·网络·python