【问题标题】:Spring webclient is not logging error response and performing consumer action on receiving errorSpring webclient 没有记录错误响应并在接收错误时执行消费者操作
【发布时间】:2026-02-18 02:45:02
【问题描述】:

我有一个 Spring Boot 服务,其中 API 通过 RouterFunction 公开。一旦收到 API 请求,就会触发某些验证。其中一个验证,通过 webclient 调用另一个 API 来验证接收到的值是否存在。如果 value 不存在,则必须记录错误消息并将错误消息添加到数组列表中。

但是,下面的实现既不记录错误也不记录成功消息,也不能在数组列表中添加错误消息。

我也尝试使用 block() 但这给出了

'IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-epoll-3'

如果我通过 RouterFunction 直接公开一个 API 只是为了调用该查找 API,那么一切正常。这表明通过验证层进行调用会导致问题,可能是同步和反应式调用方式存在问题。

我无法理解我的实现问题。请指导我做错了什么。

触发外部验证的验证类

@Slf4j
@Component
@RequiredArgsConstructor
public class EmployeeValidation{
    
    private final IdentityApiClient identityApiClient;
    
    public List<ErrorDetail> validate(Optional<EmployeeRequestDto> employeeRequest) {
        var errors = new ArrayList<ErrorDetail>();
        employeeRequest
                .ifPresent(employee -> {
                    //some other validations
                    if (errors.isEmpty()) {
                        validateIfIdentityExist(employee.getSecuredEmployeeDetail(), errors::add);
                    }
                });
        return errors;
    }
    
    private void validateIfIdentityExist(SecuredEmployeeDetailDto securedEmployeeDetailDto, Consumer<ErrorDetail> errorDetailConsumer) {
        Optional.ofNullable(securedEmployeeDetailDto)
                .map(SecuredEmployeeDetailDto::getIdentity)
                .ifPresent(identityLocal -> {
                    log.info("Going to retrieve identity [{}] detail", identityLocal);
                    identityApiClient.retrieveIdentityDetail(identityLocal)
                            .doOnError(e -> errorDetailConsumer.accept(new ErrorDetail(REQUEST_INVALID_PARAM, e.getMessage())));

                });
    }
}

Webclient 正在调用另一个 API 来验证值

@Slf4j
@Component
@RequiredArgsConstructor
public class IdentityApiClient {

    private final WebClient identityWebClient;
    private final IdentityProperties identityProperties;
    
    public Mono<IdentityDetail> retrieveIdentityDetail(String identity) {
        log.info("Going to retrieve identity [{}] detail", identity);
        return identityWebClient
                .get()
                .uri(identityProperties.getLookupPath(), Map.of("identity", identity))
                .retrieve()
                .onStatus(httpStatus -> httpStatus.equals(UNAUTHORIZED),
                        clientResponse -> clientResponse.bodyToMono(String.class)
                                .flatMap(identityLookUpErrorResponse -> Mono.error(new IdentityLookUpException(UNAUTHORIZED.value(), IdentityLookUpErrorResponse.builder()
                                        .error(identityLookUpErrorResponse)
                                        .message("Unauthorized Access")
                                        .status(UNAUTHORIZED.value())
                                        .build()))))
                .onStatus(HttpStatus::is4xxClientError,
                        clientResponse -> clientResponse.bodyToMono(IdentityLookUpErrorResponse.class)
                                .switchIfEmpty(Mono.just(IdentityLookUpErrorResponse.builder()
                                        .error("Received Empty Response Body")
                                        .message("Unknown Identity")
                                        .status(NOT_FOUND.value())
                                        .build()))
                                .flatMap(identityLookUpErrorResponse -> Mono.error(new IdentityLookUpException(identityLookUpErrorResponse.getStatus(), identityLookUpErrorResponse))))
                .onStatus(HttpStatus::is5xxServerError,
                        clientResponse -> clientResponse.bodyToMono(IdentityLookUpErrorResponse.class)
                                .switchIfEmpty(Mono.just(IdentityLookUpErrorResponse.builder()
                                        .error("Received Empty Response Body")
                                        .message(INTERNAL_SERVER_ERROR.getReasonPhrase())
                                        .status(INTERNAL_SERVER_ERROR.value())
                                        .build()))
                                .flatMap(identityLookUpErrorResponse -> Mono.error(new IdentityLookUpException(identityLookUpErrorResponse.getStatus(), identityLookUpErrorResponse))))
                .bodyToMono(IdentityDetail.class)
                .doOnNext(response -> log.info("Identity [{}] response received", response)) // not getting logged when called via validator class
                .doOnError(e -> log.error("Identity [{}] error response received", identity, e));// not getting logged when called via validator class
    }   
}

【问题讨论】:

  • 使用您最喜欢的 IDE,尝试深入调试模式以准确了解逐行发生的情况。此外,您在 EmployeeValidation 和 IdentityApiClient 类之间构建了一个高耦合代码。如果可能,请检查并重新设计。

标签: java spring-boot spring-webflux spring-webclient


【解决方案1】:

identityApiClient.retrieveIdentityDetail 方法返回一个未被任何人订阅的Mono。这段代码孤立地没有做任何事情。

identityApiClient.retrieveIdentityDetail(identityLocal)
                            .doOnError(e -> errorDetailConsumer.accept(new ErrorDetail(REQUEST_INVALID_PARAM, e.getMessage())));

响应式编程的口头禅是

“在您订阅之前什么都不会发生”

对于大多数用例,底层框架(在本例中为 spring)将为您订阅 Mono,只要您从 RouterFunction 返回它。

我并不完全熟悉您的要求,但一种可能的解决方案是重构您的验证器以使其具有响应性并将其添加到您的响应流中。

【讨论】:

  • 验证器已存在。我只需要使用 webclient 添加另一个身份验证。所以在不干扰验证器代码的情况下,我需要实现这个额外的验证。
  • 我不明白你的意思。 EmployeeValidation 类需要修复。如果你不能干扰这个代码,那么你就不能让代码工作
  • 我的意思是,是的,我们可以更改 EmployeeValidation 以添加这个额外的 IdentityValidator 但不能重构整个 EmployeeValidation 类,因为还包括某些其他验证。
  • @Michal McFadyen subscribe() 完成了部分工作,现在我可以看到日志和响应。但是,由于 subscribe() 是 async ,因此请求将继续创建员工,稍后我们会收到验证错误消息。任何关于如何停止和制作的建议都是同步的。
  • 关于如何解决这个问题的建议在上面的答案中。从您的方法中返回 Mono 并让底层框架订阅它。
最近更新 更多