Spring Boot 2 Webflux的全局异常处理

https://www.jianshu.com/p/6f631f3e00b9

本文首先将会回顾Spring 5之前的SpringMVC异常处理机制,然后主要讲解Spring Boot 2 Webflux的全局异常处理机制。

SpringMVC的异常处理

Spring 统一异常处理有 3 种方式,分别为:

  • 使用 @ExceptionHandler 注解
  • 实现 HandlerExceptionResolver 接口
  • 使用 @controlleradvice 注解

使用@ExceptionHandler注解

用于局部方法捕获,与抛出异常的方法处于同一个Controller类:

@Controller
public class BuzController {

    @ExceptionHandler({NullPointerException.class})
    public String exception(NullPointerException e) {
        System.out.println(e.getMessage());
        e.printStackTrace();
        return "null pointer exception";
    }

    @RequestMapping("test")
    public void test() {
        throw new NullPointerException("出错了!");
    }
}

如上的代码实现,针对BuzController抛出的NullPointerException异常,将会捕获局部异常,返回指定的内容。

实现HandlerExceptionResolver接口

通过实现HandlerExceptionResolver接口,这里我们通过继承SimpleMappingExceptionResolver实现类(HandlerExceptionResolver实现,允许将异常类名称映射到视图名称,既可以是一组给定的handlers处理程序,也可以是DispatcherServlet中的所有handlers)定义全局异常:

@Component
public class CustomMvcExceptionHandler extends SimpleMappingExceptionResolver {

    private ObjectMapper objectMapper;

    public CustomMvcExceptionHandler() {
        objectMapper = new ObjectMapper();
    }

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
                                         Object o, Exception ex) {
        response.setStatus(200);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Cache-Control", "no-cache, must-revalidate");
        Map<String, Object> map = new HashMap<>();
        if (ex instanceof NullPointerException) {
            map.put("code", ResponseCode.NP_EXCEPTION);
        } else if (ex instanceof IndexOutOfBoundsException) {
            map.put("code", ResponseCode.INDEX_OUT_OF_BOUNDS_EXCEPTION);
        } else {
            map.put("code", ResponseCode.CATCH_EXCEPTION);
        }
        try {
            map.put("data", ex.getMessage());
            response.getWriter().write(objectMapper.writeValueAsString(map));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new ModelAndView();
    }
}

如上为示例的使用方式,我们可以根据各种异常定制错误的响应。

使用@controlleradvice注解

@ControllerAdvice
public class ExceptionController {
    @ExceptionHandler(RuntimeException.class)
    public ModelAndView handlerRuntimeException(RuntimeException ex) {
        if (ex instanceof MaxUploadSizeExceededException) {
            return new ModelAndView("error").addObject("msg", "文件太大!");
        }
        return new ModelAndView("error").addObject("msg", "未知错误:" + ex);
    }

    @ExceptionHandler(Exception.class)
    public ModelAndView handlerMaxUploadSizeExceededException(Exception ex) {
        if (ex != null) {
            return new ModelAndView("error").addObject("msg", ex);
        }

        return new ModelAndView("error").addObject("msg", "未知错误:" + ex);

    }
}

和第一种方式的区别在于,ExceptionHandler的定义和异常捕获可以扩展到全局。

Spring 5 Webflux的异常处理

webflux支持mvc的注解,是一个非常便利的功能,相比较于RouteFunction,自动扫描注册比较省事。异常处理可以沿用ExceptionHandler。如下的全局异常处理对于RestController依然生效。

@RestControllerAdvice
public class CustomExceptionHandler {
    private final Log logger = LogFactory.getLog(getClass());

    @ExceptionHandler(Exception.class)
    @ResponseStatus(code = HttpStatus.OK)
    public ErrorCode handleCustomException(Exception e) {
        logger.error(e.getMessage());
        return new ErrorCode("e","error" );
    }
}

WebFlux示例

WebFlux提供了一套函数式接口,可以用来实现类似MVC的效果。我们先接触两个常用的。

Controller定义对Request的处理逻辑的方式,主要有方面:

  • 方法定义处理逻辑;
  • 然后用@RequestMapping注解定义好这个方法对什么样url进行响应。

在WebFlux的函数式开发模式中,我们用HandlerFunction和RouterFunction来实现上边这两点。

HandlerFunction

HandlerFunction相当于Controller中的具体处理方法,输入为请求,输出为装在Mono中的响应:

    Mono<T> handle(ServerRequest var1);

在WebFlux中,请求和响应不再是WebMVC中的ServletRequest和ServletResponse,而是ServerRequest和ServerResponse。后者是在响应式编程中使用的接口,它们提供了对非阻塞和回压特性的支持,以及Http消息体与响应式类型Mono和Flux的转换方法。

@Component
public class TimeHandler {
    public Mono<ServerResponse> getTime(ServerRequest serverRequest) {
        String timeType = serverRequest.queryParam("type").get();
        //return ...
    }
}

如上定义了一个TimeHandler,根据请求的参数返回当前时间。

RouterFunction

RouterFunction,顾名思义,路由,相当于@RequestMapping,用来判断什么样的url映射到那个具体的HandlerFunction。输入为请求,输出为Mono中的Handlerfunction

Mono<HandlerFunction<T>> route(ServerRequest var1);

针对我们要对外提供的功能,我们定义一个Route。

@Configuration
public class RouterConfig {
    private final TimeHandler timeHandler;

    @Autowired
    public RouterConfig(TimeHandler timeHandler) {
        this.timeHandler = timeHandler;
    }

    @Bean
    public RouterFunction<ServerResponse> timerRouter() {
        return route(GET("/time"), req -> timeHandler.getTime(req));
    }
}

可以看到访问/time的GET请求,将会由TimeHandler::getTime处理。

功能级别处理异常

如果我们在没有指定时间类型(type)的情况下调用相同的请求地址,例如/time,它将抛出异常。
Mono和Flux APIs内置了两个关键操作符,用于处理功能级别上的错误。

使用onErrorResume处理错误

还可以使用onErrorResume处理错误,fallback方法定义如下:

Mono<T> onErrorResume(Function<? super Throwable, ? extends Mono<? extends T>> fallback);

当出现错误时,我们使用fallback方法执行替代路径:

@Component
public class TimeHandler {
    public Mono<ServerResponse> getTime(ServerRequest serverRequest) {
        String timeType = serverRequest.queryParam("time").orElse("Now");
        return getTimeByType(timeType).flatMap(s -> ServerResponse.ok()
                .contentType(MediaType.TEXT_PLAIN).syncBody(s))
                .onErrorResume(e -> Mono.just("Error: " + e.getMessage()).flatMap(s -> ServerResponse.ok().contentType(MediaType.TEXT_PLAIN).syncBody(s)));
    }

    private Mono<String> getTimeByType(String timeType) {
        String type = Optional.ofNullable(timeType).orElse(
                "Now"
        );
        switch (type) {
            case "Now":
                return Mono.just("Now is " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            case "Today":
                return Mono.just("Today is " + new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
            default:
                return Mono.empty();
        }
    }
}

在如上的实现中,每当getTimeByType()抛出异常时,将会执行我们定义的fallback方法。除此之外,我们还可以捕获、包装和重新抛出异常,例如作为自定义业务异常:

    public Mono<ServerResponse> getTime(ServerRequest serverRequest) {
        String timeType = serverRequest.queryParam("time").orElse("Now");
        return ServerResponse.ok()
                .body(getTimeByType(timeType)
                        .onErrorResume(e -> Mono.error(new ServerException(new ErrorCode(HttpStatus.BAD_REQUEST.value(),
                                "timeType is required", e.getMessage())))), String.class);
    }

使用onErrorReturn处理错误

每当发生错误时,我们可以使用onErrorReturn()返回静态默认值:

    public Mono<ServerResponse> getDate(ServerRequest serverRequest) {
        String timeType = serverRequest.queryParam("time").get();
        return getTimeByType(timeType)
                .onErrorReturn("Today is " + new SimpleDateFormat("yyyy-MM-dd").format(new Date()))
                .flatMap(s -> ServerResponse.ok()
                        .contentType(MediaType.TEXT_PLAIN).syncBody(s));
    }

全局异常处理

如上的配置是在方法的级别处理异常,如同对注解的Controller全局异常处理一样,WebFlux的函数式开发模式也可以进行全局异常处理。要做到这一点,我们只需要自定义全局错误响应属性,并且实现全局错误处理逻辑。

我们的处理程序抛出的异常将自动转换为HTTP状态和JSON错误正文。要自定义这些,我们可以简单地扩展DefaultErrorAttributes类并覆盖其getErrorAttributes()方法:

@Component
public class GlobalErrorAttributes extends DefaultErrorAttributes {

    public GlobalErrorAttributes() {
        super(false);
    }

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

    private Map<String, Object> assembleError(ServerRequest request) {
        Map<String, Object> errorAttributes = new LinkedHashMap<>();
        Throwable error = getError(request);
        if (error instanceof ServerException) {
            errorAttributes.put("code", ((ServerException) error).getCode().getCode());
            errorAttributes.put("data", error.getMessage());
        } else {
            errorAttributes.put("code", HttpStatus.INTERNAL_SERVER_ERROR);
            errorAttributes.put("data", "INTERNAL SERVER ERROR");
        }
        return errorAttributes;
    }
    //...有省略
}

如上的实现中,我们对ServerException进行了特别处理,根据传入的ErrorCode对象构造对应的响应。

接下来,让我们实现全局错误处理程序。为此,Spring提供了一个方便的AbstractErrorWebExceptionHandler类,供我们在处理全局错误时进行扩展和实现:

@Component
@Order(-2)
public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {

    //构造函数
    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(final ErrorAttributes errorAttributes) {
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }

    private Mono<ServerResponse> renderErrorResponse(final ServerRequest request) {

        final Map<String, Object> errorPropertiesMap = getErrorAttributes(request, true);

        return ServerResponse.status(HttpStatus.OK)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(BodyInserters.fromObject(errorPropertiesMap));
    }
}

这里将全局错误处理程序的顺序设置为-2。这是为了让它比@Order(-1)注册的DefaultErrorWebExceptionHandler处理程序更高的优先级。

该errorAttributes对象将是我们在网络异常处理程序的构造函数传递一个的精确副本。理想情况下,这应该是我们自定义的Error Attributes类。然后,我们清楚地表明我们想要将所有错误处理请求路由到renderErrorResponse()方法。最后,我们获取错误属性并将它们插入服务器响应主体中。

然后,它会生成一个JSON响应,其中包含错误,HTTP状态和计算机客户端异常消息的详细信息。对于浏览器客户端,它有一个whitelabel错误处理程序,它以HTML格式呈现相同的数据。当然,这可以是定制的。

小结

本文首先讲了Spring 5之前的SpringMVC异常处理机制,SpringMVC统一异常处理有 3 种方式:使用 @ExceptionHandler 注解、实现 HandlerExceptionResolver 接口、使用 @controlleradvice 注解;然后通过WebFlux的函数式接口构建Web应用,讲解Spring Boot 2 Webflux的函数级别和全局异常处理机制(对于Spring WebMVC风格,基于注解的方式编写响应式的Web服务,仍然可以通过SpringMVC统一异常处理实现)。

注:本文后半部分基本翻译自https://www.baeldung.com/spring-webflux-errors

原文地址:https://www.cnblogs.com/xiang--liu/p/11422509.html

时间: 2024-10-03 10:59:00

Spring Boot 2 Webflux的全局异常处理的相关文章

spring boot 1.5.4 统一异常处理(九)

上一篇:springboot 1.5.4 配置文件详解(八) 1      Spring Boot统一异常处理 Spring Boot中实现了默认的error映射,但是在实际应用中,上面你的错误页面对用户来说并不够友好,我们通常需要去实现我们自己的异常提示. 以springboot项目为例,进行处理! springboot项目源码: https://git.oschina.net/wyait/springboot1.5.4.git 1.1  创建全局异常处理类 通过使用@ControllerAd

spring-boot实战【07】【转】:Spring Boot中Web应用的统一异常处理

我们在做Web应用的时候,请求处理过程中发生错误是非常常见的情况.Spring Boot提供了一个默认的映射:/error,当处理中抛出异常之后,会转到该请求中处理,并且该请求有一个全局的错误页面用来展示异常内容. 选择一个之前实现过的Web应用(Chapter3-1-2)为基础,启动该应用,访问一个不存在的URL,或是修改处理内容,直接抛出异常,如: 1 2 3 4 @RequestMapping("/hello") public String hello() throws Exce

Spring Boot中Web应用的统一异常处理

我们在做Web应用的时候,请求处理过程中发生错误是非常常见的情况.Spring Boot提供了一个默认的映射:/error,当处理中抛出异常之后,会转到该请求中处理,并且该请求有一个全局的错误页面用来展示异常内容. 选择一个之前实现过的Web应用(Chapter3-1-2)为基础,启动该应用,访问一个不存在的URL,或是修改处理内容,直接抛出异常,如: 1 2 3 4 @RequestMapping("/hello") public String hello() throws Exce

spring boot 1.5.4 整合log4j2(十一)

上一篇:spring boot 1.5.4 定时任务和异步调用(十) Spring Boot整合log4j2 spring boot整合log4j2项目spring-boot-jsp源码: https://git.oschina.net/wyait/springboot1.5.4.git 1.1  log4j2概要 对于我们开发人员来说,日志记录往往不被重视.在生产环境中,日志是查找问题来源的重要依据.日志可记录程序运行时产生的错误信息.状态信息.调试信息和执行时间信息等多种多样的信息.可以在程

spring boot学习随笔

Spring Boot 概述 ---------------------------------------------------------------------------------********************************-------------------------------------------------------------------- Spring Boot介绍: Spring Boot的目的在于创建和启动新的基于Spring框架的项目,S

Spring Boot干货系列:(二)配置文件解析

Spring Boot:配置文件解析   前言 上一篇介绍了Spring Boot的入门,知道了Spring Boot使用"习惯优于配置"(项目中存在大量的配置,此外还内置了一个习惯性的配置,让你无需手动进行配置)的理念让你的项目快速运行起来.所以,我们要想把Spring Boot玩的溜,就要懂得如何开启各个功能模块的默认配置,这就需要了解Spring Boot的配置文件application.properties. 正文 Spring Boot使用了一个全局的配置文件applicat

spring boot 1.5.4 配置文件详解(八)

上一篇:spring boot 1.5.4 集成spring-Data-JPA(七) 1      Spring Boot配置文件详解 相信很多人选择Spring Boot主要是考虑到它既能兼顾Spring的强大功能,还能实现快速开发的便捷.我们在Spring Boot使用过程中,最直观的感受就是没有了原来自己整合Spring应用时繁多的XML配置内容,替代它的是在pom.xml中引入模块化的Starter POMs,其中各个模块都有自己的默认配置,所以如果不是特殊应用场景,就只需要在appli

spring boot 1.5.4 定时任务和异步调用(十)

上一篇:spring boot1.5.4 统一异常处理(九) 1      Spring Boot定时任务和异步调用 我们在编写Spring Boot应用中经常会遇到这样的场景,比如:我需要定时地发送一些短信.邮件之类的操作,也可能会定时地检查和监控一些标志.参数等. spring boot定时任务spring-boot-jsp项目源码: https://git.oschina.net/wyait/springboot1.5.4.git 1.1  创建定时任务 在Spring Boot中编写定时

spring boot 1.5.4 整合 druid(十三)

上一篇:spring boot 1.5.4 整合 mybatis(十二) 1      集成druid连接池 spring boot集成druid项目mybatis-spring-boot源码地址: https://git.oschina.net/wyait/springboot1.5.4.git 1.1  druid简介 Druid是阿里巴巴开源的一个项目.,整个项目由数据库连接池.插件框架和SQL解析器组成.该项目主要是为了扩展JDBC的一些限制,可以让程序员实现一些特殊的需求,比如向密钥服