This article will first review SpringMVC exception handling prior to Spring 5 and then focus on global exception handling for Spring Boot 2 Webflux.

Exception handling for SpringMVC

Spring provides unified exception handling in three ways:

  • use@ExceptionHandlerannotations
  • implementationHandlerExceptionResolverinterface
  • use@controlleradviceannotations

use@ExceptionHandlerannotations

For local method capture, in the same Controller class as the method that threw the exception:

@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(a) {
        throw new NullPointerException("Wrong!); }}Copy the code

A NullPointerException thrown by BuzController will catch the local exception and return the specified content.

implementationHandlerExceptionResolverinterface

By implementing HandlerExceptionResolver interface, here we through inheritance SimpleMappingExceptionResolver implementation class (HandlerExceptionResolver implementation, allows the exception class name mapping to view name, Handlers can be a given set of handlers, or all handlers in a DispatcherServlet) define global exceptions:

@Component
public class CustomMvcExceptionHandler extends SimpleMappingExceptionResolver {

    private ObjectMapper objectMapper;

    public CustomMvcExceptionHandler(a) {
        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 newModelAndView(); }}Copy the code

Using the example above, we can customize the response to the error based on the various exceptions.

use@controlleradviceannotations

@ControllerAdvice
public class ExceptionController {
    @ExceptionHandler(RuntimeException.class)
    public ModelAndView handlerRuntimeException(RuntimeException ex) {
        if (ex instanceof MaxUploadSizeExceededException) {
            return new ModelAndView("error").addObject("msg"."The file is too big!");
        }
        return new ModelAndView("error").addObject("msg"."Unknown error:" + 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"."Unknown error:"+ ex); }}Copy the code

The difference from the first approach is that the definition of ExceptionHandler and exception catching can be extended globally.

Exception handling for Spring 5 Webflux

Webflux supports MVC annotations, which is a very convenient feature. Compared with RouteFunction, automatic scan registration is much easier. ExceptionHandler can be used for exception handling. The following global exception handling still applies to 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"); }}Copy the code

WebFlux sample

WebFlux provides a set of functional interfaces that can be used to achieve mVC-like effects. Let’s start with two common ones.

Controller defines the way in which Request logic is handled. The main aspects are:

  • Method definition processing logic;
  • Then use the @RequestMapping annotation to define what URL the method should respond to.

In the functional development mode of WebFlux, we use HandlerFunction and RouterFunction to implement these two points.

HandlerFunction

The HandlerFunction corresponds to the handler in the Controller, where the input is the request and the output is the response loaded in the Mono:

    Mono<T> handle(ServerRequest var1);
Copy the code

In WebFlux, requests and responses are no longer ServletRequest and ServletResponse in WebMVC, but ServerRequest and ServerResponse. The latter are interfaces used in reactive programming, which provide support for non-blocking and backpressure features, as well as methods of converting Http message bodies to reactive types Mono and Flux.

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

We defined a TimeHandler that returns the current time based on the parameters of the request.

RouterFunction

The RouterFunction, as the name suggests, is the equivalent of @requestMapping, which determines what URL maps to that particular HandlerFunction. Input is request, output is Handlerfunction in Mono:

Mono<HandlerFunction<T>> route(ServerRequest var1);
Copy the code

We define a Route for the functionality we want to provide externally.

@Configuration
public class RouterConfig {
    private final TimeHandler timeHandler;

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

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

You can see that GET requests to /time will be handled by TimeHandler::getTime.

Function level Handle exceptions

If we call the same request address, such as /time, without specifying type, it will throw an exception. There are two key operators built into Mono and the Flux APIs for handling functional-level errors.

Use onErrorResume to handle errors

You can also use onErrorResume to handle errors. The fallback method is defined as follows:

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

When an error occurs, we use the fallback method to execute the alternative path:

@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:
                returnMono.empty(); }}}Copy the code

In the above implementation, whenever getTimeByType() throws an exception, the fallback method we defined will be executed. In addition, we can catch, wrap, and rethrow exceptions, for example, as custom business exceptions:

    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);
    }
Copy the code

Use onErrorReturn to handle errors

Whenever an error occurs, we can use onErrorReturn() to return the static default:

    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));
    }
Copy the code

Global exception handling

The above configuration handles exceptions at the method level, as well as the annotated Controller global exception handling for The functional development mode of WebFlux. To do this, we just need to customize the global error response property and implement the global error handling logic.

Exceptions thrown by our handler are automatically converted to HTTP status and JSON error bodies. To customize this, we can simply extend the DefaultErrorAttributes class and override its getErrorAttributes() method:

@Component
public class GlobalErrorAttributes extends DefaultErrorAttributes {

    public GlobalErrorAttributes(a) {
        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;
    }
    / /... There are omitted
}
Copy the code

In the above implementation, we treat ServerException in a special way, constructing the corresponding response from the ErrorCode object passed in.

Next, let’s implement the global error handler. Therefore, Spring provides a convenient AbstractErrorWebExceptionHandler class, for us in addressing the global error for extension and implementation:

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

	// constructor
    @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);

        returnServerResponse.status(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON_UTF8) .body(BodyInserters.fromObject(errorPropertiesMap)); }}Copy the code

Here the order of the global error handlers is set to -2. This is to let it than @ Order (1) registered DefaultErrorWebExceptionHandler handler is higher priority.

The errorAttributes object will be an exact copy of one we pass in the constructor of the network exception handler. Ideally, this would be our custom Error Attributes class. We then make it clear that we want to route all error-handling requests to the renderErrorResponse() method. Finally, we get the error attributes and insert them into the server response body.

It then generates a JSON response containing details of the error, HTTP status, and computer client exception messages. For the browser client, it has a whitelabel error handler that renders the same data in HTML format. Of course, this can be customized.

summary

In this article, we first introduce the SpringMVC exception handling mechanism prior to Spring 5. There are three ways for SpringMVC to handle exceptions uniformly: Use the @ExceptionHandler annotation, implement the HandlerExceptionResolver interface, and use the @ControllerAdvice annotation. Then build Web applications through the functional interface of WebFlux, and explain the functional level and global exception handling mechanism of Spring Boot 2 WebFlux (for Spring WebMVC style, write responsive Web services based on annotations, It can still be implemented with SpringMVC unified exception handling).

Note: The second half of this article is basically translated from www.baeldung.com/spring-webf…

Subscribe to the latest articles, welcome to follow my official account

reference

  1. Handling Errors in Spring WebFlux
  2. Spring WebFlux quick to Get started — Responsive Spring’s Tao Wizard