浅谈 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 和业务语义

相关推荐
海兰19 分钟前
使用 Spring AI 打造企业级 RAG 知识库第二部分:AI 实战
java·人工智能·spring
历程里程碑36 分钟前
二叉树---二叉树的中序遍历
java·大数据·开发语言·elasticsearch·链表·搜索引擎·lua
小信丶1 小时前
Spring Cloud Stream EnableBinding注解详解:定义、应用场景与示例代码
java·spring boot·后端·spring
无限进步_1 小时前
【C++】验证回文字符串:高效算法详解与优化
java·开发语言·c++·git·算法·github·visual studio
亚历克斯神1 小时前
Spring Cloud 2026 架构演进
java·spring·微服务
七夜zippoe1 小时前
Spring Cloud与Dubbo架构哲学对决
java·spring cloud·架构·dubbo·配置中心
海派程序猿1 小时前
Spring Cloud Config拉取配置过慢导致服务启动延迟的优化技巧
java
阿维的博客日记1 小时前
为什么不逃逸代表不需要锁,JIT会直接删掉锁
java
William Dawson1 小时前
CAS的底层实现
java
九英里路1 小时前
cpp容器——string模拟实现
java·前端·数据结构·c++·算法·容器·字符串