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:
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);
}
}