🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

Jackson視圖神技:一個DTO幹掉N個DTO,告別DTO爆炸問題

前言

在API開發中,你是否遇到過這樣的困擾:

  • 列表頁只需要用戶的id和name
  • 詳情頁需要顯示用戶的所有字段
  • 管理員頁面需要看到敏感信息

於是你開始創建各種DTO:

UserSummaryDTO、UserDetailDTO、UserAdminDTO...

最終導致DTO類"爆炸",程式碼維護成本激增。

今天分享一個被90%開發者忽略的Jackson"神技"——Jackson Views,用1個DTO + 註解,優雅解決API響應資料的多場景展示問題。

痛點場景

讓我們先看一個典型的業務場景:

1. 用戶實體類

@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

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}
// 期望返回:所有字段信息

3. 傳統解決方案的弊端

很多開發者會這樣做:

// 摘要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. 定義視圖介面

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註解

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中指定視圖

@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. 效果演示

調用列表頁接口

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用戶,需要重點關注"
}

高級用法

1. 多字段組合視圖

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. 組合視圖使用

// 基礎信息 + 聯絡信息
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. 動態視圖選擇

@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. 常用視圖模板

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. 避免常見陷阱

❌ 錯誤做法

// 視圖層級過深,增加維護複雜度
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 {}

4. 與其他註解的配合

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是一個強大但被低估的功能,它能夠:

  1. 減少DTO類數量:從N個DTO合併為1個DTO
  2. 降低維護成本:字段變更時只需修改一處
  3. 提高程式碼可讀性:視圖名稱直觀,用途明確
  4. 保持靈活性:通過視圖組合滿足複雜業務需求

適用場景

  • 同一實體在不同接口中需要返回不同字段
  • 需要區分用戶權限看到不同資料
  • API版本升級時需要漸進式暴露字段

不適用場景

  • 字段差異極大,無法通過視圖合理組織
  • 需要複雜的字段轉換邏輯(此時建議使用專門的DTO)

通過合理使用Jackson Views,我們可以構建出更加簡潔、高效、易維護的API接口,告別DTO爆炸的困擾。


原文出處:https://juejin.cn/post/7569037914342096932


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝11   💬5   ❤️1
249
🥈
我愛JS
📝1   💬3   ❤️3
47
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付