Error Handling for REST with Spring

80 阅读3分钟

This tutorial will illustrate how to implement Exception Handling with Spring for a REST API.  We’ll also get a bit of historical overview and see which new options the different versions introduced.

Before Spring 3.2

@ExceptionHandler

该注解方式最大的缺点需要每个@Controller 写一个注解方法。没办法统一管理。

但有一个解决方法:将该@ExceptionHandler写在一个基类中,其他 Controller 继承该基类。

Before Spring 3.2, the two main approaches to handling exceptions in a Spring MVC application were HandlerExceptionResolver or the  @ExceptionHandler annotation.  Both have some clear downsides.

@ExceptionHandler
public Answer exceptionHandler(Exception exception) {
    log.info("in exception handler.", exception);
    return Answer.builder().msg("exception handler").build();
}

HandlerExceptionResolver

spring 默认开启了DefaultHandlerExceptionResolver。会对错误码做一个转换,但是 body没有。比如访问一个不存在的uri:

image.png

ResponseStatusExceptionResolver

该类跟上面的HandlerExceptionResolver一样,只是可以将业务自定义的异常类映射到响应的状态码。

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class MyResourceNotFoundException extends RuntimeException {
    public MyResourceNotFoundException() {
        super();
    }
    public MyResourceNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
    public MyResourceNotFoundException(String message) {
        super(message);
    }
    public MyResourceNotFoundException(Throwable cause) {
        super(cause);
    }
}

AbstractHandlerExceptionResolver

可以通过继承AbstractHandlerExceptionResolver自定义ExceptionResolver。

@Component
@Slf4j
public class MyResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver {
    @Override
    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        try {
            // modify status code
            response.sendError(600, "biz error");
        } catch (Exception e) {
            log.error("error in MyResponseStatusExceptionResolver.", e);
        }
        return new ModelAndView();
    }
}

小结

上面讲到的内容无法返回 body 信息,有些方法还比较底层。

Since Spring 3.2

Spring 3.2 brings support for a global  @ExceptionHandler with the @ControllerAdvice annotation.

继承ResponseEntityExceptionHandler

@ControllerAdvice
@Slf4j
@Order(1)
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
    @ExceptionHandler(RuntimeException.class)
    public final ResponseEntity<Object> handleBizException(Exception ex, WebRequest request) {
        log.info("catch exception in RestResponseEntityExceptionHandler, path: {}", request.getContextPath());
        Answer answer = Answer.builder().msg("catch exception in RestResponseEntityExceptionHandler").build();
        return handleExceptionInternal(ex, answer, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
    }
}

该处理方式会返回ResponseEntity,最终返回给前端Answer对象。并且可以设置http状态码。

自定义实现

@ControllerAdvice
@Slf4j
@Order(2)
public class DefaultExceptionHandler {
    @ExceptionHandler(RuntimeException.class)
    @ResponseBody
    public JsonResponse<Void> handleRuntimeException(HttpServletRequest request, RuntimeException e) {
        log.info("handle RuntimeException in DefaultExceptionHandler, path: {}", request.getRequestURL());
        return JsonResponse.error(700, e.getMessage());
    }
}

该种方式可以自定义

顺序

如果业务 controller 中抛出了RuntimeException,上面的两个 Handler 都可以处理,会按照@Order 的顺序处理。

小结

Controller 业务层抛出的异常以及 Interceptor抛出的异常都可以被上面的方式拦截掉。

Controller 和 Interceptor 工作在 spring 容器中,会被其拦截。

上面的拦截结论不太准确,因为在Interceptor的 afterCompletion中抛出的异常已经被 spring catch 了。

	void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
		for (int i = this.interceptorIndex; i >= 0; i--) {
			HandlerInterceptor interceptor = this.interceptorList.get(i);
			try {
				interceptor.afterCompletion(request, response, this.handler, ex);
			}
			catch (Throwable ex2) {
				logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
			}
		}
	}

下面的代码。

preHandle与 postHandle 不会被 catch 异常。也好理解,已经 completion 的也不用再往外抛异常了。

	boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
		for (int i = 0; i < this.interceptorList.size(); i++) {
			HandlerInterceptor interceptor = this.interceptorList.get(i);
			if (!interceptor.preHandle(request, response, this.handler)) {
				triggerAfterCompletion(request, response, null);
				return false;
			}
			this.interceptorIndex = i;
		}
		return true;
	}

	/**
	 * Apply postHandle methods of registered interceptors.
	 */
	void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
			throws Exception {

		for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
			HandlerInterceptor interceptor = this.interceptorList.get(i);
			interceptor.postHandle(request, response, this.handler, mv);
		}
	}

ErrorController

如果在 filter 抛出异常,最后会被 BasicErrorController 处理。但是如果在ErrorController中抛出了异常,那么可以被上面的 ControllerAdvice、ExceptionHandler 处理。

@Component
@Slf4j
public class MyErrorController extends BasicErrorController {

    public MyErrorController(
            ErrorAttributes errorAttributes, ServerProperties serverProperties) {
        super(errorAttributes, serverProperties.getError());
    }

    @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        log.info("in MyErrorController, path: {}", request.getRequestURL());
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity<>(status);
        }
        Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        body.put("biz", "biz error");
//        if (true) {
//            throw new RuntimeException("MyErrorController throw exception");
//        }

        return new ResponseEntity<>(body, status);
    }

}

参考