本文目录导读:

我将为您提供一个完整的Java BFF(Backend For Frontend)案例,BFF模式是微服务架构中专门为前端提供服务的后端层,它聚合多个后端服务并为特定前端定制API。
项目结构
bff-demo/
├── pom.xml
├── src/main/java/com/example/bff/
│ ├── BffApplication.java
│ ├── config/
│ │ └── WebClientConfig.java
│ ├── controller/
│ │ └── BFFController.java
│ ├── service/
│ │ ├── BFFService.java
│ │ └── DownstreamServiceClient.java
│ ├── dto/
│ │ ├── UserProfileDTO.java
│ │ ├── OrderDTO.java
│ │ └── DashboardDTO.java
│ └── exception/
│ └── GlobalExceptionHandler.java
└── src/main/resources/
└── application.yml
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>bff-demo</artifactId>
<version>1.0.0</version>
<name>bff-demo</name>
<description>BFF Pattern Implementation Example</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- WebFlux for WebClient -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
主应用类
package com.example.bff;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class BffApplication {
public static void main(String[] args) {
SpringApplication.run(BffApplication.class, args);
}
}
配置文件
application.yml
server:
port: 8080
spring:
application:
name: bff-service
# 配置下游服务地址
services:
user-service:
base-url: http://localhost:8081
order-service:
base-url: http://localhost:8082
product-service:
base-url: http://localhost:8083
management:
endpoints:
web:
exposure:
include: health,info,metrics
logging:
level:
com.example.bff: DEBUG
WebClient配置
package com.example.bff.config;
import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@Configuration
public class WebClientConfig {
@Value("${services.user-service.base-url}")
private String userServiceUrl;
@Value("${services.order-service.base-url}")
private String orderServiceUrl;
@Value("${services.product-service.base-url}")
private String productServiceUrl;
@Bean
public WebClient userServiceWebClient() {
return createWebClient(userServiceUrl);
}
@Bean
public WebClient orderServiceWebClient() {
return createWebClient(orderServiceUrl);
}
@Bean
public WebClient productServiceWebClient() {
return createWebClient(productServiceUrl);
}
private WebClient createWebClient(String baseUrl) {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofMillis(5000))
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));
return WebClient.builder()
.baseUrl(baseUrl)
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
}
DTO类
package com.example.bff.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserProfileDTO {
private String userId;
private String username;
private String email;
private String phone;
private String avatar;
private String role;
}
package com.example.bff.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderDTO {
private String orderId;
private String userId;
private List<OrderItemDTO> items;
private BigDecimal totalAmount;
private String status;
private LocalDateTime createTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class OrderItemDTO {
private String productId;
private String productName;
private Integer quantity;
private BigDecimal price;
}
}
package com.example.bff.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DashboardDTO {
private UserProfileDTO userProfile;
private List<OrderDTO> recentOrders;
private List<ProductDTO> recommendedProducts;
private BigDecimal totalSpent;
private int orderCount;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ProductDTO {
private String productId;
private String productName;
private String description;
private BigDecimal price;
private int stock;
}
}
服务层
package com.example.bff.service;
import com.example.bff.dto.DashboardDTO;
import com.example.bff.dto.OrderDTO;
import com.example.bff.dto.UserProfileDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Service
@Slf4j
@RequiredArgsConstructor
public class BFFService {
@Qualifier("userServiceWebClient")
private final WebClient userServiceWebClient;
@Qualifier("orderServiceWebClient")
private final WebClient orderServiceWebClient;
@Qualifier("productServiceWebClient")
private final WebClient productServiceWebClient;
/**
* 获取用户仪表盘数据(聚合多个服务)
*/
public Mono<DashboardDTO> getDashboardData(String userId) {
log.info("Fetching dashboard data for user: {}", userId);
// 并行调用多个服务
Mono<UserProfileDTO> userProfileMono = fetchUserProfile(userId)
.timeout(Duration.ofSeconds(3))
.onErrorResume(e -> {
log.error("Failed to fetch user profile", e);
return Mono.just(new UserProfileDTO()); // 返回空对象作为降级
});
Mono<List<OrderDTO>> ordersMono = fetchRecentOrders(userId)
.timeout(Duration.ofSeconds(3))
.onErrorResume(e -> {
log.error("Failed to fetch orders", e);
return Mono.just(new ArrayList<>());
});
Mono<List<DashboardDTO.ProductDTO>> productsMono = fetchRecommendedProducts()
.timeout(Duration.ofSeconds(3))
.onErrorResume(e -> {
log.error("Failed to fetch products", e);
return Mono.just(new ArrayList<>());
});
// 使用zip聚合结果
return Mono.zip(userProfileMono, ordersMono, productsMono)
.map(tuple -> {
DashboardDTO dashboard = new DashboardDTO();
dashboard.setUserProfile(tuple.getT1());
dashboard.setRecentOrders(tuple.getT2());
dashboard.setRecommendedProducts(tuple.getT3());
// 计算统计信息
dashboard.setOrderCount(tuple.getT2().size());
dashboard.setTotalSpent(tuple.getT2().stream()
.map(OrderDTO::getTotalAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add));
return dashboard;
});
}
/**
* 简化版的同步方法
*/
public DashboardDTO getDashboardDataSync(String userId) {
CompletableFuture<UserProfileDTO> userFuture =
CompletableFuture.supplyAsync(() -> fetchUserProfileSync(userId));
CompletableFuture<List<OrderDTO>> ordersFuture =
CompletableFuture.supplyAsync(() -> fetchRecentOrdersSync(userId));
CompletableFuture<List<DashboardDTO.ProductDTO>> productsFuture =
CompletableFuture.supplyAsync(() -> fetchRecommendedProductsSync());
CompletableFuture.allOf(userFuture, ordersFuture, productsFuture).join();
DashboardDTO dashboard = new DashboardDTO();
dashboard.setUserProfile(userFuture.join());
dashboard.setRecentOrders(ordersFuture.join());
dashboard.setRecommendedProducts(productsFuture.join());
dashboard.setOrderCount(dashboard.getRecentOrders().size());
dashboard.setTotalSpent(dashboard.getRecentOrders().stream()
.map(OrderDTO::getTotalAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add));
return dashboard;
}
// 异步方法
private Mono<UserProfileDTO> fetchUserProfile(String userId) {
return userServiceWebClient.get()
.uri("/api/users/{id}", userId)
.retrieve()
.bodyToMono(UserProfileDTO.class);
}
private Mono<List<OrderDTO>> fetchRecentOrders(String userId) {
return orderServiceWebClient.get()
.uri("/api/orders?userId={userId}&limit=5", userId)
.retrieve()
.bodyToFlux(OrderDTO.class)
.collectList();
}
private Mono<List<DashboardDTO.ProductDTO>> fetchRecommendedProducts() {
return productServiceWebClient.get()
.uri("/api/products/recommended")
.retrieve()
.bodyToFlux(DashboardDTO.ProductDTO.class)
.collectList();
}
// 同步方法
private UserProfileDTO fetchUserProfileSync(String userId) {
return userServiceWebClient.get()
.uri("/api/users/{id}", userId)
.retrieve()
.bodyToMono(UserProfileDTO.class)
.block();
}
private List<OrderDTO> fetchRecentOrdersSync(String userId) {
return orderServiceWebClient.get()
.uri("/api/orders?userId={userId}&limit=5", userId)
.retrieve()
.bodyToFlux(OrderDTO.class)
.collectList()
.block();
}
private List<DashboardDTO.ProductDTO> fetchRecommendedProductsSync() {
return productServiceWebClient.get()
.uri("/api/products/recommended")
.retrieve()
.bodyToFlux(DashboardDTO.ProductDTO.class)
.collectList()
.block();
}
}
控制器
package com.example.bff.controller;
import com.example.bff.dto.DashboardDTO;
import com.example.bff.service.BFFService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/bff")
@RequiredArgsConstructor
public class BFFController {
private final BFFService bffService;
/**
* 获取用户仪表盘数据(响应式方式)
*/
@GetMapping("/dashboard/{userId}")
public Mono<ResponseEntity<DashboardDTO>> getDashboard(
@PathVariable String userId,
@RequestHeader(value = "X-Client-Type", defaultValue = "web") String clientType) {
return bffService.getDashboardData(userId)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
/**
* 获取用户仪表盘数据(同步方式)
*/
@GetMapping("/dashboard-sync/{userId}")
public ResponseEntity<DashboardDTO> getDashboardSync(
@PathVariable String userId,
@RequestHeader(value = "X-Client-Type", defaultValue = "web") String clientType) {
DashboardDTO dashboard = bffService.getDashboardDataSync(userId);
return ResponseEntity.ok(dashboard);
}
/**
* 健康检查
*/
@GetMapping("/health")
public ResponseEntity<String> healthCheck() {
return ResponseEntity.ok("BFF Service is running");
}
}
全局异常处理
package com.example.bff.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.reactive.function.client.WebClientRequestException;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(WebClientRequestException.class)
public ResponseEntity<Map<String, Object>> handleWebClientRequestException(WebClientRequestException e) {
log.error("Downstream service connection error", e);
Map<String, Object> error = new HashMap<>();
error.put("timestamp", LocalDateTime.now());
error.put("message", "Downstream service is unavailable");
error.put("status", HttpStatus.SERVICE_UNAVAILABLE.value());
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(error);
}
@ExceptionHandler(WebClientResponseException.class)
public ResponseEntity<Map<String, Object>> handleWebClientResponseException(WebClientResponseException e) {
log.error("Downstream service error", e);
Map<String, Object> error = new HashMap<>();
error.put("timestamp", LocalDateTime.now());
error.put("message", "Error calling downstream service");
error.put("details", e.getResponseBodyAsString());
error.put("status", e.getStatusCode().value());
return ResponseEntity.status(e.getStatusCode()).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleGenericException(Exception e) {
log.error("Unexpected error", e);
Map<String, Object> error = new HashMap<>();
error.put("timestamp", LocalDateTime.now());
error.put("message", "Internal server error");
error.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
return ResponseEntity.internalServerError().body(error);
}
}
测试类
package com.example.bff;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.reactive.server.WebTestClient;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class BFFControllerTest {
@Autowired
private WebTestClient webTestClient;
@Test
public void testGetDashboard() {
webTestClient.get()
.uri("/api/bff/dashboard/user123")
.header("X-Client-Type", "mobile")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.userProfile").exists()
.jsonPath("$.recentOrders").isArray()
.jsonPath("$.recommendedProducts").isArray();
}
}
模拟下游服务的示例
为了测试BFF,您可以创建简单的模拟服务,以下是模拟用户服务的示例:
package com.example.mock.service;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.*;
@SpringBootApplication
public class MockUserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(MockUserServiceApplication.class, args);
}
@RestController
@RequestMapping("/api/users")
public static class UserController {
@GetMapping("/{id}")
public UserProfile getUser(@PathVariable String id) {
return new UserProfile(id, "john_doe", "john@example.com",
"1234567890", "avatar.png", "USER");
}
}
static class UserProfile {
public String userId;
public String username;
public String email;
public String phone;
public String avatar;
public String role;
public UserProfile(String userId, String username, String email,
String phone, String avatar, String role) {
this.userId = userId;
this.username = username;
this.email = email;
this.phone = phone;
this.avatar = avatar;
this.role = role;
}
}
}
部署和运行
-
启动下游服务:运行模拟的用户服务(端口8081)、订单服务(端口8082)、产品服务(端口8083)
-
启动BFF服务:
mvn spring-boot:run
-
测试API:
curl -H "X-Client-Type: mobile" http://localhost:8080/api/bff/dashboard/user123
关键特性
响应式支持
- 使用Mono和Flux实现异步非阻塞编程
- 支持高并发场景
服务聚合
- 聚合用户、订单、产品等多个下游服务
- 并行调用提高性能
容错处理
- 超时控制
- 错误降级
- 全局异常处理
客户端适配
- 通过Header标识不同客户端(Web/Mobile)
- 可以根据客户端类型定制响应
性能优化
- 并行请求
- 响应式编程
- 连接池管理
这个BFF案例展示了如何构建一个聚合多个下游服务的中间层,为前端提供定制化的API,您可以根据实际需求扩展更多功能,如缓存、限流、认证等。