在API開發中,你是否遇到過這樣的困擾:
於是你開始創建各種DTO:
UserSummaryDTO、UserDetailDTO、UserAdminDTO...
最終導致DTO類"爆炸",程式碼維護成本激增。
今天分享一個被90%開發者忽略的Jackson"神技"——Jackson Views,用1個DTO + 註解,優雅解決API響應資料的多場景展示問題。
讓我們先看一個典型的業務場景:
@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省略...
}
列表頁API:只需要id和username
GET /api/users
// 期望返回:[{id: 1, username: "張三"}, {id: 2, username: "李四"}]
詳情頁API:需要完整資訊(除了敏感字段)
GET /api/users/{id}
// 期望返回:{id: 1, username: "張三", email: "[email protected]", phone: "13800138000", ...}
管理員API:需要所有字段包括敏感信息
GET /api/admin/users/{id}
// 期望返回:所有字段信息
很多開發者會這樣做:
// 摘要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;
}
問題分析:
Jackson Views提供了一種優雅的解決方案:通過視圖介面和註解,控制JSON序列化時包含哪些字段。
public class Views {
// 公共基礎視圖
public interface Public {}
// 摘要視圖(繼承Public)
public interface Summary extends Public {}
// 詳情視圖(繼承Summary)
public interface Detail extends Summary {}
// 管理員視圖(繼承Detail)
public interface Admin extends Detail {}
}
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省略...
}
@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);
}
}
調用列表頁接口:
GET /api/users
響應結果:
[
{
"id": 1,
"username": "張三"
},
{
"id": 2,
"username": "李四"
}
]
調用詳情頁接口:
GET /api/users/1
響應結果:
{
"id": 1,
"username": "張三",
"email": "[email protected]",
"phone": "13800138000",
"address": "北京市朝陽區",
"avatar": "http://example.com/avatar1.jpg"
}
調用管理員接口:
GET /api/admin/users/1
響應結果:
{
"id": 1,
"username": "張三",
"email": "[email protected]",
"phone": "13800138000",
"address": "北京市朝陽區",
"avatar": "http://example.com/avatar1.jpg",
"updateTime": "2024-01-15T10:30:00",
"internalNote": "VIP用戶,需要重點關注"
}
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;
}
// 基礎信息 + 聯絡信息
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);
}
@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);
}
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 {}
}
❌ 錯誤做法:
// 視圖層級過深,增加維護複雜度
public interface A extends B {}
public interface B extends C {}
public interface C extends D {}
public interface D extends E {}
✅ 正確做法:
// 視圖層級保持在3層以內
public interface Public {}
public interface Summary extends Public {}
public interface Detail extends Summary {}
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是一個強大但被低估的功能,它能夠:
適用場景:
不適用場景:
通過合理使用Jackson Views,我們可以構建出更加簡潔、高效、易維護的API接口,告別DTO爆炸的困擾。