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 Propagation のパターンについては、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() でテストする
MDC/セキュリティのために Context を伝播させるThreadLocal を使う

本番環境向けチェックリスト

  • WebClient にタイムアウトを設定
  • エラー処理を完備
  • 一時的なエラーに対する再試行ロジック
  • バックプレッシャー戦略を定義
  • ロギングのための Context 伝播
  • リアクティブメトリクス
  • StepVerifier でテスト

このスキルを使用しない場合

  • 従来型のブロッキングアプリ - spring-web スキルを使用
  • シンプルな CRUD API - spring-rest スキルを使用
  • CPU バウンドのワークロード - ここではリアクティブ処理は役に立ちません
  • リアクティブ処理に不慣れなチーム - 学習の難易度が高いです

アンチパターン

アンチパターン問題解決策
何も実行されないPublisher がサブスクライブされていないsubscribe を確実に行うか、コントローラーから返す
ブロッキング呼び出しリアクティブチェーン内の .block()block を避け、オペレーターを使う
Context が失われるMDC が伝播されないContext 伝播を使う
メモリリークバックプレッシャーのない無限 Fluxバックプレッシャーオペレーターを使う
Cold と Hot の混同サブスクライブのたびに Publisher が再作成される.share() または .cache() を使う

トラブルシューティングのクイックガイド

問題診断修正方法
Mono/Flux が完了しないsubscribe の不足を確認コントローラーから返す
Context を利用できない伝播を確認Hooks.enableAutomaticContextPropagation() を使う
バックプレッシャーのオーバーフローバッファーサイズを確認onBackpressure* オペレーターを使う
テストがタイムアウトするStepVerifier の使い方を確認遅延には仮想時間を使う
メモリ使用量が増え続けるリークを確認.limitRate() または .take() を使う

リファレンスドキュメント

関連スキル

  • spring-r2dbc - リアクティブなデータベースアクセス用
  • spring-web - MVC との比較用
  • spring-websocket - リアクティブな WebSocket 用

コメント