接口颗粒度设计方法
总结
- 接口颗粒度核心问题:不同调用方需要的字段组合各不相同,怎么设计不接口爆炸
- 三种方案:稀疏字段集(改造成本低)、BFF(多端差异大)、GraphQL(字段组合复杂)
- 不推荐"三粒度分级",边界模糊、DTO 重叠、调用方还是要猜
1. 先说结论
接口颗粒度的核心问题是:一个业务对象字段很多,不同调用方需要的字段组合各不相同,怎么设计才不会接口爆炸?
三种方案,按推荐程度排:
| 方案 | 适用场景 | 复杂度 |
|---|---|---|
稀疏字段集(?fields=) |
REST 风格,改造成本低 | 低 |
| BFF(Backend for Frontend) | 多端差异大,微服务架构 | 中 |
| GraphQL | 字段组合复杂,调用方多样 | 高 |
2. 稀疏字段集(Sparse Fieldsets)
REST 下最简单的折中方案。一个接口,调用方通过 fields 参数声明要哪些字段,服务端按需返回。
GET /orders/123?fields=orderId,status,amount
GET /orders/123?fields=orderId,status,amount,userName,userPhone,products
GET /orders/123 // 不传则返回完整数据
2.1 服务端怎么实现?
@GetMapping("/orders/{id}")
public OrderDTO getOrder(@PathVariable String id,
@RequestParam(required = false) Set<String> fields) {
OrderDTO order = orderService.getOrder(id);
if (fields == null || fields.isEmpty()) {
return order;
}
// 按 fields 过滤,只返回调用方需要的字段
return FieldFilter.filter(order, fields);
}
FieldFilter 可以用反射或 Jackson 的 @JsonFilter 实现:
// 用 Jackson FilterProvider 动态过滤字段
ObjectMapper mapper = new ObjectMapper();
FilterProvider filters = new SimpleFilterProvider()
.addFilter("fieldFilter", SimpleBeanPropertyFilter.filterOutAllExcept(fields));
mapper.writer(filters).writeValueAsString(order);
2.2 优缺点
优点:
- 一个接口搞定所有粒度,不用维护多个 DTO
- 调用方按需取,不过度获取数据
- 改造成本低,现有接口加个参数就行
缺点:
- 服务端仍然查了全量数据,只是返回时过滤了字段
- 如果要连 DB 查询也按需裁剪,需要额外处理(传 fields 到 Mapper 层)
- 调用方需要知道有哪些字段可以选
3. BFF(Backend for Frontend)
不同端(移动端、PC、第三方)各自有一个聚合层,按端的需求裁剪和聚合数据,核心服务只提供原子接口。
移动端 App → Mobile BFF ─┐
PC 管理后台 → Admin BFF ─┤→ 订单服务 / 用户服务 / 商品服务
第三方平台 → Open BFF ─┘
3.1 核心服务只做原子接口
// 订单服务:只返回订单自身的数据
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable String id);
// 用户服务:只返回用户数据
@GetMapping("/users/{id}")
public User getUser(@PathVariable String id);
3.2 BFF 层按端聚合
// Mobile BFF:移动端只需要简要信息
@GetMapping("/mobile/orders/{id}")
public MobileOrderVO getOrder(@PathVariable String id) {
Order order = orderClient.getOrder(id);
User user = userClient.getUser(order.getUserId());
// 只组装移动端需要的字段
return MobileOrderVO.builder()
.orderId(order.getId())
.status(order.getStatus())
.amount(order.getAmount())
.userName(user.getName())
.build();
}
// Admin BFF:后台需要完整信息
@GetMapping("/admin/orders/{id}")
public AdminOrderVO getOrder(@PathVariable String id) {
Order order = orderClient.getOrder(id);
User user = userClient.getUser(order.getUserId());
List<Product> products = productClient.getByOrderId(id);
List<Log> logs = logClient.getByOrderId(id);
return AdminOrderVO.assemble(order, user, products, logs);
}
3.3 优缺点
优点:
- 核心服务保持简单,职责单一
- 各端可以独立演进,互不影响
- 聚合逻辑集中在 BFF,方便维护
缺点:
- 多了一层服务,增加部署和运维成本
- BFF 容易变成"大泥球",什么逻辑都往里塞
- 团队需要明确 BFF 的边界:只做聚合裁剪,不做业务逻辑
4. GraphQL
让调用方自己声明要哪些字段,服务端按声明精确返回,从根本上解决字段组合爆炸的问题。
4.1 调用方按需查询
# 列表页:只要基本字段
query {
order(id: "123") {
orderId
status
amount
}
}
# 详情页:要更多字段
query {
order(id: "123") {
orderId
status
amount
user {
name
phone
}
products {
name
quantity
price
}
}
}
4.2 服务端定义 Schema
type Order {
orderId: ID!
status: String!
amount: Float!
createTime: String!
user: User
products: [Product]
paymentInfo: PaymentInfo
logs: [OrderLog]
}
type Query {
order(id: ID!): Order
orders(userId: ID, status: String): [Order]
}
4.3 Resolver 按需加载
@Component
public class OrderResolver implements GraphQLQueryResolver {
public Order order(String id) {
// 只查订单基本信息
return orderService.getOrder(id);
}
}
@Component
public class OrderFieldResolver implements GraphQLResolver<Order> {
// 只有调用方查询了 user 字段,这个方法才会被调用
public User user(Order order) {
return userService.getUser(order.getUserId());
}
// 只有查询了 products 字段才加载
public List<Product> products(Order order) {
return productService.getByOrderId(order.getId());
}
}
4.4 优缺点
优点:
- 调用方完全按需取数据,不多不少
- 一个端点替代大量 REST 接口
- 天然支持关联数据的按需加载
缺点:
- 学习成本高,团队需要时间适应
- 缓存比 REST 复杂(URL 不固定)
- 调试和监控比 REST 麻烦
- 不适合对外开放的公共 API
5. 怎么选?
用稀疏字段集:
- 现有 REST 项目,不想大改
- 调用方场景差异不大
- 快速解决过度获取问题
用 BFF:
- 多端(App、PC、小程序)需求差异明显
- 已经是微服务架构
- 团队能维护多个 BFF 服务
用 GraphQL:
- 调用方多样,字段组合复杂
- 新项目,团队愿意投入学习成本
- 内部系统,不对外暴露
不推荐"三粒度接口":
按字段数量切接口(粗/中/细)是一种工程妥协,边界模糊、DTO 重叠、调用方还是要猜该用哪个。如果场景固定、团队规模小,短期内可以用,但不是长期方案。