关于 spring-webflux
面向 Spring WebFlux 开发者的参考技能,涵盖 Project Reactor 的 Mono、Flux、操作符及 WebClient。还提供函数式端点、错误处理、测试和响应式实践指导。
主要功能
- 01Mono 与 Flux 基础
- 02响应式操作符与错误处理
- 03WebClient 配置与调用
- 04函数式端点示例
- 05响应式测试与实践指导
适用场景
- 01构建响应式 Spring API
- 02处理异步数据流
- 03调用外部 API
- 04排查背压与上下文传播问题
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill spring-webflux安装后在 Claude Code 中输入/spring-webflux确认已加载。
附带文件advanced.md
详细介绍
Spring WebFlux
完整参考:有关使用 Sinks 实现 SSE、使用 StepVerifier 进行测试以及 Context 传播模式,请参阅 advanced.md。
快速入门
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public Mono<UserResponse> getUser(@PathVariable Long id) {
return userService.findById(id);
}
@GetMapping
public Flux<UserResponse> getAllUsers() {
return userService.findAll();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<UserResponse> createUser(@RequestBody Mono<CreateUserRequest> request) {
return request.flatMap(userService::create);
}
}
Mono 和 Flux 基础
Mono(0 或 1 个元素)
Mono<String> empty = Mono.empty();
Mono<String> just = Mono.just("Hello");
Mono<String> fromCallable = Mono.fromCallable(() -> expensiveOperation());
Mono<String> defer = Mono.defer(() -> Mono.just(dynamicValue()));
Mono<String> fromOptional = Mono.justOrEmpty(optionalValue);
Mono<String> fromFuture = Mono.fromFuture(completableFuture);
Flux(0 到 N 个元素)
Flux<Integer> just = Flux.just(1, 2, 3);
Flux<Integer> fromIterable = Flux.fromIterable(List.of(1, 2, 3));
Flux<Integer> range = Flux.range(1, 10);
Flux<Long> interval = Flux.interval(Duration.ofSeconds(1));
Flux<Integer> generate = Flux.generate(
() -> 0,
(state, sink) -> {
sink.next(state);
if (state == 10) sink.complete();
return state + 1;
}
);
响应式运算符
转换
// map - transform each element
users.map(user -> new UserResponse(user.getId(), user.getName()));
// flatMap - async transformation (parallel)
users.flatMap(user -> orderRepository.findByUserId(user.getId()));
// flatMapSequential - maintains order
users.flatMapSequential(user -> orderRepository.findByUserId(user.getId()));
// concatMap - sequential, one at a time
users.concatMap(user -> orderRepository.findByUserId(user.getId()));
// switchMap - cancels previous when new arrives
searchTerms.switchMap(term -> searchService.search(term));
过滤
// filter
users.filter(user -> user.getStatus() == Status.ACTIVE);
// filterWhen - async filter
users.filterWhen(user -> permissionService.canAccess(user.getId()));
// distinct / distinctUntilChanged
items.distinct();
values.distinctUntilChanged();
// take / skip
users.skip((long) page * size).take(size);
组合
// zip - combine by position
Flux.zip(users, orders, UserWithOrders::new);
// merge - interleave from multiple sources
Flux.merge(source1, source2);
// concat - sequential
Flux.concat(first, second);
// zipWith on Mono
userRepository.findById(userId)
.zipWith(profileRepository.findByUserId(userId))
.map(tuple -> new UserWithProfile(tuple.getT1(), tuple.getT2()));
错误处理
// onErrorReturn - default value on error
userRepository.findById(id).onErrorReturn(new User("default"));
// onErrorResume - fallback Publisher
primaryRepository.findById(id)
.onErrorResume(e -> fallbackRepository.findById(id));
// onErrorResume with specific type
userRepository.findById(id)
.onErrorResume(NotFoundException.class, e -> Mono.empty())
.onErrorResume(TimeoutException.class, e -> cacheRepository.findById(id));
// onErrorMap - transform exception
userRepository.findById(id)
.onErrorMap(DataAccessException.class,
e -> new ServiceException("Database error", e));
// retryWhen - advanced retry
userRepository.findById(id)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
.filter(e -> e instanceof TransientException));
副作用
userRepository.findById(id)
.doOnSubscribe(s -> log.info("Subscribed"))
.doOnNext(user -> log.info("Found user: {}", user.getId()))
.doOnError(e -> log.error("Error: {}", e.getMessage()))
.doFinally(signalType -> log.info("Finally: {}", signalType));
WebClient
配置
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(WebClient.Builder builder) {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofSeconds(30))
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(30, TimeUnit.SECONDS))
.addHandlerLast(new WriteTimeoutHandler(10, TimeUnit.SECONDS)));
return builder
.baseUrl("https://api.example.com")
.clientConnector(new ReactorClientHttpConnector(httpClient))
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}
使用
@Service
@RequiredArgsConstructor
public class ExternalApiService {
private final WebClient webClient;
public Mono<UserDto> getUser(Long id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(UserDto.class);
}
public Mono<UserDto> getUserSafe(Long id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, response ->
response.bodyToMono(ErrorResponse.class)
.flatMap(error -> Mono.error(new ClientException(error.getMessage()))))
.onStatus(HttpStatusCode::is5xxServerError, response ->
Mono.error(new ServerException("Server error")))
.bodyToMono(UserDto.class);
}
// Parallel calls
public Mono<AggregatedData> getAggregatedData(Long userId) {
return Mono.zip(
getUser(userId),
getOrders(userId).collectList(),
getProfile(userId)
).map(tuple -> new AggregatedData(tuple.getT1(), tuple.getT2(), tuple.getT3()));
}
}
函数式端点
@Configuration
public class RouterConfig {
@Bean
public RouterFunction<ServerResponse> userRoutes(UserHandler handler) {
return RouterFunctions.route()
.path("/api/users", builder -> builder
.GET("", handler::getAll)
.GET("/{id}", handler::getById)
.POST("", handler::create)
.PUT("/{id}", handler::update)
.DELETE("/{id}", handler::delete)
)
.build();
}
}
@Component
@RequiredArgsConstructor
public class UserHandler {
private final UserService userService;
public Mono<ServerResponse> getById(ServerRequest request) {
Long id = Long.parseLong(request.pathVariable("id"));
return userService.findById(id)
.flatMap(user -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(user))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> create(ServerRequest request) {
return request.bodyToMono(CreateUserRequest.class)
.flatMap(userService::create)
.flatMap(user -> ServerResponse.created(
URI.create("/api/users/" + user.getId()))
.bodyValue(user));
}
}
最佳实践
| 应做事项 | 不应做事项 |
|---|---|
| 保持链路完全响应式 | 在处理器中使用 .block() |
| 使用合适的运算符(flatMap 与 concatMap) | 混用阻塞式和响应式代码 |
| 使用 onError* 运算符处理错误 | 忽略错误 |
| 使用 StepVerifier 进行测试 | 使用 .block() 测试 |
| 传播 Context,以用于 MDC/安全机制 | 使用 ThreadLocal |
生产环境检查清单
- 为 WebClient 配置超时
- 完成错误处理
- 为瞬时错误设置重试逻辑
- 定义背压策略
- 为日志记录传播 Context
- 配置响应式指标
- 使用 StepVerifier 进行测试
不适合使用此技能的情况
- 传统阻塞式应用 - 使用
spring-web技能 - 简单的 CRUD API - 使用
spring-rest技能 - 受 CPU 限制的工作负载 - 响应式编程在此无济于事
- 团队不熟悉响应式编程 - 学习曲线较陡
反模式
| 反模式 | 问题 | 解决方案 |
|---|---|---|
| 没有任何内容执行 | Publisher 未订阅 | 确保调用 subscribe,或从控制器返回 |
| 阻塞调用 | 在响应式链中使用 .block() | 避免使用 block,改用运算符 |
| Context 丢失 | MDC 未传播 | 使用 Context 传播 |
| 内存泄漏 | 无限 Flux 没有背压 | 使用背压运算符 |
| 混淆冷与热 | 每次订阅都会重新创建 Publisher | 使用 .share() 或 .cache() |
快速故障排查
| 问题 | 诊断 | 修复 |
|---|---|---|
| Mono/Flux 始终无法完成 | 检查是否缺少 subscribe | 从控制器返回 |
| Context 不可用 | 检查传播情况 | 使用 Hooks.enableAutomaticContextPropagation() |
| 背压溢出 | 检查缓冲区大小 | 使用 onBackpressure* 运算符 |
| 测试超时 | 检查 StepVerifier 的用法 | 对延迟使用虚拟时间 |
| 内存持续增长 | 检查是否存在泄漏 | 使用 .limitRate() 或 .take() |
参考文档
相关技能
spring-r2dbc- 用于响应式数据库访问spring-web- 用于与 MVC 对比spring-websocket- 用于响应式 WebSocket
评论
支持 Markdown