点击上方“程序员蜗牛g”,选择“设为星标”

跟蜗牛哥一起,每天进步一点点

程序员蜗牛g

大厂程序员一枚 跟蜗牛一起 每天进步一点点

33篇原创内容

公众号

在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: "zhang@qq.com", 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": "zhang@example.com",
    "phone": "13800138000",
    "address": "北京市朝阳区",
    "avatar": "http://example.com/avatar1.jpg"
}

调用管理员接口:

GET /api/admin/users/1

响应结果:

{
    "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. 多字段组合视图

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是一个强大但被低估的功能,它能够:

  • • 减少DTO类数量: 从N个DTO合并为1个DTO

  • • 降低维护成本: 字段变更时只需修改一处

  • • 提高代码可读性: 视图名称直观,用途明确

  • • 保持灵活性: 通过视图组合满足复杂业务需求

适用场景

  • • 同一实体在不同接口中需要返回不同字段

  • • 需要区分用户权限看到不同数据

  • • API版本升级时需要渐进式暴露字段

不适用场景

  • • 字段差异极大,无法通过视图合理组织

  • • 需要复杂的字段转换逻辑(此时建议使用专门的DTO)

通过合理使用Jackson Views,我们可以构建出更加简洁、高效、易维护的API接口,告别DTO爆炸的困扰。

如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。

关注公众号:woniuxgg,在公众号中回复:笔记  就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐