在SpringBoot开发中,曾被大量重复的DTO(数据传输对象)搞得焦头烂额?列表接口要精简字段(id、name),详情接口要完整信息(含email、phone),创建接口只接收必填项(name、age),更新接口又要排除部分字段(如createTime)—— 结果就是项目里堆满了UserListDTO、UserDetailDTO、UserCreateDTO、UserUpdateDTO,字段修改时要同步改N个类,维护成本直线上升。
解锁Jackson的「视图(View)」功能,用1个POJO+N个视图标记替代一堆冗余DTO,维护效率翻倍!本文将从核心原理、实操步骤、进阶技巧到避坑指南,带你彻底掌握这一高效技巧。
一、痛点直击:多DTO方案的致命问题
先看一个典型场景:用户模块需要4个接口,对应4个DTO:
- 列表接口(/users):返回
id、username、nickname - 详情接口(/users/{id}):返回
id、username、nickname、email、phone、createTime - 创建接口(/users):接收
username、nickname、email、password - 更新接口(/users/{id}):接收
nickname、email、phone
传统方案需要创建4个DTO类,每个类都有重复字段(如username、nickname),带来3大问题:
- 代码冗余:重复字段拷贝粘贴,项目体积臃肿;
- 维护成本高:字段更名/类型修改需同步所有DTO,易漏改;
- 开发效率低:新增接口需手动创建新DTO,重复劳动。
而Jackson视图的核心思想是:在同一个POJO中,用「视图标记」区分不同场景的字段,接口层指定使用的视图,实现字段按需序列化/反序列化。
二、什么是Jackson视图?
Jackson的@JsonView注解是实现视图功能的核心,它的工作流程如下:
- 定义「视图接口」(空接口,仅作为标记);
- 在POJO的字段/getter方法上用
@JsonView标记该字段所属的视图; - 在SpringMVC接口中,用
@JsonView指定当前接口使用的视图; - Jackson序列化/反序列化时,仅处理标记了该视图的字段。
简单说:视图就是「字段的分组标签」,接口指定标签后,只返回/接收该标签下的字段。
三、实操演示:1个POJO搞定4个接口
环境准备
SpringBoot默认集成Jackson,无需额外导入依赖(若未集成,添加以下依赖):
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
步骤1:定义视图接口
创建空接口作为视图标记,建议按「业务场景+功能」命名,便于维护:
/**
* 视图接口:仅作为标记,无任何逻辑
*/
public class UserViews {
// 列表视图:精简字段
public interface ListView {}
// 详情视图:继承列表视图(包含列表所有字段+额外字段)
public interface DetailView extends ListView {}
// 创建视图:接收创建用户的必填字段
public interface CreateView {}
// 更新视图:接收更新用户的可选字段
public interface UpdateView {}
}
⚠️ 关键技巧:视图支持继承!DetailView extends ListView表示详情视图包含列表视图的所有字段,无需重复标记。
步骤2:POJO中标记字段
创建一个User类,用@JsonView为每个字段标记所属视图:
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonView;
import lombok.Data;
import java.time.LocalDateTime;
@Data // Lombok简化getter/setter
public class User {
// 列表、详情、更新视图需要(创建时无需id)
@JsonView({UserViews.ListView.class, UserViews.UpdateView.class})
private Long id;
// 列表、详情、创建、更新视图都需要
@JsonView({UserViews.ListView.class, UserViews.CreateView.class, UserViews.UpdateView.class})
private String username;
// 列表、详情、创建、更新视图都需要
@JsonView({UserViews.ListView.class, UserViews.CreateView.class, UserViews.UpdateView.class})
private String nickname;
// 详情、创建、更新视图需要(列表不需要)
@JsonView({UserViews.DetailView.class, UserViews.CreateView.class, UserViews.UpdateView.class})
private String email;
// 详情、更新视图需要(列表、创建不需要)
@JsonView({UserViews.DetailView.class, UserViews.UpdateView.class})
private String phone;
// 创建视图需要(列表、详情、更新不需要)
@JsonView(UserViews.CreateView.class)
private String password;
// 详情视图需要(自动填充,无需前端传递)
@JsonView(UserViews.DetailView.class)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}
步骤3:Controller中指定视图
在接口方法上用@JsonView指定当前接口使用的视图,实现「字段按需返回/接收」:
import com.fasterxml.jackson.annotation.JsonView;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
/**
* 列表接口:使用ListView,仅返回id、username、nickname
*/
@GetMapping
@JsonView(UserViews.ListView.class)
public List<User> list() {
// 模拟数据库查询
User user1 = new User();
user1.setId(1L);
user1.setUsername("zhangsan");
user1.setNickname("张三");
user1.setEmail("zhangsan@xxx.com"); // 不在ListView,会被过滤
User user2 = new User();
user2.setId(2L);
user2.setUsername("lisi");
user2.setNickname("李四");
user2.setPhone("13800138000"); // 不在ListView,会被过滤
return Arrays.asList(user1, user2);
}
/**
* 详情接口:使用DetailView,返回所有字段(继承ListView+额外字段)
*/
@GetMapping("/{id}")
@JsonView(UserViews.DetailView.class)
public User detail(@PathVariable Long id) {
User user = new User();
user.setId(id);
user.setUsername("zhangsan");
user.setNickname("张三");
user.setEmail("zhangsan@xxx.com");
user.setPhone("13800138000");
user.setCreateTime(LocalDateTime.now());
return user;
}
/**
* 创建接口:使用CreateView,仅接收username、nickname、email、password
*/
@PostMapping
@JsonView(UserViews.CreateView.class)
public String create(@RequestBody @JsonView(UserViews.CreateView.class) User user) {
// 模拟保存数据库(忽略密码加密逻辑)
System.out.println("创建用户:" + user); // 仅能获取到CreateView标记的字段
return "创建成功,用户ID:" + (user.getUsername().hashCode() & 0x7FFFFFFF);
}
/**
* 更新接口:使用UpdateView,仅接收id、username、nickname、email、phone
*/
@PutMapping("/{id}")
@JsonView(UserViews.UpdateView.class)
public String update(@PathVariable Long id,
@RequestBody @JsonView(UserViews.UpdateView.class) User user) {
user.setId(id); // 确保ID一致
System.out.println("更新用户:" + user); // 仅能获取到UpdateView标记的字段
return "更新成功";
}
}
步骤4:测试验证
用Postman或curl测试4个接口,观察字段过滤效果:
1. 列表接口(/users)返回结果(仅3个字段):
[
{
"id": 1,
"username": "zhangsan",
"nickname": "张三"
},
{
"id": 2,
"username": "lisi",
"nickname": "李四"
}
]
2. 详情接口(/users/1)返回结果(所有字段):
{
"id": 1,
"username": "zhangsan",
"nickname": "张三",
"email": "zhangsan@xxx.com",
"phone": "13800138000",
"createTime": "2025-11-24 15:30:00"
}
3. 创建接口(/users)入参(仅接收4个字段):
{
"username": "wangwu",
"nickname": "王五",
"email": "wangwu@xxx.com",
"password": "123456"
}
👉 若传入phone或id,会被Jackson忽略,不会绑定到User对象。
4. 更新接口(/users/3)入参(仅接收5个字段):
{
"username": "wangwu",
"nickname": "王五666",
"email": "wangwu_new@xxx.com",
"phone": "13900139000"
}
👉 传入password会被忽略,id从路径参数获取并覆盖。
四、进阶技巧:让视图用得更灵活
1. 嵌套对象的视图控制
如果POJO包含嵌套对象(如User包含Address),可在嵌套对象中也使用视图标记,实现深层字段过滤:
// 地址视图接口
public class AddressViews {
public interface SimpleView {}
public interface FullView extends SimpleView {}
}
@Data
public class Address {
@JsonView(AddressViews.SimpleView.class)
private String province; // 简单视图:省份
@JsonView(AddressViews.SimpleView.class)
private String city; // 简单视图:城市
@JsonView(AddressViews.FullView.class)
private String detail; // 完整视图:详细地址
}
// User类中添加嵌套字段
@Data
public class User {
// ... 其他字段 ...
@JsonView({UserViews.DetailView.class})
private Address address; // 仅详情视图返回地址
}
此时访问用户详情接口,会返回Address的FullView字段(需确保Address的字段标记与User视图兼容)。
2. 视图与Jackson其他注解兼容
@JsonView可与@JsonIgnore、@JsonFormat、@JsonProperty等注解协同使用,例如:
@JsonIgnore:强制忽略字段,优先级高于@JsonView;@JsonFormat:格式化日期/数字(如上文的createTime);@JsonProperty:自定义JSON字段名(如username序列化后为user_name)。
3. 全局配置与自定义ObjectMapper
若需全局调整视图序列化规则(如忽略空值),可自定义ObjectMapper:
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// 忽略空值字段(所有视图生效)
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 支持Java 8日期时间类型
objectMapper.registerModule(new JavaTimeModule());
return objectMapper;
}
}
五、方案对比:代码量直降80%
以本文的用户模块为例,对比传统多DTO方案与Jackson视图方案:
| 对比维度 | 传统多DTO方案 | Jackson视图方案 |
|---|---|---|
| 类数量 | 4个DTO + 1个POJO = 5个类 | 1个POJO + 2个视图接口类 = 3个类 |
| 代码行数 | 每个DTO约10行,共40行+POJO20行=60行 | POJO25行 + 视图接口10行=35行 |
| 维护成本 | 字段修改需同步4个DTO | 字段修改仅需改1个POJO |
| 开发效率 | 新增接口需创建新DTO | 新增接口仅需添加视图标记 |
| 字段冗余度 | 高(重复字段拷贝) | 无(统一维护在POJO) |
👉 实际项目中,若接口数量达10个,传统方案需10个DTO,Jackson视图方案仅需1个POJO+若干视图接口,代码量可从100行降至20行,直降80%!
六、注意事项:避坑指南
- 视图命名规范:视图接口建议按「业务+场景」命名(如
OrderViews.ListView),避免全局混乱; - 避免过度使用:若不同场景的字段差异极大(如A场景需5个字段,B场景需6个完全不重叠字段),建议单独创建DTO,否则POJO会过于臃肿;
- 反序列化注意事项:
@JsonView用于@RequestBody时,仅绑定标记字段,未标记字段会被设为null,需注意非空校验; - 继承视图的顺序:若字段标记多个视图,且视图存在继承关系,序列化时会包含所有父视图字段(如
DetailView包含ListView字段); - Swagger文档适配:若使用Swagger生成接口文档,需额外配置
@JsonView支持(如springdoc-openapi需添加jackson-module-jaxb-annotations依赖)。
七、总结
Jackson视图是SpringBoot开发中「减少冗余DTO、提升维护效率」的神器,其核心价值在于:
- 「1个POJO适配多场景」:用标记替代重复类,代码量大幅下降;
- 「字段统一维护」:修改字段仅需改1处,避免漏改风险;
- 「灵活扩展」:支持视图继承、嵌套对象、与其他Jackson注解兼容。
当你的项目中出现大量重复字段的DTO时,不妨试试Jackson视图—— 少写N个类,让代码更优雅,开发更高效!
1364

被折叠的 条评论
为什么被折叠?



