一、引言:响应式编程与 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不熟悉的可先阅读
二、项目环境搭建:从依赖到配置的生产级实践
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.transferTo
和Flux.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>
后,自动获得save
、findById
、deleteById
等基础方法; - 方法名遵循 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); // 保存更新后的用户
});
}
}
响应式操作亮点:
- 方法返回值均为
Mono
或Flux
,确保操作链全程非阻塞; 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(如多条件组合查询),DatabaseClient
比 Repository
更灵活:
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 令牌桶限流原理
令牌桶算法通过以下机制控制流量:
- 系统以固定速率(如 100 个/秒)向桶中添加令牌;
- 每个请求需从桶中获取 1 个令牌才能被处理;
- 若桶中无令牌,请求被拒绝(返回 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 中同样适用,但方法返回值需为 Mono
或 Flux
:
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
注解的方法必须返回Mono
或Flux
,否则缓存不会生效;- 缓存 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
),否则会破坏响应式流; - 使用
doOnSuccess
、doOnComplete
等操作符记录日志,避免阻塞流处理; - 切面逻辑应尽量轻量化,避免影响主流程性能。
十四、测试策略:响应式代码的验证方法
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