WebFlux 实战指南 :从入门到精通,环境搭建、控制器设计、数据库交互(含 R2DBC)到高并发优化(限流、缓存)的响应式编程全链路实践

一、引言:响应式编程与 WebFlux 的核心价值

在当今分布式系统与高并发场景中,传统的同步阻塞式编程模型逐渐暴露瓶颈——每个请求独占一个线程,当面对大量 I/O 操作(如数据库查询、远程调用)时,线程会陷入阻塞等待,导致线程资源耗尽、系统吞吐量下降。而响应式编程以异步非阻塞为核心,通过事件驱动的方式实现资源高效利用,能够用少量线程处理数万级并发请求,成为应对高流量场景的关键技术。

Spring WebFlux 作为 Spring 生态中响应式 Web 框架的代表,基于 Reactive Streams 规范实现,具备三大核心优势:

  • 非阻塞 I/O:通过事件循环(Event Loop)机制,用固定线程处理海量请求,避免线程上下文切换开销;
  • 背压(Backpressure)支持:消费者可向生产者反馈处理能力,防止数据积压导致的内存溢出;
  • 多范式兼容:同时支持注解式编程(类似 Spring MVC)和函数式编程,灵活适配不同开发习惯。

结合 Java 19 引入的虚拟线程(Virtual Threads),WebFlux 可进一步优化线程资源调度——虚拟线程的轻量级特性(单个 JVM 可支持百万级虚拟线程)与 WebFlux 的非阻塞模型结合,能在保持低资源占用的同时,简化异步代码的编写与调试。

本文将从实战角度出发,系统讲解 WebFlux 在实际项目中的核心应用场景,涵盖控制器设计、过滤器链、文件处理、响应式数据库交互、安全鉴权、限流策略、缓存设计等关键环节,并通过代码示例与原理分析,帮助开发者掌握响应式编程的实践技巧。

架构

在多年前我写过一篇初入WebFlux的文章,多年以后的今天因为SpringAI的春风 曾经抵触响应式的企业也开始拥抱,对于WebFlux不熟悉的可先阅读

响应式开发之webFlux & Reactor

二、项目环境搭建:从依赖到配置的生产级实践

2.1 依赖引入:响应式生态的核心组件

WebFlux 的高效运行依赖于响应式生态的协同工作,以下是生产环境中常用的依赖配置(pom.xml),并附核心组件说明:

<dependencies>
    <!-- 核心:Spring WebFlux 响应式 Web 框架 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>

    <!-- 响应式数据库:R2DBC 驱动(替代 JDBC 的异步非阻塞方案) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-r2dbc</artifactId>
    </dependency>
    <dependency>
        <groupId>io.r2dbc</groupId>
        <artifactId>r2dbc-postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- 响应式缓存:Redis 响应式客户端 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    </dependency>

    <!-- 安全框架:响应式 Spring Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- 消息队列:响应式 RabbitMQ 客户端 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.amqp</groupId>
        <artifactId>spring-rabbit-reactive</artifactId>
    </dependency>

    <!-- 数据校验:响应式环境下的参数验证 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <!-- 工具类:Lombok 简化实体类代码 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- 测试:响应式测试支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>io.projectreactor</groupId>
        <artifactId>reactor-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

核心依赖解析

  • spring-boot-starter-webflux:包含 WebFlux 核心组件(如 DispatcherHandler、响应式编码器/解码器);
  • spring-boot-starter-data-r2dbc:响应式数据库访问框架,替代传统 JDBC,支持异步 SQL 执行;
  • spring-boot-starter-data-redis-reactive:响应式 Redis 客户端,避免 Redis 操作阻塞事件循环;
  • spring-rabbit-reactive:响应式 RabbitMQ 客户端,支持消息发送/接收的非阻塞处理。

2.2 配置文件:生产级参数优化

application.yml 配置需兼顾性能与可靠性,以下为关键配置项示例:

spring:
  # 服务器配置(Netty 作为默认容器)
  webflux:
    server:
      max-http-request-size: 10MB  # 最大请求大小(文件上传需调整)
      compression:
        enabled: true  # 启用响应压缩(节省带宽)
        mime-types: application/json,application/xml,text/html  # 压缩类型

  # R2DBC 数据库配置(PostgreSQL 示例)
  r2dbc:
    url: r2dbc:postgresql://localhost:5432/webflux_db?sslMode=disable
    username: db_user
    password: db_pass
    pool:
      max-size: 20  # 连接池最大连接数(根据 CPU 核心数调整)
      idle-timeout: 30s  # 连接空闲超时
      validation-query: SELECT 1  # 连接有效性校验

  # Redis 响应式配置
  redis:
    host: localhost
    port: 6379
    password: redis_pass
    timeout: 2s  # 连接超时
    lettuce:
      pool:
        max-active: 16  # 最大活跃连接
        max-idle: 8     # 最大空闲连接
        min-idle: 2     # 最小空闲连接

  # RabbitMQ 配置
  rabbitmq:
    host: localhost
    port: 5672
    username: rabbit_user
    password: rabbit_pass
    virtual-host: /webflux
    connection-timeout: 3s
    listener:
      simple:
        concurrency: 5  # 消费者并发数
        max-concurrency: 10
        prefetch: 100   # 每次从队列拉取的消息数(控制背压)

# 日志配置(响应式场景需避免同步日志阻塞)
logging:
  level:
    org.springframework.web: INFO
    reactor.netty: WARN  # 降低 Netty 日志级别(避免刷屏)
    com.example: DEBUG   # 业务日志级别
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"

# 自定义业务配置
app:
  upload:
    base-dir: /data/webflux/uploads  # 文件上传根目录
    chunk-size: 5MB  # 分片上传的分片大小
  rate-limit:
    default-capacity: 1000  # 默认令牌桶容量
    default-rate: 100  # 默认令牌生成速率(个/秒)

关键配置说明

  • r2dbc.pool.max-size:连接池大小建议为 CPU 核心数 * 2,避免连接过多导致数据库压力;
  • redis.lettuce.pool:Lettuce 是 Redis 响应式客户端,池化配置需平衡并发与资源占用;
  • rabbitmq.listener.simple.prefetch:控制消费者每次拉取的消息数,结合背压防止消息堆积。

三、控制器(Controller):响应式请求处理范式

WebFlux 控制器作为请求入口,需充分利用响应式类型(Mono/Flux)处理异步逻辑。与 Spring MVC 不同,WebFlux 控制器方法的返回值必须是响应式类型,以确保非阻塞执行。

3.1 基础控制器:请求参数与响应处理

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.validation.Valid;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/users")
public class UserController {

    /**
     * 1. 基础 GET 请求:路径参数 + 响应式返回
     */
    @GetMapping("/{id}")
    public Mono<ResponseEntity<User>> getUserById(@PathVariable Long id) {
        // 模拟数据库查询(实际应调用 service)
        Mono<User> userMono = Mono.just(new User(id, "Alice", 30))
                .delayElement(Duration.ofMillis(100)); // 模拟 I/O 延迟

        // 处理不存在的情况(返回 404)
        return userMono
                .map(ResponseEntity::ok)
                .defaultIfEmpty(ResponseEntity.notFound().build());
    }

    /**
     * 2. POST 请求:请求体验证 + 异步响应
     */
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<User> createUser(@Valid @RequestBody Mono<User> userMono) {
        // @Valid 支持响应式类型,自动校验请求体
        return userMono
                .doOnNext(user -> {
                    // 可在此处添加业务校验(如用户名唯一性检查)
                    if (user.getAge() < 0) {
                        throw new IllegalArgumentException("年龄不能为负数");
                    }
                })
                .delayElement(Duration.ofMillis(200)) // 模拟保存延迟
                .map(user -> {
                    user.setId(100L); // 模拟数据库生成 ID
                    return user;
                });
    }

    /**
     * 3. 批量查询:Flux 响应流 + 分页参数
     */
    @GetMapping
    public Flux<User> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        // 模拟分页查询(实际应从数据库获取)
        return Flux.range(0, 50)
                .map(i -> new User((long) (page * size + i), "User" + i, 20 + i % 30))
                .skip(page * size) // 分页跳过
                .take(size) // 分页截取
                .delayElements(Duration.ofMillis(50)); // 模拟流数据延迟返回
    }

    /**
     * 4. 错误处理:全局异常外的局部处理
     */
    @GetMapping("/error-demo")
    public Mono<String> errorDemo() {
        return Mono.error(new RuntimeException("故意抛出的异常"))
                .onErrorResume(e -> {
                    // 局部异常处理(返回友好提示)
                    return Mono.just("请求失败:" + e.getMessage());
                });
    }
}

// 实体类(含验证注解)
@Data
@NoArgsConstructor
@AllArgsConstructor
class User {
    private Long id;

    @NotBlank(message = "用户名不能为空")
    private String name;

    @Min(value = 0, message = "年龄不能为负数")
    @Max(value = 150, message = "年龄不能超过 150")
    private Integer age;
}

核心特性

  • 响应式返回值Mono<User> 表示单个用户(异步获取),Flux<User> 表示用户流(支持分页/批量数据);
  • 参数验证@Valid 注解支持 Mono<User> 类型,自动触发 JSR-303 验证(如 @NotBlank@Min);
  • 错误处理onErrorResume 可局部捕获异常并返回默认值,避免异常中断整个响应流。

3.2 高级场景:响应式数据组合与转换

实际业务中常需组合多个数据源(如数据库 + 缓存 + 远程调用),WebFlux 提供丰富的操作符实现流的组合:

@RestController
@RequestMapping("/api/advanced")
public class AdvancedController {

    private final UserService userService;
    private final OrderService orderService;
    private final RemoteApiClient remoteApiClient;

    // 构造器注入依赖(省略)

    /**
     * 组合多个异步数据源:用户信息 + 订单列表 + 远程数据
     */
    @GetMapping("/user-details/{userId}")
    public Mono<UserDetailsDTO> getUserDetails(@PathVariable Long userId) {
        // 1. 获取用户基本信息
        Mono<User> userMono = userService.getUserById(userId);

        // 2. 获取用户订单列表
        Mono<List<Order>> ordersMono = orderService.getOrdersByUserId(userId)
                .collectList();

        // 3. 调用远程 API 获取额外信息
        Mono<RemoteData> remoteDataMono = remoteApiClient.getRemoteData(userId);

        // 4. 组合三个异步结果(当所有结果就绪后合并)
        return Mono.zip(userMono, ordersMono, remoteDataMono)
                .map(tuple -> {
                    User user = tuple.getT1();
                    List<Order> orders = tuple.getT2();
                    RemoteData remoteData = tuple.getT3();

                    // 转换为 DTO 返回
                    UserDetailsDTO dto = new UserDetailsDTO();
                    dto.setUserId(user.getId());
                    dto.setUserName(user.getName());
                    dto.setOrderCount(orders.size());
                    dto.setRemoteInfo(remoteData.getInfo());
                    return dto;
                });
    }

    /**
     * 流数据转换:将用户流转换为精简信息流
     */
    @GetMapping("/user-summaries")
    public Flux<UserSummaryDTO> getUserSummaries() {
        // 从数据库获取用户流,转换为精简 DTO 流
        return userService.getAllUsers()
                .map(user -> new UserSummaryDTO(user.getId(), user.getName()))
                .filter(dto -> dto.getUserName().startsWith("A")) // 过滤名字以 A 开头的用户
                .take(10); // 只返回前 10 条
    }
}

操作符解析

  • Mono.zip:组合多个 Mono,等待所有结果就绪后合并(类似 CompletableFuture.allOf);
  • Flux.map:对流中每个元素进行转换;
  • filter/take:对流数据进行过滤和截取,减少不必要的数据传输。

四、过滤器(Filter):请求生命周期的拦截与增强

WebFlux 的 WebFilter 用于拦截请求/响应,与 Spring MVC 的 Filter 不同,它基于响应式 API 设计,支持异步操作。常见用途包括日志记录、跨域处理、请求头修改、异常拦截等。

4.1 过滤器链执行机制

WebFlux 过滤器通过 WebFilterChain 形成链式调用,每个过滤器可选择:

  • 直接返回响应(如鉴权失败);
  • 修改请求/响应后继续传递(如添加请求头);
  • 处理响应结果(如记录响应时间)。

过滤器执行顺序由 @Order 注解控制,值越小越先执行(建议使用负数值优先于业务过滤器)。

4.2 核心过滤器实现示例

4.2.1 请求日志与耗时统计过滤器

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

import java.time.Instant;

/**
 * 记录请求详细信息与处理耗时
 */
@Component
@Order(-100) // 优先执行(数值越小越先)
@Slf4j
public class RequestLogFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        // 1. 记录请求开始时间
        Instant start = Instant.now();
        String method = exchange.getRequest().getMethodValue();
        String path = exchange.getRequest().getPath().value();
        String clientIp = getClientIp(exchange); // 处理代理场景的 IP 获取

        // 2. 打印请求信息
        log.info("Request start: {} {} from {}", method, path, clientIp);

        // 3. 继续执行过滤器链,并在响应完成后记录耗时
        return chain.filter(exchange)
                .doFinally(signalType -> {
                    // 响应完成后执行(无论成功/失败)
                    long duration = Instant.now().toEpochMilli() - start.toEpochMilli();
                    int status = exchange.getResponse().getStatusCode().value();
                    log.info("Request end: {} {} {} in {}ms", method, path, status, duration);
                })
                .onErrorResume(e -> {
                    // 异常时记录错误信息
                    long duration = Instant.now().toEpochMilli() - start.toEpochMilli();
                    log.error("Request error: {} {} failed in {}ms", method, path, duration, e);
                    return Mono.error(e); // 继续抛出异常(由全局异常处理器处理)
                });
    }

    /**
     * 处理代理场景的客户端 IP 获取(如通过 Nginx 转发)
     */
    private String getClientIp(ServerWebExchange exchange) {
        String xForwardedFor = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            // X-Forwarded-For 格式:clientIp, proxy1Ip, proxy2Ip
            return xForwardedFor.split(",")[0].trim();
        }
        // 直接获取远程地址
        return exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
    }
}

4.2.2 跨域(CORS)过滤器

WebFlux 虽内置 CORS 配置,但复杂场景需自定义过滤器:

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

/**
 * 自定义跨域过滤器(支持复杂跨域场景)
 */
@Component
@Order(-200) // 比日志过滤器更先执行
public class CorsFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        // 1. 设置跨域响应头
        HttpHeaders headers = exchange.getResponse().getHeaders();
        headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); // 生产环境需指定具体域名
        headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS");
        headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, Authorization");
        headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600"); // 预检请求缓存时间(1小时)

        // 2. 处理 OPTIONS 预检请求(直接返回 200)
        if (HttpMethod.OPTIONS.equals(exchange.getRequest().getMethod())) {
            exchange.getResponse().setStatusCode(HttpStatus.OK);
            return exchange.getResponse().setComplete();
        }

        // 3. 非 OPTIONS 请求继续处理
        return chain.filter(exchange);
    }
}

4.2.3 全局异常处理过滤器

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;

/**
 * 全局异常捕获与统一响应格式
 */
@Component
@Order(-300) // 最优先执行(确保所有异常被捕获)
public class GlobalExceptionFilter implements WebFilter {

    private final ObjectMapper objectMapper;

    public GlobalExceptionFilter(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(exchange)
                .onErrorResume(e -> {
                    // 1. 确定响应状态码
                    HttpStatus status = determineStatus(e);

                    // 2. 构建统一错误响应体
                    Map<String, Object> error = new HashMap<>();
                    error.put("code", status.value());
                    error.put("message", e.getMessage());
                    error.put("path", exchange.getRequest().getPath().value());

                    // 3. 设置响应头与状态码
                    exchange.getResponse().setStatusCode(status);
                    exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);

                    // 4. 写入错误响应
                    try {
                        byte[] bytes = objectMapper.writeValueAsBytes(error);
                        return exchange.getResponse().writeWith(Mono.just(
                                exchange.getResponse().bufferFactory().wrap(bytes)
                        ));
                    } catch (Exception ex) {
                        return Mono.error(ex);
                    }
                });
    }

    /**
     * 根据异常类型确定响应状态码
     */
    private HttpStatus determineStatus(Throwable e) {
        if (e instanceof IllegalArgumentException) {
            return HttpStatus.BAD_REQUEST; // 参数错误
        } else if (e instanceof ResourceNotFoundException) {
            return HttpStatus.NOT_FOUND; // 资源不存在
        } else {
            return HttpStatus.INTERNAL_SERVER_ERROR; // 其他异常
        }
    }
}

// 自定义业务异常
class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

五、文件上传下载:响应式流的高效处理

文件处理是 Web 应用的常见场景,WebFlux 通过 FilePart(上传)和 Resource(下载)支持响应式文件操作,避免大文件处理时的内存溢出。

5.1 分片文件上传:断点续传与并发合并

大文件(如 1GB+)需采用分片上传,结合 Redis 记录分片状态实现断点续传:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

@RestController
@RequestMapping("/api/upload")
public class ChunkUploadController {

    // 上传根目录(从配置文件读取)
    @Value("${app.upload.base-dir}")
    private String baseDir;

    // 分片大小(从配置文件读取)
    @Value("${app.upload.chunk-size}")
    private long chunkSize;

    // 临时存储分片上传状态(生产环境建议用 Redis 替代)
    private final ConcurrentMap<String, UploadStatus> uploadStatusMap = new ConcurrentHashMap<>();

    /**
     * 1. 初始化上传(返回上传 ID 和分片数量)
     */
    @PostMapping("/init")
    public Mono<UploadInitDTO> initUpload(@RequestBody UploadInitRequest request) {
        // 生成唯一上传 ID
        String uploadId = UUID.randomUUID().toString();
        // 计算分片数量
        int totalChunks = (int) Math.ceil((double) request.getFileSize() / chunkSize);
        // 创建临时目录
        Path tempDir = Paths.get(baseDir, "temp", uploadId);
        try {
            Files.createDirectories(tempDir);
        } catch (IOException e) {
            return Mono.error(new RuntimeException("创建临时目录失败", e));
        }
        // 记录上传状态
        uploadStatusMap.put(uploadId, new UploadStatus(totalChunks, tempDir.toString()));
        // 返回结果
        return Mono.just(new UploadInitDTO(uploadId, totalChunks, chunkSize));
    }

    /**
     * 2. 上传分片(支持断点续传)
     */
    @PostMapping("/chunk")
    public Mono<ChunkUploadDTO> uploadChunk(
            @RequestPart("file") FilePart filePart,
            @RequestParam("uploadId") String uploadId,
            @RequestParam("chunkIndex") int chunkIndex,
            ServerWebExchange exchange) {

        // 校验上传状态
        UploadStatus status = uploadStatusMap.get(uploadId);
        if (status == null) {
            return Mono.error(new IllegalArgumentException("上传 ID 不存在"));
        }
        if (chunkIndex < 0 || chunkIndex >= status.getTotalChunks()) {
            return Mono.error(new IllegalArgumentException("分片索引无效"));
        }

        // 分片保存路径
        Path chunkPath = Paths.get(status.getTempDir(), "chunk_" + chunkIndex);

        // 检查分片是否已上传(断点续传支持)
        if (Files.exists(chunkPath)) {
            return Mono.just(new ChunkUploadDTO(true, "分片已存在"));
        }

        // 保存分片(响应式写入,不阻塞事件循环)
        return filePart.transferTo(chunkPath)
                .then(Mono.fromRunnable(() -> {
                    // 标记分片已完成
                    status.markChunkCompleted(chunkIndex);
                }))
                .then(Mono.just(new ChunkUploadDTO(true, "分片上传成功")))
                .onErrorResume(e -> Mono.just(new ChunkUploadDTO(false, "分片上传失败:" + e.getMessage())));
    }

    /**
     * 3. 合并分片(所有分片上传完成后调用)
     */
    @PostMapping("/merge")
    public Mono<UploadResultDTO> mergeChunks(@RequestParam("uploadId") String uploadId,
                                             @RequestParam("fileName") String fileName) {
        UploadStatus status = uploadStatusMap.get(uploadId);
        if (status == null) {
            return Mono.error(new IllegalArgumentException("上传 ID 不存在"));
        }
        if (!status.allChunksCompleted()) {
            return Mono.error(new IllegalArgumentException("存在未完成的分片"));
        }

        // 最终文件路径
        Path targetPath = Paths.get(baseDir, fileName);
        // 临时分片目录
        Path tempDir = Paths.get(status.getTempDir());

        // 合并分片(按索引顺序)
        return Flux.range(0, status.getTotalChunks())
                .flatMap(chunkIndex -> {
                    // 读取分片内容
                    Path chunkPath = Paths.get(tempDir.toString(), "chunk_" + chunkIndex);
                    return Flux.using(
                            () -> Files.newInputStream(chunkPath),
                            inputStream -> Flux.fromInputStream(inputStream),
                            inputStream -> {
                                try {
                                    inputStream.close();
                                    Files.delete(chunkPath); // 合并后删除分片
                                } catch (IOException e) {
                                    throw new RuntimeException("删除分片失败", e);
                                }
                            }
                    );
                })
                // 写入最终文件
                .collectList()
                .flatMap(dataBuffers -> {
                    try {
                        // 创建父目录
                        Files.createDirectories(targetPath.getParent());
                        // 合并写入
                        for (byte[] buffer : dataBuffers) {
                            Files.write(targetPath, buffer, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
                        }
                        // 删除临时目录
                        Files.delete(Paths.get(status.getTempDir()));
                        // 移除上传状态
                        uploadStatusMap.remove(uploadId);
                        // 返回结果
                        return Mono.just(new UploadResultDTO(true, targetPath.toString(), targetPath.toFile().length()));
                    } catch (IOException e) {
                        return Mono.error(new RuntimeException("合并文件失败", e));
                    }
                });
    }

    // 内部类:上传状态
    private static class UploadStatus {
        private final int totalChunks;
        private final String tempDir;
        private final boolean[] completedChunks;

        public UploadStatus(int totalChunks, String tempDir) {
            this.totalChunks = totalChunks;
            this.tempDir = tempDir;
            this.completedChunks = new boolean[totalChunks];
        }

        public void markChunkCompleted(int chunkIndex) {
            completedChunks[chunkIndex] = true;
        }

        public boolean allChunksCompleted() {
            for (boolean completed : completedChunks) {
                if (!completed) return false;
            }
            return true;
        }

        // Getter 省略
    }

    // DTO 类(省略 Getter/Setter)
    static class UploadInitRequest {
        private String fileName;
        private long fileSize;
    }

    static class UploadInitDTO {
        private String uploadId;
        private int totalChunks;
        private long chunkSize;
    }

    // 其他 DTO 类省略
}

核心优势

  • 分片上传:将大文件拆分为小分片,降低单次请求压力;
  • 断点续传:通过检查分片是否已存在,避免重复上传;
  • 响应式流处理:FilePart.transferToFlux.using 确保文件操作非阻塞,不占用事件循环线程。

5.2 支持断点续传的文件下载

大文件下载需支持 Range 请求(断点续传),避免网络中断后重新下载:

import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@RestController
@RequestMapping("/api/download")
public class ResumeDownloadController {

    @Value("${app.upload.base-dir}")
    private String baseDir;

    /**
     * 支持断点续传的文件下载
     */
    @GetMapping("/{fileName}")
    public Mono<ResponseEntity<Resource>> downloadFile(
            @PathVariable String fileName,
            @RequestHeader(value = HttpHeaders.RANGE, required = false) String rangeHeader) {

        // 文件路径
        Path filePath = Paths.get(baseDir, fileName);
        File file = filePath.toFile();

        // 校验文件是否存在
        if (!file.exists() || !file.isFile()) {
            return Mono.just(ResponseEntity.notFound().build());
        }

        return Mono.fromSupplier(() -> {
            try {
                long fileLength = file.length();
                HttpRange range = parseRange(rangeHeader, fileLength);

                if (range == null) {
                    // 1. 完整下载(无 Range 头)
                    return ResponseEntity.ok()
                            .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileLength))
                            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\\\\"" + fileName + "\\\\"")
                            .header(HttpHeaders.ACCEPT_RANGES, "bytes") // 声明支持断点续传
                            .body(new FileSystemResource(file));
                } else {
                    // 2. 部分下载(有 Range 头)
                    long start = range.getRangeStart(fileLength);
                    long end = range.getRangeEnd(fileLength);
                    long contentLength = end - start + 1;

                    // 构建部分内容资源(只读取指定范围)
                    Resource partialResource = new PartialFileSystemResource(file, start, end);

                    return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
                            .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength))
                            .header(HttpHeaders.CONTENT_RANGE, "bytes " + start + "-" + end + "/" + fileLength)
                            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\\\\"" + fileName + "\\\\"")
                            .header(HttpHeaders.ACCEPT_RANGES, "bytes")
                            .body(partialResource);
                }
            } catch (Exception e) {
                return ResponseEntity.internalServerError().build();
            }
        });
    }

    /**
     * 解析 Range 请求头(格式:bytes=start-end)
     */
    private HttpRange parseRange(String rangeHeader, long fileLength) {
        if (rangeHeader == null || !rangeHeader.startsWith("bytes=")) {
            return null;
        }
        String rangeStr = rangeHeader.substring("bytes=".length());
        String[] parts = rangeStr.split("-");
        try {
            long start = Long.parseLong(parts[0]);
            long end = parts.length > 1 ? Long.parseLong(parts[1]) : fileLength - 1;
            // 修正范围(确保不超过文件大小)
            start = Math.max(0, Math.min(start, fileLength - 1));
            end = Math.max(start, Math.min(end, fileLength - 1));
            return HttpRange.create(start, end);
        } catch (NumberFormatException e) {
            return null; // 解析失败,返回完整文件
        }
    }

    /**
     * 自定义资源类:只读取文件的指定范围(支持断点续传)
     */
    static class PartialFileSystemResource extends FileSystemResource {
        private final long start;
        private final long end;

        public PartialFileSystemResource(File file, long start, long end) {
            super(file);
            this.start = start;
            this.end = end;
        }

        @Override
        public long contentLength() {
            return end - start + 1;
        }

        @Override
        public InputStream getInputStream() throws IOException {
            // 只读取 [start, end] 范围的内容
            InputStream is = super.getInputStream();
            is.skip(start); // 跳过起始前的内容
            return new BoundedInputStream(is, end - start + 1); // 限制读取长度
        }
    }

    /**
     * 限制输入流读取长度的包装类
     */
    static class BoundedInputStream extends FilterInputStream {
        private final long maxBytes;
        private long bytesRead = 0;

        protected BoundedInputStream(InputStream in, long maxBytes) {
            super(in);
            this.maxBytes = maxBytes;
        }

        @Override
        public int read() throws IOException {
            if (bytesRead >= maxBytes) {
                return -1;
            }
            int b = super.read();
            if (b != -1) {
                bytesRead++;
            }
            return b;
        }

        // 省略其他 read 方法的重写(确保不超过 maxBytes)
    }
}

断点续传核心逻辑

  • 通过 Range 请求头解析客户端需要的文件片段范围;
  • 服务器只返回指定范围的内容,并通过 Content-Range 头告知客户端片段位置;
  • 客户端可通过多次请求拼接完整文件(如浏览器/下载工具的断点续传功能)。

六、响应式数据库 R2DBC:异步数据访问的核心

传统 JDBC 因阻塞特性无法充分发挥 WebFlux 的性能优势,而 R2DBC(Reactive Relational Database Connectivity) 作为响应式关系型数据库连接标准,通过异步非阻塞方式执行 SQL 操作,完美适配 WebFlux 的事件循环模型。

6.1 R2DBC 核心优势与适用场景

  • 非阻塞 I/O:SQL 执行不会阻塞事件循环线程,资源利用率更高;
  • 背压支持:结果集通过 Flux 流式返回,避免大结果集导致的内存溢出;
  • 多数据库支持:主流数据库(PostgreSQL、MySQL、SQL Server 等)均提供 R2DBC 驱动;
  • 事务支持:通过 @Transactional 注解实现响应式事务管理。

适用场景:高并发读操作(如商品列表查询)、I/O 密集型业务(如日志记录);不适用场景:复杂多表联查(需手动处理关联关系)、CPU 密集型计算。

6.2 实体类设计与表映射

import lombok.Data;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;

import java.time.LocalDateTime;

/**
 * 用户实体类(与数据库表 sys_user 映射)
 */
@Data
@Table("sys_user") // 指定映射的表名
public class User {

    @Id // 主键标识
    private Long id;

    @Column("user_name") // 映射表中字段(默认与属性名一致,可省略)
    private String name;

    private Integer age;

    private String email;

    @CreatedDate // 自动填充创建时间(需配合审计配置)
    @Column("create_time")
    private LocalDateTime createTime;

    @LastModifiedDate // 自动填充更新时间
    @Column("update_time")
    private LocalDateTime updateTime;

    // 忽略 getter/setter(Lombok 的 @Data 注解自动生成)
}

审计配置(启用 @CreatedDate 等注解):

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.r2dbc.config.EnableR2dbcAuditing;
import org.springframework.data.r2dbc.convert.R2dbcCustomConversions;

import java.util.Collections;

@Configuration
@EnableR2dbcAuditing // 启用 R2DBC 审计功能
public class R2dbcConfig {

    // 自定义类型转换器(如 LocalDateTime 与数据库时间类型的转换)
    @Bean
    public R2dbcCustomConversions customConversions() {
        return new R2dbcCustomConversions(Collections.emptyList());
    }
}

6.3 响应式 Repository 接口

Spring Data R2DBC 提供 R2dbcRepository 接口,简化数据访问层代码:

import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.data.repository.query.Param;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * 用户数据访问接口(继承 R2dbcRepository 获得基础 CRUD 能力)
 */
public interface UserRepository extends R2dbcRepository<User, Long> {

    /**
     * 按用户名查询(基于方法名自动生成 SQL)
     */
    Mono<User> findByName(String name);

    /**
     * 按年龄范围查询(自定义 SQL)
     */
    @Query("SELECT * FROM sys_user WHERE age BETWEEN :minAge AND :maxAge ORDER BY age ASC")
    Flux<User> findByAgeRange(@Param("minAge") Integer minAge, @Param("maxAge") Integer maxAge);

    /**
     * 按邮箱前缀查询
     */
    Flux<User> findByEmailStartingWith(String prefix);

    /**
     * 检查用户名是否存在
     */
    @Query("SELECT COUNT(*) > 0 FROM sys_user WHERE name = :name")
    Mono<Boolean> existsByName(@Param("name") String name);
}

方法解析

  • 继承 R2dbcRepository<User, Long> 后,自动获得 savefindByIddeleteById 等基础方法;
  • 方法名遵循 Spring Data 命名规范(如 findByName 对应 WHERE name = ?);
  • @Query 注解支持自定义 SQL,灵活应对复杂查询场景。

6.4 服务层实现:响应式业务逻辑

服务层需处理业务逻辑,并通过响应式操作符组合数据流:

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
@RequiredArgsConstructor // Lombok 自动生成构造器
public class UserService {

    private final UserRepository userRepository;

    /**
     * 保存用户(含业务校验)
     */
    public Mono<User> saveUser(User user) {
        // 1. 业务校验(用户名唯一性检查)
        return userRepository.existsByName(user.getName())
                .flatMap(exists -> {
                    if (exists) {
                        return Mono.error(new IllegalArgumentException("用户名已存在"));
                    }
                    // 2. 校验通过,保存用户
                    return userRepository.save(user);
                });
    }

    /**
     * 按 ID 查询用户(处理不存在的情况)
     */
    public Mono<User> getUserById(Long id) {
        return userRepository.findById(id)
                .switchIfEmpty(Mono.error(new ResourceNotFoundException("用户不存在:" + id)));
    }

    /**
     * 分页查询用户(结合 Pageable)
     */
    public Flux<User> getUsersByPage(int page, int size) {
        // 计算偏移量(页码从 0 开始)
        long offset = (long) page * size;
        return userRepository.findAll()
                .skip(offset) // 跳过前面的记录
                .take(size); // 取指定数量的记录
    }

    /**
     * 批量保存用户(演示 Flux 操作)
     */
    public Flux<User> batchSave(Flux<User> userFlux) {
        return userFlux
                // 过滤无效用户(年龄 <= 0)
                .filter(user -> user.getAge() != null && user.getAge() > 0)
                // 批量保存(R2DBC 会优化为批量操作)
                .flatMap(userRepository::save);
    }

    /**
     * 更新用户邮箱(乐观锁思路:先查后改)
     */
    public Mono<User> updateEmail(Long id, String newEmail) {
        return userRepository.findById(id)
                .switchIfEmpty(Mono.error(new ResourceNotFoundException("用户不存在")))
                .flatMap(user -> {
                    user.setEmail(newEmail);
                    return userRepository.save(user); // 保存更新后的用户
                });
    }
}

响应式操作亮点

  • 方法返回值均为 MonoFlux,确保操作链全程非阻塞;
  • flatMap 用于串联异步操作(如先查后改);
  • switchIfEmpty 处理“资源不存在”等场景,返回友好异常;
  • filter 对流数据进行清洗,减少无效数据库操作。

6.5 数据库事务管理

R2DBC 支持声明式事务,通过 @Transactional 注解确保操作原子性:

import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Mono;

@Service
public class UserTransactionService {

    private final UserRepository userRepository;
    private final UserRoleRepository userRoleRepository; // 假设存在用户角色关联表

    // 构造器注入(省略)

    /**
     * 事务示例:同时保存用户和关联角色
     */
    @Transactional // 标记为事务方法
    public Mono<User> saveUserWithRole(User user, Long roleId) {
        // 1. 保存用户
        return userRepository.save(user)
                // 2. 保存用户-角色关联(若失败,用户保存会回滚)
                .flatMap(savedUser -> {
                    UserRole userRole = new UserRole(savedUser.getId(), roleId);
                    return userRoleRepository.save(userRole)
                            .thenReturn(savedUser); // 返回保存的用户
                });
    }
}

事务特性

  • @Transactional 注解在响应式方法上同样生效,确保 flatMap 中的所有操作要么全成功,要么全回滚;
  • 事务仅对 Mono<Void>Mono<T>Flux<T> 类型的返回值有效;
  • 可通过 rollbackFor 属性指定触发回滚的异常类型。

七、高级查询与分页:优化响应式数据获取

实际项目中,复杂查询(如多条件过滤、排序、分页)是常态,R2DBC 提供多种方式实现高效查询。

7.1 标准分页实现(基于 Pageable)

Spring Data R2DBC 支持 Pageable 对象,简化分页参数处理:

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

@Service
public class UserPagingService {

    private final UserRepository userRepository;

    // 构造器注入(省略)

    /**
     * 分页查询(含总条数统计)
     */
    public Mono<Page<User>> getUsersPage(Pageable pageable) {
        // 1. 查询当前页数据
        Flux<User> users = userRepository.findAll()
                .skip(pageable.getOffset())
                .take(pageable.getPageSize());

        // 2. 查询总条数
        Mono<Long> total = userRepository.count();

        // 3. 组合分页结果
        return Mono.zip(users.collectList(), total)
                .map(tuple -> new PageImpl<>(
                        tuple.getT1(), // 当前页数据
                        pageable,      // 分页参数
                        tuple.getT2()  // 总条数
                ));
    }
}

分页控制器

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/users")
public class UserPagingController {

    private final UserPagingService pagingService;

    // 构造器注入(省略)

    /**
     * 分页查询接口(支持排序)
     */
    @GetMapping("/page")
    public Mono<Page<User>> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id,desc") String[] sort) {

        // 解析排序参数(格式:字段名,方向,如 "age,asc")
        Sort.Direction direction = Sort.Direction.DESC;
        String property = "id";
        if (sort.length > 0) {
            String[] parts = sort[0].split(",");
            property = parts[0];
            if (parts.length > 1 && "asc".equalsIgnoreCase(parts[1])) {
                direction = Sort.Direction.ASC;
            }
        }

        // 创建 Pageable 对象
        Pageable pageable = PageRequest.of(
                page,
                size,
                Sort.by(direction, property)
        );

        return pagingService.getUsersPage(pageable);
    }
}

7.2 复杂条件查询(使用 DatabaseClient)

对于动态 SQL(如多条件组合查询),DatabaseClientRepository 更灵活:

import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

@Service
public class UserAdvancedQueryService {

    private final DatabaseClient databaseClient;

    // 构造器注入(省略)

    /**
     * 动态条件查询(年龄范围 + 邮箱后缀)
     */
    public Flux<User> queryUsers(Integer minAge, Integer maxAge, String emailSuffix) {
        // 基础 SQL
        StringBuilder sql = new StringBuilder("SELECT * FROM sys_user WHERE 1=1");
        // 动态拼接条件
        if (minAge != null) {
            sql.append(" AND age >= :minAge");
        }
        if (maxAge != null) {
            sql.append(" AND age <= :maxAge");
        }
        if (emailSuffix != null && !emailSuffix.isEmpty()) {
            sql.append(" AND email LIKE :emailSuffix");
        }

        // 构建查询
        DatabaseClient.GenericExecuteSpec spec = databaseClient.sql(sql.toString());

        // 绑定参数
        if (minAge != null) {
            spec = spec.bind("minAge", minAge);
        }
        if (maxAge != null) {
            spec = spec.bind("maxAge", maxAge);
        }
        if (emailSuffix != null && !emailSuffix.isEmpty()) {
            spec = spec.bind("emailSuffix", "%" + emailSuffix);
        }

        // 执行查询并映射结果
        return spec.map((row, rowMetadata) -> {
            User user = new User();
            user.setId(row.get("id", Long.class));
            user.setName(row.get("user_name", String.class));
            user.setAge(row.get("age", Integer.class));
            user.setEmail(row.get("email", String.class));
            user.setCreateTime(row.get("create_time", LocalDateTime.class));
            return user;
        }).all();
    }
}

优势

  • 支持动态拼接 SQL,适配多条件组合查询;
  • 通过 bind 方法安全绑定参数,避免 SQL 注入;
  • map 方法手动映射结果集,灵活处理复杂字段。

八、安全鉴权:响应式环境下的身份认证与授权

WebFlux 结合 Spring Security 可实现响应式安全控制,支持 JWT、OAuth2 等主流认证方式,确保接口访问安全。

8.1 基于 JWT 的认证配置

JWT(JSON Web Token)适合无状态服务,以下是完整配置示例:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;

@Configuration
@EnableWebFluxSecurity // 启用 WebFlux 安全
public class SecurityConfig {

    /**
     * 安全过滤器链配置
     */
    @Bean
    public SecurityWebFilterChain securityWebFilterChain(
            ServerHttpSecurity http,
            ReactiveAuthenticationManager authManager,
            ServerAuthenticationConverter jwtConverter) {

        // JWT 认证过滤器
        AuthenticationWebFilter jwtFilter = new AuthenticationWebFilter(authManager);
        jwtFilter.setServerAuthenticationConverter(jwtConverter); // 设置 JWT 转换器

        return http
                // 1. 配置路径权限
                .authorizeExchange()
                .pathMatchers("/public/**", "/auth/login").permitAll() // 公开路径
                .pathMatchers("/admin/**").hasRole("ADMIN") // 管理员路径
                .pathMatchers("/user/**").hasAnyRole("USER", "ADMIN") // 用户路径
                .anyExchange().authenticated() // 其他路径需认证
                .and()
                // 2. 添加 JWT 过滤器(在默认认证过滤器之前)
                .addFilterAt(jwtFilter, SecurityWebFiltersOrder.AUTHENTICATION)
                // 3. 禁用不需要的功能
                .csrf().disable() // 无状态服务可禁用 CSRF
                .formLogin().disable() // 禁用表单登录
                .httpBasic().disable() // 禁用 HTTP Basic
                .build();
    }

    /**
     * 认证管理器(从用户详情服务获取用户信息)
     */
    @Bean
    public ReactiveAuthenticationManager authenticationManager(
            ReactiveUserDetailsService userDetailsService,
            PasswordEncoder passwordEncoder) {
        UserDetailsRepositoryReactiveAuthenticationManager manager =
                new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
        manager.setPasswordEncoder(passwordEncoder); // 设置密码编码器
        return manager;
    }

    /**
     * 密码编码器(生产环境必须使用加密方式)
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * JWT 转换器(从请求头解析并验证 JWT)
     */
    @Bean
    public ServerAuthenticationConverter jwtAuthenticationConverter(JwtUtil jwtUtil) {
        return exchange -> {
            // 1. 从请求头获取 Token
            String token = extractToken(exchange);
            if (token == null) {
                return Mono.empty(); // 无 Token,返回空(后续会拒绝访问)
            }
            // 2. 验证 Token 并转换为认证信息
            return Mono.just(jwtUtil.validateTokenAndGetAuthentication(token));
        };
    }

    // 从请求头提取 Token(Bearer 格式)
    private String extractToken(ServerWebExchange exchange) {
        String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }
        return null;
    }
}

8.2 JWT 工具类实现

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;

@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secret; // 密钥(生产环境需复杂且保密)

    @Value("${jwt.expiration:86400000}") // 默认 24 小时
    private long expiration;

    private final UserDetailsService userDetailsService;

    // 构造器注入(省略)

    // 生成 JWT
    public String generateToken(String username) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);

        Key key = Keys.hmacShaKeyFor(secret.getBytes()); // 基于密钥生成签名密钥

        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(key, SignatureAlgorithm.HS512) // 使用 HS512 签名
                .compact();
    }

    // 验证 JWT 并返回认证信息
    public Authentication validateTokenAndGetAuthentication(String token) {
        // 1. 解析 Token 并验证签名
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
                .build()
                .parseClaimsJws(token)
                .getBody();

        // 2. 获取用户名并加载用户详情
        String username = claims.getSubject();
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);

        // 3. 返回认证信息
        return new UsernamePasswordAuthenticationToken(
                userDetails, null, userDetails.getAuthorities());
    }
}

8.3 登录接口实现

import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/auth")
public class AuthController {

    private final ReactiveAuthenticationManager authManager;
    private final JwtUtil jwtUtil;

    // 构造器注入(省略)

    /**
     * 登录接口(生成 JWT)
     */
    @PostMapping("/login")
    public Mono<JwtResponse> login(@RequestBody LoginRequest request) {
        // 1. 构建认证令牌
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                request.getUsername(), request.getPassword());

        // 2. 认证用户(调用 Spring Security 认证管理器)
        return authManager.authenticate(token)
                // 3. 认证成功,生成 JWT 并返回
                .map(authentication -> new JwtResponse(
                        jwtUtil.generateToken(authentication.getName()),
                        "Bearer"
                ));
    }
}

// 登录请求 DTO
record LoginRequest(String username, String password) {}

// JWT 响应 DTO
record JwtResponse(String token, String tokenType) {}

8.4 权限控制与用户上下文

在控制器或服务中获取当前登录用户信息:

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/user")
public class UserContextController {

    /**
     * 获取当前登录用户信息
     */
    @GetMapping("/me")
    public Mono<UserInfoDTO> getCurrentUser() {
        // 从安全上下文获取认证信息
        return ReactiveSecurityContextHolder.getContext()
                .map(context -> context.getAuthentication())
                .map(Authentication::getPrincipal)
                .cast(UserDetails.class) // 转换为 UserDetails
                .map(userDetails -> new UserInfoDTO(
                        userDetails.getUsername(),
                        userDetails.getAuthorities().stream()
                                .map(auth -> auth.getAuthority())
                                .toList()
                ));
    }
}

核心工具

  • ReactiveSecurityContextHolder.getContext():响应式获取安全上下文,避免阻塞;
  • 上下文信息存储在 WebSession 中,适合分布式系统(需配合 Session 共享,如 Redis)。

九、限流策略:保护系统免受流量冲击

高并发场景下,限流是防止系统过载的关键手段,WebFlux 结合 Redis 可实现分布式限流。

9.1 令牌桶限流原理

令牌桶算法通过以下机制控制流量:

  1. 系统以固定速率(如 100 个/秒)向桶中添加令牌;
  2. 每个请求需从桶中获取 1 个令牌才能被处理;
  3. 若桶中无令牌,请求被拒绝(返回 429 Too Many Requests)。

Redis + Lua 可实现分布式令牌桶,确保多实例环境下的限流一致性。

9.2 分布式限流实现

import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.time.Instant;
import java.util.Collections;

@Component
public class RedisTokenBucketLimiter {

    private final ReactiveRedisTemplate<String, Object> redisTemplate;
    private final RedisScript<Long> tokenBucketScript;

    // 构造器注入(加载 Lua 脚本)
    public RedisTokenBucketLimiter(ReactiveRedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
        // 加载 Lua 脚本(内容见下文)
        this.tokenBucketScript = RedisScript.of(
                new ClassPathResource("lua/token_bucket.lua"),
                Long.class
        );
    }

    /**
     * 判断请求是否允许通过
     * @param key 限流键(如:接口名、用户 ID、IP)
     * @param capacity 令牌桶容量
     * @param rate 令牌生成速率(个/秒)
     */
    public Mono<Boolean> allowRequest(String key, int capacity, int rate) {
        String bucketKey = "rate_limit:" + key;
        long now = Instant.now().getEpochSecond(); // 当前时间戳(秒)

        // 执行 Lua 脚本(确保原子性)
        return redisTemplate.execute(
                tokenBucketScript,
                Collections.singletonList(bucketKey),
                String.valueOf(capacity),
                String.valueOf(rate),
                String.valueOf(now)
        ).map(result -> result == 1L); // 1=允许,0=拒绝
    }
}

Lua 脚本(token_bucket.lua)

-- 令牌桶限流逻辑(确保原子性)
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

-- 1. 初始化桶(首次访问)
local bucket = redis.call('hgetall', key)
if table.getn(bucket) == 0 then
    redis.call('hset', key, 'tokens', capacity, 'last_refill', now)
    bucket = { 'tokens', capacity, 'last_refill', now }
end

-- 2. 解析桶数据
local tokens = tonumber(bucket[2])
local lastRefill = tonumber(bucket[4])

-- 3. 计算新生成的令牌(根据时间差)
local elapsed = now - lastRefill
local newTokens = math.min(capacity, tokens + elapsed * rate)

-- 4. 尝试消耗令牌
if newTokens >= 1 then
    redis.call('hset', key, 'tokens', newTokens - 1, 'last_refill', now)
    return 1 -- 允许
else
    redis.call('hset', key, 'tokens', newTokens, 'last_refill', now)
    return 0 -- 拒绝
end

9.3 限流过滤器

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

@Component
@Order(-100) // 优先于其他过滤器执行
public class RateLimitFilter implements WebFilter {

    private final RedisTokenBucketLimiter limiter;

    @Value("${rate.limit.global.capacity:1000}")
    private int globalCapacity;

    @Value("${rate.limit.global.rate:100}")
    private int globalRate;

    // 构造器注入(省略)

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        // 1. 确定限流键(示例:按接口路径限流)
        String path = exchange.getRequest().getPath().value();
        String limitKey = "api:" + path;

        // 2. 检查是否允许请求
        return limiter.allowRequest(limitKey, globalCapacity, globalRate)
                .flatMap(allowed -> {
                    if (allowed) {
                        return chain.filter(exchange); // 允许通过
                    } else {
                        // 3. 限流拒绝(返回 429)
                        exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
                        return exchange.getResponse().setComplete();
                    }
                });
    }
}

扩展场景

  • 按用户 ID 限流:limitKey = "user:" + userId
  • 按 IP 限流:limitKey = "ip:" + clientIp
  • 不同接口不同策略:通过配置文件动态加载每个接口的 capacity 和 rate。

十、缓存设计:响应式缓存的高效实现

在高并发场景中,缓存是提升系统性能的关键手段。WebFlux 结合响应式 Redis 客户端,可实现全链路非阻塞的缓存操作,避免传统缓存因阻塞导致的性能瓶颈。

10.1 响应式缓存配置

使用 Redis 作为缓存存储,需配置响应式缓存管理器:

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableCaching // 启用缓存注解
public class ReactiveCacheConfig {

    /**
     * 配置 Redis 缓存管理器(响应式)
     */
    @Bean
    public RedisCacheManager cacheManager(ReactiveRedisConnectionFactory connectionFactory) {
        // 1. 默认缓存配置
        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10)) // 默认缓存过期时间
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer())) // 键序列化
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer())) // 值序列化(JSON)
                .disableCachingNullValues(); // 不缓存 null 值

        // 2. 自定义缓存配置(针对不同缓存名设置不同过期时间)
        Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
        configMap.put("userCache", defaultConfig.entryTtl(Duration.ofHours(1))); // 用户缓存 1 小时
        configMap.put("productCache", defaultConfig.entryTtl(Duration.ofMinutes(30))); // 商品缓存 30 分钟

        // 3. 构建缓存管理器
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(defaultConfig)
                .withInitialCacheConfigurations(configMap)
                .build();
    }
}

10.2 缓存注解的使用

Spring 的缓存注解(@Cacheable@CachePut@CacheEvict)在 WebFlux 中同样适用,但方法返回值需为 MonoFlux

import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

@Service
public class CachedUserService {

    private final UserRepository userRepository;

    // 构造器注入(省略)

    /**
     * 查询用户并缓存(key 为 userId)
     */
    @Cacheable(value = "userCache", key = "#userId")
    public Mono<User> getUserById(Long userId) {
        // 方法执行时会先查缓存,缓存未命中才执行方法体
        return userRepository.findById(userId)
                .doOnNext(user -> System.out.println("从数据库查询用户: " + userId)); // 用于验证缓存是否生效
    }

    /**
     * 更新用户并刷新缓存
     */
    @CachePut(value = "userCache", key = "#user.id") // 更新缓存(key 与查询一致)
    public Mono<User> updateUser(User user) {
        return userRepository.save(user);
    }

    /**
     * 删除用户并清除缓存
     */
    @CacheEvict(value = "userCache", key = "#id") // 删除指定 key 的缓存
    public Mono<Void> deleteUser(Long id) {
        return userRepository.deleteById(id);
    }

    /**
     * 清除所有用户缓存(慎用)
     */
    @CacheEvict(value = "userCache", allEntries = true) // 清除整个 userCache 缓存
    public Mono<Void> clearAllUserCache() {
        return Mono.empty(); // 无需业务逻辑,仅用于触发缓存清除
    }
}

注意事项

  • @Cacheable 注解的方法必须返回 MonoFlux,否则缓存不会生效;
  • 缓存 key 的 SpEL 表达式需正确匹配参数(如 #userId#user.id);
  • @CacheEvict(allEntries = true) 会清除指定缓存名的所有数据,适合批量更新场景。

10.3 手动缓存控制(高级场景)

对于复杂缓存逻辑(如条件缓存、缓存预热),可直接使用 ReactiveRedisTemplate 手动操作:

import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.time.Duration;

@Service
public class ManualCacheService {

    private final ReactiveRedisTemplate<String, User> redisTemplate;
    private final UserRepository userRepository;

    // 构造器注入(省略)

    /**
     * 手动实现 "查询-缓存" 逻辑
     */
    public Mono<User> getAndCacheUser(Long userId) {
        String cacheKey = "userCache:" + userId;

        // 1. 先查缓存
        return redisTemplate.opsForValue().get(cacheKey)
                // 2. 缓存命中:直接返回
                .switchIfEmpty(
                    // 3. 缓存未命中:查数据库并写入缓存
                    userRepository.findById(userId)
                        .flatMap(user ->
                            // 写入缓存(设置过期时间)
                            redisTemplate.opsForValue().set(cacheKey, user, Duration.ofHours(1))
                                .thenReturn(user)
                        )
                );
    }

    /**
     * 条件缓存(仅缓存 VIP 用户)
     */
    public Mono<User> cacheVipUserOnly(User user) {
        if (user.isVip()) { // 仅 VIP 用户需要缓存
            String cacheKey = "vipUserCache:" + user.getId();
            return redisTemplate.opsForValue().set(cacheKey, user, Duration.ofDays(1))
                    .thenReturn(user);
        }
        return Mono.just(user); // 非 VIP 用户不缓存
    }
}

十一、OpenFeign 远程调用:响应式环境下的服务通信

微服务架构中,服务间远程调用不可避免。OpenFeign 作为声明式 HTTP 客户端,可简化调用代码,但需注意其阻塞特性对 WebFlux 的影响。

11.1 OpenFeign 配置与使用

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * Feign 客户端(调用远程用户服务)
 */
@FeignClient(name = "user-service", url = "${service.user.url:<http://localhost:8082>}")
public interface UserServiceClient {

    /**
     * 远程查询用户信息
     */
    @GetMapping("/api/users/{id}")
    UserDTO getUserById(@PathVariable("id") Long id);
}

在 WebFlux 中使用 Feign 客户端

import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

@Service
public class RemoteUserService {

    private final UserServiceClient userClient;

    // 构造器注入(省略)

    /**
     * 调用远程服务(将阻塞调用包装为响应式)
     */
    public Mono<UserDTO> getRemoteUser(Long userId) {
        // 注意:Feign 调用是阻塞的,需切换到弹性线程池执行,避免阻塞事件循环
        return Mono.fromSupplier(() -> userClient.getUserById(userId))
                .subscribeOn(Schedulers.boundedElastic()); // 使用弹性线程池(适合阻塞操作)
    }
}

关键注意点

  • Feign 客户端默认使用阻塞 IO,必须通过 subscribeOn(Schedulers.boundedElastic()) 将调用切换到专用线程池,防止阻塞 WebFlux 的事件循环;
  • 对于高并发场景,建议使用响应式 HTTP 客户端(如 WebClient)替代 Feign,避免线程阻塞。

11.2 WebClient 替代方案(推荐)

WebClient 是 WebFlux 内置的响应式 HTTP 客户端,更适合响应式环境:

import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Service
public class ReactiveRemoteUserService {

    private final WebClient webClient;

    // 构造器注入 WebClient(配置基础 URL)
    public ReactiveRemoteUserService(@Value("${service.user.url:<http://localhost:8082>}") String baseUrl) {
        this.webClient = WebClient.builder()
                .baseUrl(baseUrl)
                .build();
    }

    /**
     * 响应式远程调用
     */
    public Mono<UserDTO> getRemoteUser(Long userId) {
        return webClient.get()
                .uri("/api/users/{id}", userId)
                .retrieve()
                .bodyToMono(UserDTO.class)
                .onErrorResume(e -> {
                    // 远程调用失败处理(如返回默认值或重试)
                    return Mono.error(new RuntimeException("远程服务调用失败", e));
                });
    }
}

WebClient 优势

  • 全响应式非阻塞,不阻塞事件循环线程;
  • 支持背压,自动调节请求速率;
  • 内置重试、超时、错误处理机制。

十二、MQ 接入:响应式消息通信

消息队列(MQ)是解耦服务、削峰填谷的关键组件。WebFlux 项目中应使用响应式 MQ 客户端,避免阻塞操作。

12.1 RabbitMQ 响应式集成

以 RabbitMQ 为例,使用 spring-rabbit-reactive 实现响应式消息处理:

import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * RabbitMQ 队列配置
 */
@Configuration
public class RabbitMQConfig {

    // 定义队列(持久化)
    @Bean
    public Queue userEventQueue() {
        return new Queue("user.event.queue", true, false, false);
    }
}

12.1.1 响应式生产者

import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.ReactiveRabbitTemplate;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
public class ReactiveRabbitProducer {

    private final ReactiveRabbitTemplate rabbitTemplate;

    // 构造器注入(基于 ConnectionFactory 创建模板)
    public ReactiveRabbitProducer(ConnectionFactory connectionFactory) {
        this.rabbitTemplate = new ReactiveRabbitTemplate(connectionFactory);
    }

    /**
     * 发送用户事件消息
     */
    public Mono<Boolean> sendUserEvent(UserEvent event) {
        // 发送消息并等待确认(响应式)
        return rabbitTemplate.convertAndSend("user.event.queue", event)
                .thenReturn(true) // 发送成功返回 true
                .onErrorResume(e -> {
                    // 发送失败处理
                    System.err.println("消息发送失败: " + e.getMessage());
                    return Mono.just(false);
                });
    }
}

// 用户事件实体
@Data
class UserEvent {
    private Long userId;
    private String eventType; // 如:REGISTER、UPDATE、DELETE
    private LocalDateTime timestamp;
}

12.1.2 响应式消费者

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
public class ReactiveRabbitConsumer {

    private final UserService userService;

    // 构造器注入(省略)

    /**
     * 消费用户事件消息(响应式处理)
     */
    @RabbitListener(queues = "user.event.queue")
    public Mono<Void> handleUserEvent(UserEvent event) {
        // 根据事件类型处理业务(响应式)
        return switch (event.getEventType()) {
            case "REGISTER" -> handleUserRegister(event.getUserId());
            case "UPDATE" -> handleUserUpdate(event.getUserId());
            case "DELETE" -> handleUserDelete(event.getUserId());
            default -> Mono.empty(); // 忽略未知事件
        };
    }

    private Mono<Void> handleUserRegister(Long userId) {
        return userService.getUserById(userId)
                .doOnNext(user -> System.out.println("处理用户注册事件: " + user.getName()))
                .then(); // 转换为 Mono<Void>
    }

    // 其他事件处理方法(省略)
}

响应式消费特点

  • 消费者方法返回 Mono<Void>,表示异步处理完成;
  • 消息确认机制:默认自动确认(处理完成后确认),可配置为手动确认;
  • 支持背压:当处理能力不足时,会减少从队列拉取消息的速率。

十三、AOP 切面:响应式方法的增强

WebFlux 支持 AOP 切面编程,可用于日志记录、性能监控、异常处理等,但需注意切面方法必须返回响应式类型(Mono/Flux)。

13.1 响应式日志切面

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.lang.reflect.Method;

@Aspect
@Component
public class ReactiveLoggingAspect {

    private static final Logger log = LoggerFactory.getLogger(ReactiveLoggingAspect.class);

    /**
     * 环绕通知:记录方法调用日志
     */
    @Around("execution(* com.example.service..*(..)) || execution(* com.example.controller..*(..))")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 获取方法信息
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        String className = method.getDeclaringClass().getSimpleName();
        String methodName = method.getName();

        // 2. 记录入参
        Object[] args = joinPoint.getArgs();
        log.info("[{}#{}] 入参: {}", className, methodName, args);

        // 3. 执行目标方法并计时
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed(); // 执行方法
        long costTime = System.currentTimeMillis() - startTime;

        // 4. 处理响应式返回值(根据类型添加日志)
        if (result instanceof Mono) {
            // 对 Mono 结果添加日志(doOnSuccess 不阻塞流)
            return ((Mono<?>) result).doOnSuccess(retVal ->
                log.info("[{}#{}] 出参: {}, 耗时: {}ms", className, methodName, retVal, costTime)
            );
        } else if (result instanceof Flux) {
            // 对 Flux 结果添加日志(doOnComplete 表示流结束)
            return ((Flux<?>) result).doOnComplete(() ->
                log.info("[{}#{}] 流处理完成, 耗时: {}ms", className, methodName, costTime)
            );
        } else {
            // 非响应式返回值(如集成 MyBatis 时)
            log.info("[{}#{}] 出参: {}, 耗时: {}ms", className, methodName, result, costTime);
            return result;
        }
    }
}

切面注意事项

  • 环绕通知的返回值必须与目标方法一致(Mono/Flux),否则会破坏响应式流;
  • 使用 doOnSuccessdoOnComplete 等操作符记录日志,避免阻塞流处理;
  • 切面逻辑应尽量轻量化,避免影响主流程性能。

十四、测试策略:响应式代码的验证方法

WebFlux 代码测试需使用响应式测试工具(如 reactor-test),确保异步逻辑的正确性。

14.1 控制器测试

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;

import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

@WebFluxTest(UserController.class) // 仅测试 UserController
public class UserControllerTest {

    @Autowired
    private WebTestClient webClient; // 响应式测试客户端

    @MockBean
    private UserService userService; // 模拟服务层依赖

    @Test
    public void testGetUserById() {
        // 1. 准备测试数据
        Long userId = 1L;
        User mockUser = new User(userId, "TestUser", 25);

        // 2. 模拟服务层返回
        when(userService.getUserById(eq(userId))).thenReturn(Mono.just(mockUser));

        // 3. 发送请求并验证响应
        webClient.get()
                .uri("/api/users/{id}", userId)
                .exchange()
                .expectStatus().isOk() // 验证状态码 200
                .expectBody(User.class)
                .value(user -> { // 验证响应体
                    assert user.getId().equals(userId);
                    assert user.getName().equals("TestUser");
                });
    }

    @Test
    public void testUserNotFound() {
        // 1. 模拟服务层返回空(用户不存在)
        when(userService.getUserById(eq(999L))).thenReturn(Mono.empty());

        // 2. 验证返回 404
        webClient.get()
                .uri("/api/users/999")
                .exchange()
                .expectStatus().isNotFound();
    }
}

14.2 服务层测试

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository; // 模拟 Repository

    @InjectMocks
    private UserService userService; // 测试目标

    @Test
    public void testSaveUser_Success() {
        // 1. 准备数据
        User user = new User(null, "NewUser", 30);
        User savedUser = new User(1L, "NewUser", 30);

        // 2. 模拟 Repository
        when(userRepository.existsByName(eq("NewUser"))).thenReturn(Mono.just(false));
        when(userRepository.save(eq(user))).thenReturn(Mono.just(savedUser));

        // 3. 验证服务方法
        StepVerifier.create(userService.saveUser(user))
                .expectNext(savedUser) // 验证返回结果
                .verifyComplete(); // 验证流正常结束
    }

    @Test
    public void testSaveUser_DuplicateName() {
        // 1. 准备数据
        User user = new User(null, "Duplicate", 25);

        // 2. 模拟 Repository(用户名已存在)
        when(userRepository.existsByName(eq("Duplicate"))).thenReturn(Mono.just(true));

        // 3. 验证抛出异常
        StepVerifier.create(userService.saveUser(user))
                .expectError(IllegalArgumentException.class) // 验证异常类型
                .verify();
    }
}

测试工具说明

  • WebTestClient:用于测试控制器,模拟 HTTP 请求并验证响应;
  • StepVerifier:用于测试响应式流,验证流中的元素、顺序、异常等;
  • 测试时需通过 @MockBean@Mock 隔离外部依赖(如数据库、远程服务)。

十五、性能优化与最佳实践

15.1 线程模型优化

  • 避免阻塞操作:确保事件循环线程(reactor-http-nio)不执行阻塞操作(如 Thread.sleep、同步 IO);
  • 合理使用线程池:阻塞操作需切换到 boundedElastic 线程池(subscribeOn(Schedulers.boundedElastic()));
  • 虚拟线程结合:Java 19+ 可使用虚拟线程执行阻塞操作(Schedulers.fromExecutor(Executors.newVirtualThreadPerTaskExecutor()))。

15.2 背压控制

  • 处理大结果集时,使用 Flux.limitRate(n) 控制单次从数据源拉取的数据量;
  • 消费消息时,通过 prefetch 参数(如 RabbitMQ 的 prefetch)限制未确认消息数量。

15.3 资源管理

  • 连接池配置:R2DBC、Redis 连接池大小需根据 CPU 核心数调整(建议 CPU 核心数 * 2);
  • 超时设置:为数据库查询、远程调用设置合理超时(如 Mono.timeout(Duration.ofSeconds(5))),避免资源泄露。

15.4 监控与排查

  • 集成 Micrometer 监控响应式流指标(如 reactor.netty.http.server.requests);
  • 使用 Hooks.onOperatorDebug() 启用响应式操作符调试模式,便于定位异常堆栈。

十六、响应式和非响应式的对比

维度响应式(WebFlux)非响应式(Spring MVC)
线程模型事件循环(Event Loop)+ 工作线程每个请求一个线程(线程池)
并发能力高(固定线程处理大量请求)中(受线程池大小限制)
内存占用低(无线程上下文切换开销)高(线程栈占用 + 上下文切换)
适用场景I/O 密集型(微服务、API 网关、流处理)CPU 密集型(复杂计算)、简单业务
学习成本高(需理解响应式编程、背压、Mono/Flux)低(同步编程模型)
生态成熟度逐步完善(R2DBC、Reactive Redis)非常成熟(JDBC、MyBatis、Hibernate)

16.1 性能方面

  • 响应式(WebFlux):采用异步非阻塞的编程模型,能够以固定的线程处理高并发请求,充分发挥机器的性能,提升系统的伸缩性。在处理大量 I/O 密集型任务时,响应式编程可以避免线程阻塞,提高资源利用率。
  • 非响应式(Spring MVC):传统的同步阻塞式编程模型,每个请求都会占用一个线程,当并发请求过多时,线程数量会迅速增加,导致系统资源耗尽,性能下降。

16.2 编程复杂度方面

  • 响应式(WebFlux):需要掌握响应式编程的概念和相关的 API,如 Mono 和 Flux,编码和调试相对复杂。但对于处理复杂的异步场景,响应式编程可以提供更简洁的代码结构。
  • 非响应式(Spring MVC):编程模型较为简单,符合传统的编程思维,易于理解和维护。但在处理高并发和异步任务时,需要手动管理线程和异步操作,代码复杂度会增加。

16.3 适用场景方面

  • 响应式(WebFlux):适用于高并发、I/O 密集型的场景,如实时数据处理、服务器推送、微服务架构等。
  • 非响应式(Spring MVC):适用于对并发要求不高、业务逻辑相对简单的场景,如传统的 Web 应用开发。

十七、总结与展望

WebFlux 作为响应式编程的代表框架,在高并发、I/O 密集型场景中展现出显著优势。本文从实战角度覆盖了其核心应用场景:

流程

  • 控制器与过滤器:基于 Mono/Flux 的请求处理与拦截;
  • 数据访问:R2DBC 实现响应式数据库操作,支持事务与分页;
  • 安全与限流:JWT 认证结合 Redis 令牌桶限流,保障系统安全;
  • 缓存与通信:响应式缓存提升性能,WebClient 实现非阻塞远程调用;
  • 消息与测试:响应式 MQ 集成与针对性测试策略。
sequenceDiagram
    participant Client
    participant Filter
    participant Controller
    participant Service
    participant Database
    participant Redis
    participant RabbitMQ
    participant RemoteService

    Client->>Filter: 发送请求
    Filter->>Controller: 转发请求
    Controller->>Service: 调用服务方法
    alt 需要数据库操作
        Service->>Database: 执行数据库操作
        Database->>Service: 返回结果
    end
    alt 需要缓存操作
        Service->>Redis: 执行缓存操作
        Redis->>Service: 返回结果
    end
    alt 需要 MQ 操作
        Service->>RabbitMQ: 发送/接收消息
    end
    alt 需要远程调用
        Service->>RemoteService: 发起远程调用
        RemoteService->>Service: 返回结果
    end
    Service->>Controller: 返回服务结果
    Controller->>Client: 返回响应

未来趋势:随着 Java 虚拟线程的普及 Spring AI的春风,WebFlux 与虚拟线程的结合将进一步降低响应式编程的复杂度,同时保持高并发处理能力。开发者需根据项目场景(I/O 密集型 vs CPU 密集型)选择合适的技术栈,充分发挥 WebFlux 的性能潜力。

通过本文的实践指南,希望能帮助开发者在实际项目中熟练应用 WebFlux,构建高效、可扩展的响应式系统。

欢迎去参观我的博客:项目中 WebFlux 的全方位应用 | Honesty Blog

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值