Spring Boot 2 Webflux的全域性異常處理

aoho發表於2018-12-18

本文首先將會回顧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統一異常處理實現)。

注:本文後半部分基本翻譯自www.baeldung.com/spring-webf…

訂閱最新文章,歡迎關注我的公眾號

微信公眾號

參考

  1. Handling Errors in Spring WebFlux
  2. Spring WebFlux快速上手——響應式Spring的道法術器

相關文章