mcpField
S
spring-webflux

spring-webflux

github.com/claude-dev-suite开发4698 次安装2 天前更新
代码仓库

关于 spring-webflux

面向 Spring WebFlux 开发者的参考技能,涵盖 Project Reactor 的 Mono、Flux、操作符及 WebClient。还提供函数式端点、错误处理、测试和响应式实践指导。

主要功能

  1. 01Mono 与 Flux 基础
  2. 02响应式操作符与错误处理
  3. 03WebClient 配置与调用
  4. 04函数式端点示例
  5. 05响应式测试与实践指导

适用场景

  1. 01构建响应式 Spring API
  2. 02处理异步数据流
  3. 03调用外部 API
  4. 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