使用 Spring boot + WebFlux 进行全局错误处理

Posted

技术标签:

【中文标题】使用 Spring boot + WebFlux 进行全局错误处理【英文标题】:Global error handling using Spring boot + WebFlux 【发布时间】:2019-11-08 16:38:01 【问题描述】:

在 Spring Boot Rest Controller 中使用响应式编程时如何全局处理异常?

我会假设@ControllerAdvice 将不起作用,因为我已经尝试过了,但没有成功。

我目前的另一个尝试是这个选项,使用自定义属性:

@Component
public class OsvcErrorAttributes extends DefaultErrorAttributes 
    public OsvcErrorAttributes() 
        super(true);
    

    @Override
    public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) 
        return assembleError(request);
    

    private Map<String, Object> assembleError(ServerRequest request) 
        ServerException serverException = (ServerException)getError(request);

        Map<String, Object> errorAttributes = new HashMap<>();
        errorAttributes.put("message", serverException.getMessage());
        errorAttributes.put("errors", serverException.getErrorMap());
        return errorAttributes;
    

和这样的 WebExceptionHandler:

@Component
@Order(-2)
public class OsvcErrorHandler extends AbstractErrorWebExceptionHandler 
    public OsvcErrorHandler(ErrorAttributes errorAttributes,
                            ResourceProperties resourceProperties,
                            ApplicationContext applicationContext) 
        super(errorAttributes, resourceProperties, applicationContext);

        // TODO: 25.06.2019 temporary workaround
        ServerCodecConfigurer serverCodecConfigurer = new DefaultServerCodecConfigurer();
        setMessageWriters(serverCodecConfigurer.getWriters());
        setMessageReaders(serverCodecConfigurer.getReaders());
    

    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) 
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    

    private Mono<ServerResponse> renderErrorResponse(ServerRequest serverRequest) 

        final Map<String, Object> errorAttributes = getErrorAttributes(serverRequest, true);
        return ServerResponse.status(HttpStatus.BAD_REQUEST)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(BodyInserters.fromObject(errorAttributes));
    

产生错误的代码:

@Data
@Service
public class ContactService 
    private final ContactRepository contactRepository;

    public Mono<Business> saveNewContact(Business business) 
        return contactRepository.save(business)
                .onErrorMap(throwable ->
                    ServerException.create(throwable.getMessage())
                        .persistError("ico", business.getIco(), "ICO is probably duplicate"));
    

问题是这也不起作用。我确实遵循了本教程,但我看不出我是否有什么问题。

【问题讨论】:

“问题是这也不起作用。”你能告诉什么不工作 @ThomasAndolf 这意味着配置的全局错误处理没有被触发 为什么你的ContactService 中有一个@Data。在 lombok 中,这意味着提供了一个 getter 和 setter,但 ContactRepository 需要是 @Autowired 应该有 RequireArgsConstructor 因为自动连接是在构造函数级别完成的,所以不需要 Autowired。然而,这不是问题。 如果你的应用程序中同时有 spring-boot-starter-web 和 spring-boot-starter-webflux 模块,它会导致 Spring Boot 自动配置 Spring MVC,而不是 WebFlux。在这种情况下,您的全局错误处理程序将不会被调用。如果将应用程序类型设置为 SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE)。它应该可以工作。 【参考方案1】:

尝试注入 ServerCodecConfigurer 而不是实例化它。执行此操作时,我还会注入 ViewResolversProvider,尽管可能没有必要。

    public OsvcErrorHandler(
            final CustomErrorAttributes customAttributes,
            final ResourceProperties resourceProperties,
            final ObjectProvider<List<ViewResolver>> viewResolversProvider,
            final ServerCodecConfigurer serverCodecConfigurer,
            final ApplicationContext applicationContext
    ) 
        super(customAttributes, resourceProperties, applicationContext);

        this.setViewResolvers(viewResolversProvider.getIfAvailable(Collections::emptyList));
        this.setMessageWriters(serverCodecConfigurer.getWriters());
        this.setMessageReaders(serverCodecConfigurer.getReaders());
    

【讨论】:

我必须创建 DefaultServerCodecConfigurer 的 Bean 才能注入 ServerCodecConfigurer 但仍然无法处理错误 我遇到了同样的问题,问题是我同时拥有 spring-boot-starter-web 和 spring-boot-starter-webflux 作为依赖项。 Spring 声明,如果您在类路径上同时具有 spring-web 和 spring-webflux,则应用程序将支持 spring-web,并且默认情况下会启动一个具有底层 tomcat 服务器的非反应式应用程序。这就是为什么没有 ServerCodecConfigurer 实例的原因。如果将应用程序类型设置为 SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE)。它应该可以工作。【参考方案2】:

您只需像这样在全局错误处理程序构造函数中使用 ServerCodecConfigurer 注入。

public OsvcErrorHandler(GlobalErrorAttributes errorAttributes, ApplicationContext applicationContext, 
                ServerCodecConfigurer serverCodecConfigurer) 
   super(errorAttributes, new ResourceProperties(), applicationContext);
   super.setMessageWriters(serverCodecConfigurer.getWriters());
   super.setMessageReaders(serverCodecConfigurer.getReaders());

请在 git repository 中找到代码示例。

【讨论】:

我已经尝试过了 -> 我必须创建 bean DefaultServerCodecConfigurer,因为没有 ServerCodecConfigurer 类型的 bean。仍然,没有运气。也许我缺少一些依赖 - 为此我有没有 web 的 spring-boot-webflux 这个例子可以在我的git仓库查看,与上一篇分享。 我得到 org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.http.codec.ServerCodecConfigurer' available: 预计至少有 1 个 bean 有资格作为自动装配候选者。依赖注释:请看我的代码github.com/Drezir/osvc 删除您的存储库缓存并再次运行 gradle clean build。这就像冲突任何 jar 文件一样。它对我有用。我已经使用了您的 ErrorAttributes 和 ErrorWebExceptionHandler 类。我正在使用 Java 1.8 和 gradle 4.10.3 版本。【参考方案3】:

你需要将ErrorWebExceptionHandler定义和实现为一个bean,并设置一个小于-1@Order注解,因为这是Spring的默认DefaultErrorWebExceptionHandler

这是一个示例实现:

public class GlobalErrorHandler extends DefaultErrorWebExceptionHandler 
    public GlobalErrorHandler(
            final ErrorAttributes errorAttributes,
            final WebProperties.Resources resources,
            final ErrorProperties errorProperties,
            final ApplicationContext applicationContext) 
        super(errorAttributes, resources, errorProperties, applicationContext);
    

    @Override
    public Mono<Void> handle(final ServerWebExchange exchange, final Throwable ex) 
        final ServerHttpResponse response = exchange.getResponse();
        if (ex instanceof IllegalStateException
                && StringUtils.equals("Session was invalidated", ex.getMessage())
                && response.getStatusCode().is3xxRedirection()) 
            final DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
            return response.writeWith(Mono.just(bufferFactory.wrap("Redirecting...".getBytes())));
        
        return super.handle(exchange, ex);
    

这是一个基于org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration 类的示例配置:

@Configuration
public class ErrorWebFluxAutoConfiguration 

    private final ServerProperties serverProperties;

    public ErrorWebFluxAutoConfiguration(final ServerProperties serverProperties) 
        this.serverProperties = serverProperties;
    

    @Bean
    @Order(-2)
    public ErrorWebExceptionHandler errorWebExceptionHandler(
            final ErrorAttributes errorAttributes,
            final org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties,
            final WebProperties webProperties,
            final ObjectProvider<ViewResolver> viewResolvers,
            final ServerCodecConfigurer serverCodecConfigurer,
            final ApplicationContext applicationContext) 
        final GlobalErrorHandler exceptionHandler =
                new GlobalErrorHandler(
                        errorAttributes,
                        resourceProperties.hasBeenCustomized()
                                ? resourceProperties
                                : webProperties.getResources(),
                        serverProperties.getError(),
                        applicationContext);
        exceptionHandler.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList()));
        exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
        exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
        return exceptionHandler;
    

    @Bean
    @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
    public DefaultErrorAttributes errorAttributes() 
        return new DefaultErrorAttributes();
    

感谢this 文章指出我使用ErrorWebExceptionHandler

【讨论】:

以上是关于使用 Spring boot + WebFlux 进行全局错误处理的主要内容,如果未能解决你的问题,请参考以下文章

使用 Spring boot + WebFlux 进行全局错误处理

Spring Boot Webflux/Netty - 检测关闭的连接

spring-boot-starter-web 和 spring-boot-starter-webflux 不能一起工作吗?

如何使用 Spring Boot 对 WebFlux 进行异常处理?

如何使用 WebFlux 在 Spring Boot 2 中设置登录页面?

如何使用WebFlux在Spring Boot 2中设置登录页面?