API 风格
这一课你将能做到
- 对比 REST、GraphQL、gRPC、WebSocket 四种 API 风格的优缺点,并给出选型建议
- 用 GraphQL 实现一个灵活查询的课程接口,理解 Schema 和 Resolver
- 用 WebSocket 实现实时通知,理解长连接与轮询的区别
为什么要学
REST 不是银弹。当「前端需要的数据嵌套很深」「移动端要省流量」「微服务之间要高性能通信」「需要实时推送」时,REST 要么啰嗦要么做不到。知道每种风格的适用场景,才能在该用 REST 时用 REST,该换技术时敢换。
先建立图像
四种风格对比:
┌──────────┬─────────────┬─────────────┬─────────────┬─────────────┐
│ │ REST │ GraphQL │ gRPC │ WebSocket │
├──────────┼─────────────┼─────────────┼─────────────┼─────────────┤
│ 协议 │ HTTP/1.1 │ HTTP/1.1 │ HTTP/2 │ TCP 长连接 │
│ 数据格式 │ JSON │ JSON │ Protobuf │ 自定义 │
│ 契约 │ 弱(靠文档) │ 强(Schema)│ 强(.proto)│ 弱 │
│ 查询方式 │ 固定端点 │ 客户端指定 │ 固定方法 │ 事件驱动 │
│ 实时性 │ 轮询/SSE │ 轮询/订阅 │ 流式 │ 原生实时 │
│ 适用场景 │ 通用 CRUD │ 复杂查询 │ 微服务内部 │ 实时通信 │
└──────────┴─────────────┴─────────────┴─────────────┴─────────────┘
跟着做
步骤 1:REST 的局限
场景:前端需要「课程详情 + 讲师信息 + 最新 5 条评价」
REST 方案(3 次请求):
curl /api/lessons/123
curl /api/instructors/456
curl /api/lessons/123/reviews?limit=5
或者做一个聚合端点(后端耦合):
curl /api/lessons/123/detail
问题:要么多次请求(慢),要么后端为每个前端页面定制接口(耦合)。
步骤 2:GraphQL 方案
# Schema 定义
type Lesson {
id: ID!
title: String!
content: String!
instructor: Instructor!
reviews(first: Int): [Review!]!
}
type Instructor {
id: ID!
name: String!
avatar: String
}
type Review {
id: ID!
content: String!
rating: Int!
author: User!
}
type Query {
lesson(id: ID!): Lesson
}
# 客户端查询:一次请求拿所有数据
query {
lesson(id: "123") {
title
content
instructor {
name
avatar
}
reviews(first: 5) {
content
rating
author {
name
}
}
}
}
// Spring Boot + GraphQL
@Controller
public class LessonGraphQLController {
@QueryMapping
public Lesson lesson(@Argument String id) {
return lessonService.findById(Long.parseLong(id));
}
@SchemaMapping
public Instructor instructor(Lesson lesson) {
return instructorService.findById(lesson.getInstructorId());
}
@SchemaMapping
public List<Review> reviews(Lesson lesson, @Argument Integer first) {
return reviewService.findByLessonId(lesson.getId(), first);
}
}
步骤 3:gRPC 方案
// lesson.proto
syntax = "proto3";
service LessonService {
rpc GetLesson(LessonRequest) returns (LessonResponse);
rpc ListLessons(ListLessonsRequest) returns (stream LessonResponse);
}
message LessonRequest {
int64 id = 1;
}
message LessonResponse {
int64 id = 1;
string title = 2;
string content = 3;
}
// 服务端
@GrpcService
public class LessonGrpcService extends LessonServiceGrpc.LessonServiceImplBase {
@Override
public void getLesson(LessonRequest request, StreamObserver<LessonResponse> responseObserver) {
Lesson lesson = lessonService.findById(request.getId());
LessonResponse response = LessonResponse.newBuilder()
.setId(lesson.getId())
.setTitle(lesson.getTitle())
.setContent(lesson.getContent())
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
gRPC 优势:HTTP/2 多路复用、Protobuf 二进制序列化(比 JSON 小 3-10 倍)、强类型契约。适合微服务之间的高频调用。
步骤 4:WebSocket 实时通知
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new NotificationHandler(), "/ws/notifications")
.setAllowedOrigins("*");
}
}
@Component
public class NotificationHandler extends TextWebSocketHandler {
private static final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String userId = getUserId(session);
sessions.put(userId, session);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
// 处理客户端消息
}
public void sendNotification(String userId, String content) {
WebSocketSession session = sessions.get(userId);
if (session != null && session.isOpen()) {
try {
session.sendMessage(new TextMessage(content));
} catch (IOException e) {
sessions.remove(userId);
}
}
}
}
// 前端
const ws = new WebSocket('ws://localhost:8080/ws/notifications');
ws.onmessage = (event) => {
const notification = JSON.parse(event.data);
showToast(notification.content);
};
讲清楚要点
- REST 是默认选择:团队熟悉、生态成熟、缓存友好、调试方便。除非有明确痛点,不要盲目换 GraphQL 或 gRPC。
- GraphQL 解决「数据聚合」和「版本演进」:客户端决定要什么数据,一个请求替代多个 REST 调用;新增字段不需要改 API 版本。但缓存复杂、权限控制难、N+1 查询要手动优化。
- gRPC 是微服务内部的王者:Protobuf 高效、HTTP/2 流式、代码生成完善。但不适合浏览器直接调用(需要 grpc-web),调试不如 REST 直观。
- WebSocket 是实时场景的唯一解:聊天、协作编辑、实时行情。但连接管理复杂(心跳、重连、消息确认),水平扩展需要 Redis Pub/Sub 或消息队列做广播。
- 混合使用是常态:对外公开 API 用 REST,复杂数据聚合用 GraphQL,微服务内部用 gRPC,实时通知用 WebSocket。没有一种风格能包打天下。
你可能会懵的地方
-
GraphQL 会不会取代 REST?
不会。GraphQL 适合「前端驱动」的场景(如 Facebook 的 News Feed),REST 适合「后端驱动」的开放 API(如 Stripe、GitHub)。两者会长期共存。
-
gRPC 和 REST 性能差多少?
Protobuf 序列化比 JSON 快 5-10 倍,体积小 3-10 倍;HTTP/2 多路复用减少连接开销。内网高频调用(如每秒上万次)gRPC 优势明显,低频外部调用 REST 足够。
-
WebSocket 和 SSE(Server-Sent Events)怎么选?
SSE 是单向(服务器→客户端),基于 HTTP,自动重连,适合「服务器推消息」场景(如股票行情);WebSocket 是双向,需要额外协议处理,适合「双向交互」(如聊天)。SSE 更简单,优先用 SSE。
-
GraphQL 怎么做权限控制?
字段级权限:在 Resolver 里判断当前用户能否访问该字段;查询深度限制:防止恶意嵌套查询;查询成本分析:限制单次查询的复杂度。
-
tRPC 是什么?
tRPC 是 TypeScript 生态的 RPC 方案,类型安全从后端到前端。适合全栈 TypeScript 项目,但生态和跨语言能力不如 gRPC。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 为了用 GraphQL 而用 GraphQL | 团队学习成本高,简单 CRUD 用 REST 更快 | 明确痛点(多次请求、版本演进)再引入 |
| gRPC 直接暴露给浏览器 | 浏览器不支持 HTTP/2 trailers | 用 grpc-web 或网关转 REST |
| WebSocket 不做心跳检测 | 代理/NAT 超时断开,客户端不知道 | 定时 ping/pong,30-60 秒一次 |
| GraphQL 不做深度限制 | 恶意嵌套查询拖垮数据库 | 限制查询深度(如 10 层)和复杂度 |
卡点急救
- GraphQL N+1 问题:
DataLoader 批量加载。比如 10 门课程各查讲师,DataLoader 会合并成一次 SQL WHERE id IN (...)。
- WebSocket 连接数瓶颈:单机支撑连接数有限(约 6 万)。用 Redis 做 Session 共享,或引入专门的长连接网关(如 Socket.IO、Netty)。
- gRPC 调试困难:用 BloomRPC 或 Postman(支持 gRPC)测试;开启 gRPC 反射(Reflection)服务,让工具能动态获取 proto。
面试常问
-
REST、GraphQL、gRPC 怎么选?
对外公开 API、简单 CRUD → REST;复杂数据聚合、前端驱动 → GraphQL;微服务内部高频调用 → gRPC;实时双向通信 → WebSocket。能讲出每种风格的 2-3 个优缺点。
-
GraphQL 的 N+1 问题怎么解决?
DataLoader 批量加载、缓存。能写出 DataLoader 的使用代码。
-
WebSocket 怎么做水平扩展?
多台服务器共享 Session(Redis),消息广播走消息队列(Kafka/RabbitMQ)。能画出架构图。
自测三问
- 设计一个「实时协作代码编辑器」的 API,选什么风格?为什么?
- 如果 GraphQL 查询导致数据库 CPU 100%,可能是什么原因?怎么排查?
- 为什么 gRPC 不适合直接暴露给前端?
小结一句话
API 风格没有银弹:REST 通用、GraphQL 灵活、gRPC 高效、WebSocket 实时;按场景选型,混合使用是常态。
延伸