Java BFF案例

wen java案例 3

本文目录导读:

Java BFF案例

  1. 项目结构
  2. pom.xml
  3. 主应用类
  4. 配置文件
  5. WebClient配置
  6. DTO类
  7. 服务层
  8. 控制器
  9. 全局异常处理
  10. 测试类
  11. 模拟下游服务的示例
  12. 部署和运行
  13. 关键特性

我将为您提供一个完整的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;
        }
    }
}

部署和运行

  1. 启动下游服务:运行模拟的用户服务(端口8081)、订单服务(端口8082)、产品服务(端口8083)

  2. 启动BFF服务

    mvn spring-boot:run
  3. 测试API

    curl -H "X-Client-Type: mobile" http://localhost:8080/api/bff/dashboard/user123

关键特性

响应式支持

  • 使用Mono和Flux实现异步非阻塞编程
  • 支持高并发场景

服务聚合

  • 聚合用户、订单、产品等多个下游服务
  • 并行调用提高性能

容错处理

  • 超时控制
  • 错误降级
  • 全局异常处理

客户端适配

  • 通过Header标识不同客户端(Web/Mobile)
  • 可以根据客户端类型定制响应

性能优化

  • 并行请求
  • 响应式编程
  • 连接池管理

这个BFF案例展示了如何构建一个聚合多个下游服务的中间层,为前端提供定制化的API,您可以根据实际需求扩展更多功能,如缓存、限流、认证等。

上一篇Java GraphQL案例

下一篇安卓案例

抱歉,评论功能暂时关闭!