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