Spring Boot 2 Webflux的全局异常处理

SpringMVC的异常处理

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

使用@ExceptionHandler注解

实现HandlerExceptionResolver接口

使用@controlleradvice注解

使用@ExceptionHandler注解

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

@ControllerpublicclassBuzController{@ExceptionHandler({NullPointerException.class})publicStringexception(NullPointerException e){        System.out.println(e.getMessage());        e.printStackTrace();return"null pointer exception";    }@RequestMapping("test")publicvoidtest(){thrownewNullPointerException("出错了!");    }}

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

实现HandlerExceptionResolver接口

通过实现HandlerExceptionResolver接口,定义全局异常:

@ComponentpublicclassCustomMvcExceptionHandlerimplementsHandlerExceptionResolver{privateObjectMapper objectMapper;publicCustomMvcExceptionHandler(){        objectMapper =newObjectMapper();    }@OverridepublicModelAndViewresolveException(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 map =newHashMap<>();if(exinstanceofNullPointerException) {            map.put("code", ResponseCode.NP_EXCEPTION);        }elseif(exinstanceofIndexOutOfBoundsException) {            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();        }returnnewModelAndView();    }}

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

使用@controlleradvice注解

@ControllerAdvicepublicclassExceptionController{@ExceptionHandler(RuntimeException.class)publicModelAndViewhandlerRuntimeException(RuntimeException ex){if(exinstanceofMaxUploadSizeExceededException) {returnnewModelAndView("error").addObject("msg","文件太大!");        }returnnewModelAndView("error").addObject("msg","未知错误:"+ ex);    }@ExceptionHandler(Exception.class)publicModelAndViewhandlerMaxUploadSizeExceededException(Exception ex){if(ex !=null) {returnnewModelAndView("error").addObject("msg", ex);        }returnnewModelAndView("error").addObject("msg","未知错误:"+ ex);    }}

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

Spring 5 Webflux的异常处理

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

@RestControllerAdvicepublicclassCustomExceptionHandler{privatefinalLog logger = LogFactory.getLog(getClass());@ExceptionHandler(Exception.class)@ResponseStatus(code = HttpStatus.OK)publicErrorCodehandleCustomException(Exception e){        logger.error(e.getMessage());returnnewErrorCode("e","error");    }}

WebFlux示例

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

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

方法定义处理逻辑;

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

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

HandlerFunction

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

Monohandle(ServerRequest var1);

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

@ComponentpublicclassTimeHandler{publicMonogetTime(ServerRequest serverRequest){        String timeType = serverRequest.queryParam("type").get();//return ...}}

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

RouterFunction

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

Mono> route(ServerRequest var1);

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

@ConfigurationpublicclassRouterConfig{privatefinalTimeHandler timeHandler;@AutowiredpublicRouterConfig(TimeHandler timeHandler){this.timeHandler = timeHandler;    }@BeanpublicRouterFunctiontimerRouter(){returnroute(GET("/time"), req -> timeHandler.getTime(req));    }}

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

功能级别处理异常

如果我们在没有指定时间类型(type)的情况下调用相同的请求地址,例如/time,它将抛出异常。

Mono和Flux APIs内置了两个关键操作符,用于处理功能级别上的错误。

使用onErrorResume处理错误

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

MonoonErrorResume(Function> fallback);

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

@ComponentpublicclassTimeHandler{publicMonogetTime(ServerRequest serverRequest){        String timeType = serverRequest.queryParam("time").orElse("Now");returngetTimeByType(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)));    }privateMonogetTimeByType(String timeType){        String type = Optional.ofNullable(timeType).orElse("Now");switch(type) {case"Now":returnMono.just("Now is "+newSimpleDateFormat("HH:mm:ss").format(newDate()));case"Today":returnMono.just("Today is "+newSimpleDateFormat("yyyy-MM-dd").format(newDate()));default:returnMono.empty();        }    }}

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

publicMonogetTime(ServerRequest serverRequest){        String timeType = serverRequest.queryParam("time").orElse("Now");returnServerResponse.ok()                .body(getTimeByType(timeType)                        .onErrorResume(e -> Mono.error(newServerException(newErrorCode(HttpStatus.BAD_REQUEST.value(),"timeType is required", e.getMessage())))), String.class);    }

使用onErrorReturn处理错误

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

publicMonogetDate(ServerRequest serverRequest){        String timeType = serverRequest.queryParam("time").get();returngetTimeByType(timeType)                .onErrorReturn("Today is "+newSimpleDateFormat("yyyy-MM-dd").format(newDate()))                .flatMap(s -> ServerResponse.ok()                        .contentType(MediaType.TEXT_PLAIN).syncBody(s));    }

全局异常处理

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

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

@ComponentpublicclassGlobalErrorAttributesextendsDefaultErrorAttributes{publicGlobalErrorAttributes(){super(false);    }@OverridepublicMapgetErrorAttributes(ServerRequest request,booleanincludeStackTrace){returnassembleError(request);    }privateMapassembleError(ServerRequest request){        Map errorAttributes =newLinkedHashMap<>();        Throwable error = getError(request);if(errorinstanceofServerException) {            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");        }returnerrorAttributes;    }//...有省略}

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

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

@Component@Order(-2)publicclassGlobalErrorWebExceptionHandlerextendsAbstractErrorWebExceptionHandler{//构造函数@OverrideprotectedRouterFunctiongetRoutingFunction(finalErrorAttributes errorAttributes){returnRouterFunctions.route(RequestPredicates.all(),this::renderErrorResponse);    }privateMonorenderErrorResponse(finalServerRequest request){finalMap errorPropertiesMap = getErrorAttributes(request,true);returnServerResponse.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统一异常处理实现)。

在此我向大家推荐一个架构学习交流群。交流学习群号:938837867 暗号:555 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容