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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM