非阻塞将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
2. Define a Custom Error Type (Recommended)
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

