You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

非阻塞将Spring Webflux Mono转为Either?基于Kotlin与Arrow

Hey there! Let's tackle this problem step by step—converting a Spring WebFlux Mono to Arrow's Either and handling it with a fold-like approach makes total sense for clean, declarative error handling. Here's how you can do it properly:

1. First, Add Required Dependencies

You'll need Arrow's core library and its Reactor integration to bridge Mono and Either seamlessly. If you're using Gradle, add these to your build.gradle.kts:

implementation("io.arrow-kt:arrow-core:1.2.1")
implementation("io.arrow-kt:arrow-reactor:1.2.1") // Critical for Mono ↔ Either extensions

To make error handling more expressive, create a sealed class to encapsulate different error scenarios from your WebClient calls:

import org.springframework.http.HttpStatus

sealed class ApiError {
    data class HttpError(val status: HttpStatus, val details: String) : ApiError()
    data class GenericError(val message: String) : ApiError()
}

3. Convert WebClient's Mono to Mono<Either<ApiError, T>>

You can use Arrow's built-in either() extension to automatically wrap successful responses in Either.Right and exceptions in Either.Left, then map the raw Throwable to your custom ApiError:

import arrow.core.Either
import arrow.reactor.extensions.monokt.either
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.WebClientResponseException

data class Data(val id: String, val name: String)

val webClient = WebClient.create("https://api.example.com")

fun fetchData(): Mono<Either<ApiError, Data>> {
    return webClient.get()
        .uri("/v1/data")
        .retrieve()
        .bodyToMono(Data::class.java)
        .either() // Converts Mono<Data> to Mono<Either<Throwable, Data>>
        .mapLeft { ex ->
            // Map raw exceptions to your custom ApiError type
            when (ex) {
                is WebClientResponseException -> ApiError.HttpError(
                    ex.statusCode,
                    ex.responseBodyAsString ?: "No error details"
                )
                else -> ApiError.GenericError(ex.message ?: "Unknown error occurred")
            }
        }
}

If you prefer not to use Arrow's extension, you can manually handle success/error cases with map and onErrorResume:

fun fetchDataManual(): Mono<Either<ApiError, Data>> {
    return webClient.get()
        .uri("/v1/data")
        .retrieve()
        .bodyToMono(Data::class.java)
        .map { Either.Right(it) }
        .onErrorResume { ex ->
            val error = when (ex) {
                is WebClientResponseException -> ApiError.HttpError(ex.statusCode, ex.responseBodyAsString ?: "")
                else -> ApiError.GenericError(ex.message ?: "")
            }
            Mono.just(Either.Left(error))
        }
}

4. Implement the Fold-Like Behavior

Option A: Get a Non-Mono Result (Blocking)

If you need to convert the asynchronous Mono to a synchronous Either (and then fold it), use block() to resolve the Mono (note: only do this in non-reactive contexts like tests or initialization code—avoid blocking in WebFlux request threads!):

fun processResultSync(): String {
    val eitherResult = fetchData().block() ?: throw IllegalStateException("Mono completed without a result")
    
    return eitherResult.fold(
        onLeft = { error ->
            when (error) {
                is ApiError.HttpError -> "Request failed with status ${error.status}: ${error.details}"
                is ApiError.GenericError -> "Unexpected error: ${error.message}"
            }
        },
        onRight = { data -> "Successfully fetched data: ${data.name} (ID: ${data.id})" }
    )
}

Option B: Keep It Reactive (Non-Blocking)

If you want to stay within the reactive pipeline and avoid blocking, use flatMap to handle the Either without leaving Mono:

fun processResultReactive(): Mono<String> {
    return fetchData().flatMap { either ->
        either.fold(
            onLeft = { error ->
                Mono.just(
                    when (error) {
                        is ApiError.HttpError -> "HTTP Error ${error.status}: ${error.details}"
                        is ApiError.GenericError -> "Error: ${error.message}"
                    }
                )
            },
            onRight = { data ->
                Mono.just("Fetched data: ${data.name}")
            }
        )
    }
}

Key Notes

  • Avoid blocking in reactive flows: Using block() can defeat the purpose of WebFlux's non-blocking architecture. Only use it when you have no other choice (e.g., integrating with legacy synchronous code).
  • Arrow's extensions simplify boilerplate: The either() extension cuts down on manual error wrapping, making your code cleaner and more consistent.

内容的提问来源于stack exchange,提问作者Johan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 08:28:29