SpringMVC的異常處理
Spring 統(tǒng)一異常處理有 3 種方式,分別為:
使用@ExceptionHandler注解
實現(xiàn)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("出錯了!");? ? }}
如上的代碼實現(xiàn),針對BuzController拋出的NullPointerException異常,將會捕獲局部異常,返回指定的內容。
實現(xiàn)HandlerExceptionResolver接口
通過實現(xiàn)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);? ? }}
和第一種方式的區(qū)別在于,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提供了一套函數式接口,可以用來實現(xiàn)類似MVC的效果。我們先接觸兩個常用的。
Controller定義對Request的處理邏輯的方式,主要有方面:
方法定義處理邏輯;
然后用@RequestMapping注解定義好這個方法對什么樣url進行響應。
在WebFlux的函數式開發(fā)模式中,我們用HandlerFunction和RouterFunction來實現(xiàn)上邊這兩點。
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);
當出現(xiàn)錯誤時,我們使用fallback方法執(zhí)行替代路徑:
@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();? ? ? ? }? ? }}
在如上的實現(xiàn)中,每當getTimeByType()拋出異常時,將會執(zhí)行我們定義的fallback方法。除此之外,我們還可以捕獲、包裝和重新拋出異常,例如作為自定義業(yè)務異常:
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處理錯誤
每當發(fā)生錯誤時,我們可以使用onErrorReturn()返回靜態(tài)默認值:
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的函數式開發(fā)模式也可以進行全局異常處理。要做到這一點,我們只需要自定義全局錯誤響應屬性,并且實現(xiàn)全局錯誤處理邏輯。
我們的處理程序拋出的異常將自動轉換為HTTP狀態(tài)和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;? ? }//...有省略}
如上的實現(xiàn)中,我們對ServerException進行了特別處理,根據傳入的ErrorCode對象構造對應的響應。
接下來,讓我們實現(xiàn)全局錯誤處理程序。為此,Spring提供了一個方便的AbstractErrorWebExceptionHandler類,供我們在處理全局錯誤時進行擴展和實現(xiàn):
@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處理程序更高的優(yōu)先級。
該errorAttributes對象將是我們在網絡異常處理程序的構造函數傳遞一個的精確副本。理想情況下,這應該是我們自定義的Error Attributes類。然后,我們清楚地表明我們想要將所有錯誤處理請求路由到renderErrorResponse()方法。最后,我們獲取錯誤屬性并將它們插入服務器響應主體中。
然后,它會生成一個JSON響應,其中包含錯誤,HTTP狀態(tài)和計算機客戶端異常消息的詳細信息。對于瀏覽器客戶端,它有一個whitelabel錯誤處理程序,它以HTML格式呈現(xiàn)相同的數據。當然,這可以是定制的。
小結
本文首先講了Spring 5之前的SpringMVC異常處理機制,SpringMVC統(tǒng)一異常處理有 3 種方式:使用@ExceptionHandler注解、實現(xiàn)HandlerExceptionResolver接口、使用@controlleradvice注解;然后通過WebFlux的函數式接口構建Web應用,講解Spring Boot 2 Webflux的函數級別和全局異常處理機制(對于Spring WebMVC風格,基于注解的方式編寫響應式的Web服務,仍然可以通過SpringMVC統(tǒng)一異常處理實現(xiàn))。
在此我向大家推薦一個架構學習交流群。交流學習群號:938837867 暗號:555 里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發(fā)、高性能、分布式、微服務架構的原理,JVM性能優(yōu)化、分布式架構等這些成為架構師必備