[TOC]
前言
在 Spring Boot 中,当用户访问一个不存在的页面,或者我们的应用(确切地说是Controller)抛出异常时,会默认返回如下内容:
$ curl http://localhost:8080
{
"timestamp": "2020-05-28T09:43:43.820+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/"
}
这里的实现机制是 Spring Boot 默认创建一个自动配置类ErrorMvcAutoConfiguration,该类会创建一个全局异常控制器BasicErrorController,它被映射到路由/error,该路由会处理所有的错误信息,并返回响应。具体如下:
package org.springframework.boot.autoconfigure.web.servlet.error;
//...
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
//...
// 浏览器访问,返回 text/html 页面
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
// 客户端访问,返回 JSON 数据
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
//...
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<>(body, status);
}
如果我们不想要上述默认输出信息,那么我们就需要对 Spring Boot 进行全局异常捕获处理。
内置异常捕获指令
Spring 发展到现在,内置了多种异常捕获指令,常见的有如下几种:
-
@ExceptionHandler:只能用于捕获特定Controller抛出的异常,无法进行全局(所有Controller)捕获。
@Controller
public class FooController{
//...
@ExceptionHandler({ CustomException1.class, CustomException2.class })
public void handleException() {
//
}
}
注:我们可以通过定义一个父类Controller关联该@ExceptionHandler,这样应用中其他继承该父类Controller抛出的异常就都能被捕获到,然而,这种做法还是存在缺陷,其对于第三方库中的异常无法进行捕获。
-
@ResponseStatus:可以在类或方法上使用该注解,当与其绑定的异常发生时,会被拦截到并按照它的注解配置转换为响应。比如:
@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such Order") // 404
public class OrderNotFoundException extends RuntimeException {
// ...
}
上述代码会对OrderNotFoundException进行捕获,并将响应行内容设置为404 not found。但@ResponseStatus的一个弊端就是它与特定异常类是紧耦合关系,每个响应内容只能应用于一个异常类中,其他的异常类需要重新配置另外的@ResponseStatus注解。
-
ResponseStatusException:ResponseStatusException是 Spring 5 才引入进来的,是一个很新的 api,本质上它是一个RuntimeException。ResponseStatusException相对@ResponseStatus注解来说更加方便,因为其可以直接创建出不同的响应状态码,其构造函数第一个参数为HttpStatus,该枚举类内置了很多常见的状态码及其描述,开箱即用。比如:
// Controller
@GetMapping("/actor/{id}")
public String getActorName(@PathVariable("id") int id) {
throw new ResponseStatusException( HttpStatus.NOT_FOUND, "Actor Not Found", ex);
}
-
HandlerExceptionResolver:其是一个接口,具备全局异常捕获能力。通过实现该接口,我们就可以维护一个统一的异常处理机制。Spring 内置了多个实现该接口的实现类,比如:ExceptionHandlerExceptionResolver:SpringMVC 中的DispatcherServlet默认使能该接口,该实现类会捕获被@ExceptionHandler注解的方法抛出的异常。ResponseStatusExceptionResolver:SpringMVC 中的DispatcherServlet同样默认使能该接口,该实现类能够捕获ResponseStatusException和被@ResponseStatus注解的异常,同样转换为对应的 HTTP 状态码。比如:
@ResponseStatus(value = HttpStatus.NOT_FOUND) public class MyResourceNotFoundException extends RuntimeException { public MyResourceNotFoundException() { super(); } ... }上述代码由于自定义异常
MyResourceNotFoundException被@ResponseStatus注解了,因此ResponseStatusExceptionResolver可以捕获该异常,并将其转换为状态码404。同样,该实现类也无法对响应体内容进行设置。DefaultHandlerExceptionResolver:SpringMVC 中的DispatcherServlet默认使能该接口,但该实现类只会捕获 SpringMVC 抛出的异常并将其转换为对应的 HTTP 状态码(具体支持的异常请查看:mvc-ann-rest-spring-mvc-exceptions)。由于该实现类只能转换为状态码,不能自定义响应体内容,因此也存在不足。
...自定义类实现
HandlerExceptionResolver:由于 Spring 内置的实现类功能都有所缺陷,因此我们可以自己实现该接口,打造出一个符合我们需求的实现类。比如:
// HandlerExceptionResolver @Component public class HandlerExceptionToViewResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { return new ModelAndView((map, httpServletRequest, httpServletResponse) -> { httpServletResponse.setContentType("text/html;charset=UTF-8"); PrintWriter writer = httpServletResponse.getWriter(); writer.print("<h1>Exception Detected!!!</h1>"); writer.flush(); }); } } // Application.java @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } // 注入自定义 HandlerExceptionResolver @Bean HandlerExceptionResolver customExceptionResolver () { return new HandlerExceptionToViewResolver(); } } // Controller @RestController public class ExceptionController { @GetMapping("/exception") public String exception(){ throw new RuntimeException("this exception will be captured by HandlerExceptionToViewResolver!!"); } }上述代码在异常捕获时,Spring 会将
request和response传入,因此我们可以知道异常请求详情和对response进行输出设置,完全可控。但是,虽然自定义
HandlerExceptionResolver已能较完美满足我们对全局异常捕获及统一异常处理,但还是存在缺陷,一个方面是该方法需要手动控制HttpServletResponse,操作相对较底层,比较繁琐,另一个方面是HandlerExceptionResolver是对ModelAndView进行交互,返回的是一个视图对象,这与当前流行的 RESTful 风格数据类型不一致...也因此,为了提供一个更好的全局异常捕获机制,Spring 3.2 版本引进了一个全局异常处理注解
@ControllerAdvice。 @ControllerAdvice:对于注解了@ControllerAdvice的类,Spring 会将其作为全局异常捕获类。当应用任一Controller发生异常时,会被@ControllerAdvice注解类捕获到,然后根据其内部@ExceptionHandler会指定想要进行捕获的异常,满足时,异常即被捕获,比如:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({ Exception.class})
@ResponseBody
public ResponseEntity handleException() {
return ResponseEntity.status(500).<String>body("exception occured!!");
}
}
上述代码在任意Controller抛出Exception异常时,都会被捕获得到。
注:@ControllerAdvice对应的 RESTful 风格的 api 为:@RestControllerAdvice
Spring Boot 异常捕获机制
SpringMVC 中的异常处理的标准配置是由WebMvcConfigurationSupport提供的,该配置由@EnableWebMvc注解进行开启。
WebMvcConfigurationSupport会向 Spring 容器中注入多个Bean实例,其中就包括负责异常处理的Bean实例:HandlerExceptionResolverComposite。
HandlerExceptionResolverComposite捕获到异常后,会依次委托给其内维护的一系列异常处理器,当其中一个异常处理器返回结果不为空的时候,就说明异常被处理完成,处理结果作为最终响应。这部分内容对应的部分源码如下所示:
package org.springframework.web.servlet.config.annotation;
//...
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
//...
protected void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
}
protected void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
}
@Bean
public HandlerExceptionResolver handlerExceptionResolver(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
// 预留接口
configureHandlerExceptionResolvers(exceptionResolvers);
if (exceptionResolvers.isEmpty()) {
// 依次添加 ExceptionHandlerExceptionResolver,ResponseStatusExceptionResolver,DefaultHandlerExceptionResolver 3 个默认异常处理器
addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);
}
// 预留接口
extendHandlerExceptionResolvers(exceptionResolvers);
HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
composite.setOrder(0);
composite.setExceptionResolvers(exceptionResolvers);
return composite;
}
//...
}
注:在源码中,我们还可以看到,handlerExceptionResolver方法内部预留了两个 Hook 接口,其中:
-
configureHandlerExceptionResolvers:可以添加我们自定义的HandlerExceptionResolver,但此时就不会再添加默认的异常处理器。 -
extendHandlerExceptionResolvers:该接口在添加完 3 个默认异常处理器后才触发,因此其作用是在异常处理器列表中追加自定义HandlerExceptionResolver。
@EnableWebMvc
@EnableAsync
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 不添加默认异常处理器
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
exceptionResolvers.add(new HandlerExceptionToViewResolver());
}
// 追加自定义异常处理器
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
exceptionResolvers.add(new HandlerExceptionToViewResolver());
}
}
简而言之,WebMvcConfigurationSupport是以按以下顺序注入异常处理器给到HandlerExceptionResolverComposite:
-
ExceptionHandlerExceptionResolver:捕获处理被@ExceptionHandler注解的异常 -
ResponseStatusExceptionResolver:捕获被ResponseStatusException和@ResponseStatus注解的异常 -
DefaultHandlerExceptionResolver:捕获 Spring 内置的异常(比如:HttpRequestMethodNotSupportedException)
如果以上异常处理器都捕获失败,那么该异常就会传递到内置容器(比如:Tomcat)中。内置容器就会将该异常重定向到/error页面,此时就由 Spring Boot 默认提供的BasicErrorController进行捕获处理,也就是我们最前面讲述的内容。整个流程如下图所示:

综上,在 Spring Boot 应用中,对于 SpringMVC 异常处理,其执行逻辑大概如下:
当异常发生时,Spring 首先会在被
@ControllerAdvice注解的类中查找@ExceptionHandler注解的方法。这步由ExceptionHandlerExceptionResolver进行实现。然后会检测看异常是否被
@ResponseStatus注解,或者是来自ResponseStatusException,如果是的话就交由ResponseStatusExceptionResolver进程处理。最后由 SpringMVC 的默认异常
DefaultHandlerExceptionResolver进行处理。如果到最后还是找不到对该异常进行处理的 Handler,那么就交由定义了异常页面(error view page)的全局错误处理。
注:如果该异常是由该全局异常处理器(比如:BasicErrorController)内部抛出的,那么第 4 步不会执行(个人理解,这里更确切的说法应该是当BasicErrorController内部的某个页面发送异常时,内置容器(比如:Tomcat)会将该异常重定向到错误页面/error,而如果/error页面内部发生异常,则由于其也是一个Controller,所以会优先走@ControllerAdvice全局捕获,捕获不成功才最终交由 Tomcat 进行处理).如果找不到错误视图(比如:全局错误处理器被禁止)或者跳过第 4 步骤,那么异常就只能交给到容器进行处理。
自定义全局异常捕获
从前面的内容分析可以知道,在 Spring Boot 应用中,系统中共存在以下 3 种异常情况:
-
Controller抛出的异常:可以采用@ControllerAdvice进行全局捕获 -
其他异常:会被全局异常处理器(比如:
BasicErrorController)进行捕获 -
全局异常处理器抛出的异常:对于非
/error页面的异常,会被内置容器转发到/error进行捕获处理,而对于/error页面的异常,会先由@ControllerAdvice进行捕获,捕获不成功则交由 Spring Boot 内置的 Web 应用容器(比如:Tomcat)进行捕获。
因此,我们自定义一个全局异常捕获,就要考虑以上 3 方面异常:
-
Controller抛出的异常:比如可以像如下代码所示,简单粗暴拦截所有Controller异常,而对于ResponseStatusException异常,进行特殊处理。
@RestControllerAdvice
public class GlobalExceptionHandler {
// 捕获所有 Exception
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException() {
return ResponseEntity.status(500).<String>body("exception detected!!!");
}
@ExceptionHandler
@ResponseStatus(code= HttpStatus.INTERNAL_SERVER_ERROR)
public String handleSpecialException(ResponseStatusException e){
return "ResponseStatusException detected!!!";
}
}
注:根据我们前面的分析,由于ExceptionHandlerExceptionResolver的捕获优先于ResponseStatusExceptionResolver,而上面代码对所有异常Exception都进行了捕获处理,因此,ResponseStatusExceptionResolver和DefaultHandlerExceptionResolver永远不会被执行到。
当然,如果没有对异常基类Exception进行捕获,系统遇到其他异常,还是会走正常流程的,如果想打破这个流程,可以采用自定义HandlerExceptionResolver,并且切断默认处理器添加(即覆写configureHandlerExceptionResolvers),这样做的话,系统就只会走我们自定义的HandlerExceptionResolver,若不能处理该异常,则直接转到全局异常处理。
@EnableWebMvc
@EnableAsync
@Configuration
public class SingleHandlerExceptionResolver implements WebMvcConfigurer {
@Autowired
private CustomHandlerExceptionResolver customResolver;
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(this.customResolver);
}
@Component
static class CustomHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
System.out.println("---------hahaha-------------");
// 只捕获 ResponsStatusException
if (ex instanceof ResponseStatusException) {
return new ModelAndView((model, req, res) -> {
res.setStatus(500);
res.setContentType("application/json");
PrintWriter writer = res.getWriter();
writer.print(String.format("{code: %d,msg: %s}", -1,
"server detected ResposeStatusException!!"));
writer.flush();
});
}
return null;
}
}
}
-
其他异常:对于不是由
Controller抛出的异常,就会交到 Spring Boot 提供的默认异常处理器(即BasicErrorController)进行处理,这里有多种方法可以修改或覆盖 Spring Boot 提供的默认全局异常处理:- 自定义一个
Bean,实现接口ErrorController,则此时默认的异常处理将不再生效 - 自定义一个
Bean,继承BasicErrorController,可重用现有功能,并进行针对性修改。也可以扩展新方法,使用@RequestMapping和produces映射新地址。 - 自定义一个类型为
ErrorAttribute的Bean实例,BasicErrorController会自动采用该实现并进行渲染 - 自定义一个
Bean,继承AbstractErrorController
- 自定义一个
这里我们采用实现接口ErrorController来实现全局异常处理:
@Controller
@RequestMapping("/error")
public class CustomizedErrorController implements ErrorController {
@Override
public String getErrorPath() {
return null;
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> result = new HashMap<>();
result.put("code", "-1");
result.put("msg", "sever detected exception!!!");
return ResponseEntity.status(500).body(result);
}
}
现在当遇到未知异常时,就会被CustomizedErrorController捕获得到。
- 全局异常处理器抛出的异常:对于全局异常处理器抛出的异常,存在一定的可能无法被 Spring Boot 应用捕获处理,则最终会被 Spring Boot 内置的 Web 应用容器(比如:Tomcat)进行捕获。此时可根据自己应用内部使用的具体容器提供的相关 api 进行设置,这里就展开了。
综上,定义全局异常捕获主要就是使用到@ControllerAdvice和全局异常处理器。
@ControllerAdvice可以对所有Controller抛出的异常进行捕获,为了尽可能让我们自定义的@ControllerAdvice捕获异常,因罗列足够多,设置于使用基类Exception进行全部捕获。
而对于网址不存在404 not found等异常,则会交给到全局异常处理器进行处理,此处唯一要注意的是尽量让/error路由内部不出现异常,否则很可能会交给内置容器进行处理(其实如果@ControllerAdvice使用了基类Exception,那么/error抛出的任意异常其实都会被@ControllerAdvice进行捕获)。
下面实现一个相对全面稳健的全局异常捕获器(RESTful 风格)。
全局异常捕获器
- 首先定义响应体
Bean类,统一数据下发格式:
public final class ResponseBean<T> {
// 自定义应用状态码(不是响应状态码)
private int code;
// 描述信息
private String msg;
// 附带数据
private Collection<T> data;
public static ResponseBean success() {
ResponseBean bean = new ResponseBean();
bean.code = 0;
bean.msg = "success";
return bean;
}
public static <V> ResponseBean success(Collection<V> data) {
ResponseBean bean = new ResponseBean();
bean.code = 0;
bean.msg = "success";
bean.data = data;
return bean;
}
public static ResponseBean error(final int code,final String msg) {
ResponseBean bean = new ResponseBean();
bean.code = code;
bean.msg = msg;
return bean;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
public Collection<T> getData() {
return data;
}
@Override
public String toString() {
return String.format("{code: %d,msg: %s,data: %s}", this.code, this.msg, this.data);
}
}
- 定义一个全局
Controller异常捕获:
@RestControllerAdvice
public class GlobalExceptionHandler {
// 捕获所有 Exception,一定要加上,阻断默认异常处理器传递
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseBean handleException() {
return ResponseBean.error(3, "internal server error");
}
@ExceptionHandler
@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
public ResponseBean handleServletException(ServletException ex) {
return ResponseBean.error(1, "servlet exception");
}
@ExceptionHandler
public ResponseEntity<ResponseBean> handleSpecialException(ResponseStatusException e) {
return ResponseEntity.status(e.getStatus()).body(ResponseBean.error(2, e.getReason()));
}
}
- 自定义一个全局异常处理器,取代默认的
BasicErrorController,对剩余未捕获的异常进行捕获处理:
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class CustomizedErrorController implements ErrorController {
@Override
public String getErrorPath() {
return null;
}
@RequestMapping
@ResponseBody
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseBean error(HttpServletRequest request) {
return ResponseBean.error(4, "not found");
}
}
在上述代码中添加自己需要进行捕获的异常,则基本上能完成全局异常捕获,并统一数据格式下发。