前言
在API开发中,你是否遇到过这样的困扰:
- 列表页只需要用户的id和name
- 详情页需要显示用户的所有字段
- 管理员页面需要看到敏感信息
于是你开始创建各种DTO:
java
UserSummaryDTO、UserDetailDTO、UserAdminDTO...
最终导致DTO类"爆炸",代码维护成本激增。
今天分享一个被90%开发者忽略的Jackson"神技"------Jackson Views,用1个DTO + 注解,优雅解决API响应数据的多场景展示问题。
痛点场景
让我们先看一个典型的业务场景:
1. 用户实体类
java
@Entity
public class User {
private Long id;
private String username;
private String email;
private String phone;
private String address;
private String avatar;
private LocalDateTime createTime;
private LocalDateTime updateTime;
// getter/setter省略...
}
2. 多种API需求
列表页API:只需要id和username
java
GET /api/users
// 期望返回:[{id: 1, username: "张三"}, {id: 2, username: "李四"}]
详情页API:需要完整信息(除了敏感字段)
java
GET /api/users/{id}
// 期望返回:{id: 1, username: "张三", email: "zhang@qq.com", phone: "13800138000", ...}
管理员API:需要所有字段包括敏感信息
java
GET /api/admin/users/{id}
// 期望返回:所有字段信息
3. 传统解决方案的弊端
很多开发者会这样做:
java
// 摘要DTO
public class UserSummaryDTO {
private Long id;
private String username;
}
// 详情DTO
public class UserDetailDTO {
private Long id;
private String username;
private String email;
private String phone;
private String address;
private String avatar;
private LocalDateTime createTime;
}
// 管理员DTO
public class UserAdminDTO {
private Long id;
private String username;
private String email;
private String phone;
private String address;
private String avatar;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
问题分析:
- DTO类数量爆炸式增长
- 代码重复率高,维护成本大
- 字段变更时需要同步修改多个DTO
- 项目结构臃肿,可读性下降
Jackson Views解决方案
Jackson Views提供了一种优雅的解决方案:通过视图接口和注解,控制JSON序列化时包含哪些字段。
1. 定义视图接口
java
public class Views {
// 公共基础视图
public interface Public {}
// 摘要视图(继承Public)
public interface Summary extends Public {}
// 详情视图(继承Summary)
public interface Detail extends Summary {}
// 管理员视图(继承Detail)
public interface Admin extends Detail {}
}
2. 在DTO中使用@JsonView注解
java
public class UserDTO {
@JsonView(Views.Public.class)
private Long id;
@JsonView(Views.Summary.class)
private String username;
@JsonView(Views.Detail.class)
private String email;
@JsonView(Views.Detail.class)
private String phone;
@JsonView(Views.Detail.class)
private String address;
@JsonView(Views.Detail.class)
private String avatar;
@JsonView(Views.Admin.class)
private LocalDateTime updateTime;
@JsonView(Views.Admin.class)
private String internalNote; // 管理员专用字段
// getter/setter省略...
}
3. 在Controller中指定视图
java
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
private UserService userService;
// 列表页 - 只返回基础信息
@GetMapping("/users")
@JsonView(Views.Summary.class)
public List<UserDTO> getUserList() {
return userService.getAllUsers();
}
// 详情页 - 返回详细信息
@GetMapping("/users/{id}")
@JsonView(Views.Detail.class)
public UserDTO getUserDetail(@PathVariable Long id) {
return userService.getUserById(id);
}
// 管理员接口 - 返回所有信息
@GetMapping("/admin/users/{id}")
@JsonView(Views.Admin.class)
public UserDTO getUserForAdmin(@PathVariable Long id) {
return userService.getUserById(id);
}
}
4. 效果演示
调用列表页接口:
bash
GET /api/users
响应结果:
json
[
{
"id": 1,
"username": "张三"
},
{
"id": 2,
"username": "李四"
}
]
调用详情页接口:
bash
GET /api/users/1
响应结果:
json
{
"id": 1,
"username": "张三",
"email": "zhang@example.com",
"phone": "13800138000",
"address": "北京市朝阳区",
"avatar": "http://example.com/avatar1.jpg"
}
调用管理员接口:
bash
GET /api/admin/users/1
响应结果:
json
{
"id": 1,
"username": "张三",
"email": "zhang@example.com",
"phone": "13800138000",
"address": "北京市朝阳区",
"avatar": "http://example.com/avatar1.jpg",
"updateTime": "2024-01-15T10:30:00",
"internalNote": "VIP用户,需要重点关注"
}
高级用法
1. 多字段组合视图
java
public class UserDTO {
// 基础信息
@JsonView(Views.Basic.class)
private Long id;
@JsonView(Views.Basic.class)
private String username;
// 联系信息
@JsonView(Views.Contact.class)
private String email;
@JsonView(Views.Contact.class)
private String phone;
// 统计信息
@JsonView(Views.Statistics.class)
private Integer loginCount;
@JsonView(Views.Statistics.class)
private LocalDateTime lastLoginTime;
// 敏感信息
@JsonView(Views.Sensitive.class)
private String realName;
@JsonView(Views.Sensitive.class)
private String idCard;
}
2. 组合视图使用
java
// 基础信息 + 联系信息
public interface BasicContact extends Views.Basic, Views.Contact {}
// 统计信息 + 敏感信息
public interface FullStats extends Views.Statistics, Views.Sensitive {}
@GetMapping("/users/contact")
@JsonView(Views.BasicContact.class)
public UserDTO getUserWithContact(@PathVariable Long id) {
return userService.getUserById(id);
}
3. 动态视图选择
java
@GetMapping("/users/{id}")
public ResponseEntity<UserDTO> getUser(
@PathVariable Long id,
@RequestParam(defaultValue = "summary") String view) {
UserDTO user = userService.getUserById(id);
// 根据参数动态选择视图
Class<?> viewClass = switch (view.toLowerCase()) {
case "detail" -> Views.Detail.class;
case "admin" -> Views.Admin.class;
default -> Views.Summary.class;
};
return ResponseEntity.ok().body(user);
}
最佳实践
1. 视图设计原则
- 继承优于平级:使用视图继承关系,避免重复定义
- 粒度适中:视图粒度既不能太细(导致过多视图类),也不能太粗(失去灵活性)
- 命名清晰:视图名称要能清晰表达其用途
2. 常用视图模板
java
public class CommonViews {
// 公共接口
public interface Public {}
// 内部接口
public interface Internal extends Public {}
// 管理员接口
public interface Admin extends Internal {}
// 摘要信息
public interface Summary extends Public {}
// 详情信息
public interface Detail extends Summary {}
// 完整信息
public interface Full extends Detail {}
// 导出数据
public interface Export extends Full {}
}
3. 避免常见陷阱
❌ 错误做法:
java
// 视图层级过深,增加维护复杂度
public interface A extends B {}
public interface B extends C {}
public interface C extends D {}
public interface D extends E {}
✅ 正确做法:
java
// 视图层级保持在3层以内
public interface Public {}
public interface Summary extends Public {}
public interface Detail extends Summary {}
4. 与其他注解的配合
java
public class UserDTO {
@JsonView(Views.Summary.class)
@JsonProperty("user_id") // 自定义JSON字段名
private Long id;
@JsonView(Views.Detail.class)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") // 日期格式化
private LocalDateTime createTime;
@JsonView(Views.Admin.class)
@JsonIgnore // 在某些视图中忽略字段
private String sensitiveData;
}
总结
Jackson Views是一个强大但被低估的功能,它能够:
- 减少DTO类数量:从N个DTO合并为1个DTO
- 降低维护成本:字段变更时只需修改一处
- 提高代码可读性:视图名称直观,用途明确
- 保持灵活性:通过视图组合满足复杂业务需求
适用场景:
- 同一实体在不同接口中需要返回不同字段
- 需要区分用户权限看到不同数据
- API版本升级时需要渐进式暴露字段
不适用场景:
- 字段差异极大,无法通过视图合理组织
- 需要复杂的字段转换逻辑(此时建议使用专门的DTO)
通过合理使用Jackson Views,我们可以构建出更加简洁、高效、易维护的API接口,告别DTO爆炸的困扰。