第 8 章 · BFF 聚合与 API 编排
本章目标:理解 BFF(Backend for Frontend) 在微服务架构中的定位;在 svc-spring-demo 新增 bff-svc,聚合订单详情(订单 + 商品 + 用户)为单一响应;掌握 并行 Feign 调用、部分失败降级与超时预算;对比 BFF 与 API Gateway 聚合、GraphQL 的适用场景;为移动端与 Web SPA 提供差异化接口(概念)。
学时建议:5~6 小时(含 2 小时 bff-svc 联调)
前置:spring-cloud-web ch01~ch07;spring-boot-web ch07 统一响应;OpenFeign 与 Resilience4j 熔断(ch04~ch05)。
8.1 为什么需要 BFF
ch03 gateway-svc 负责路由、鉴权、限流,但不应承载复杂业务聚合逻辑。前端若自行串联多个 API:
SPA 加载「订单详情页」
GET /api/v1/orders/1001 → order-svc
GET /api/v1/products/42 → product-svc(从订单项解析)
GET /api/v1/users/7 → user-svc
| 问题 | BFF 方案 |
|---|---|
| 多次往返、瀑布请求 | 一次 GET /bff/v1/orders/1001/detail |
| 前端耦合微服务边界 | 聚合逻辑收敛到服务端 |
| 移动端/Web 字段差异 | 不同 BFF 面向不同客户端(选修) |
| 错误处理分散 | BFF 统一降级与错误码 |
Web SPA / 移动端
│
▼
┌─────────────────┐ ┌──────────────────┐
│ gateway-svc │────►│ bff-svc │
│ api.example.com│ │ 聚合编排 │
└────────┬────────┘ └────────┬─────────┘
│ │ 并行 Feign
│ ┌────────┼────────┐
│ ▼ ▼ ▼
│ order-svc product-svc user-svc
└────────► 其他直连路由(非聚合 API)
虚构 bff-svc、api.example.com;BFF 仍注册 Nacos,经 Gateway 暴露 /bff/** 前缀。
8.2 BFF vs Gateway vs GraphQL
| 模式 | 职责 | 优点 | 缺点 |
|---|---|---|---|
| Gateway 路由 | 转发、鉴权、限流 | 无状态、高性能 | 不宜写业务聚合 |
| BFF | 面向前端的 API 编排 | 减少客户端复杂度 | 多客户端可能多 BFF |
| GraphQL | 客户端自选字段 | 灵活查询 | 服务端 N+1、缓存复杂 |
教学选型:本课程用 Spring MVC + Feign 实现 BFF,与现有技术栈一致;GraphQL 作为了解即可。
8.3 创建 bff-svc 模块
8.3.1 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
8.3.2 启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.example.svc.bff.client")
public class BffSvcApplication {
public static void main(String[] args) {
SpringApplication.run(BffSvcApplication.class, args);
}
}
application.yml:
spring:
application:
name: bff-svc
server:
port: 8095
8.4 聚合 DTO 设计
在 svc-common 新增面向前端的视图对象,避免泄露各服务内部实体:
public record OrderDetailVO(
Long orderId,
String status,
BigDecimal totalAmount,
Instant createdAt,
UserBriefVO buyer,
List<OrderLineDetailVO> lines
) {}
public record OrderLineDetailVO(
Long productId,
String productName,
String coverUrl,
int quantity,
BigDecimal unitPrice
) {}
public record UserBriefVO(Long id, String nickname, String avatarUrl) {}
| 原则 | 说明 |
|---|---|
| 只暴露前端需要的字段 | 不含密码哈希、内部状态码 |
| 命名面向 UI | coverUrl 而非 productImagePath |
| 版本独立 | BFF VO 变更不影响领域服务 DTO |
8.5 Feign 客户端复用
@FeignClient(name = "order-svc", path = "/api/v1/orders")
public interface OrderBffClient {
@GetMapping("/{id}")
Result<OrderDTO> getById(@PathVariable Long id);
}
@FeignClient(name = "product-svc", path = "/api/v1/products")
public interface ProductBffClient {
@GetMapping("/{id}")
Result<ProductDTO> getById(@PathVariable Long id);
}
@FeignClient(name = "user-svc", path = "/api/v1/users")
public interface UserBffClient {
@GetMapping("/{id}")
Result<UserDTO> getById(@PathVariable Long id);
}
BFF 层只读聚合为主;写操作仍建议前端或 Gateway 直连 order-svc(避免 BFF 成为写路径单点)。
8.6 并行聚合服务
使用 CompletableFuture 控制超时预算(教学默认 3 秒):
@Service
@RequiredArgsConstructor
public class OrderDetailAggregator {
private final OrderBffClient orderClient;
private final ProductBffClient productClient;
private final UserBffClient userClient;
@Qualifier("bffExecutor")
private final Executor bffExecutor;
public OrderDetailVO aggregate(Long orderId) {
Result<OrderDTO> orderResult = orderClient.getById(orderId);
if (!orderResult.isSuccess()) {
throw new BffException("ORDER_NOT_FOUND", "订单不存在");