[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");
}
}
在上述代码中添加自己需要进行捕获的异常,则基本上能完成全局异常捕获,并统一数据格式下发。