在SpringBoot中,常用的异常处理有两种:一种是 BasicErrorController
,另一种是 @ControllerAdvice
。BasicErrorController
用于处理非Controller
抛出的异常,而@ControllerAdvice
用于处理Controller
抛出的异常,对于非Controller抛出的异常它是不会管的。但是,如果是Controller层调用Service层,从Service层抛出,依然会抛到Controller,所以还是会调用@ControllerAdvice进行处理。
如果返回格式是json,或用postman访问一个不存在的端点,响应消息如下:
{
"timestamp": "2020-05-25T12:29:35.418+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/"
}
原因:如果程序抛出了未能被 @ControllerAdvice
处理的异常,SpringBoot 会把请求转向 /error
这个端点(转发),SpringBoot 有一个默认的 Controller 用于处理这类请求,它就是 BasicErrorController
。
BasicErrorController
中有两个标注了 @RequestMapping
的方法,当请求头的 Accept 中包含 text/html
时,会调用 public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)
方法,其他情况则会调用 public ResponseEntity
方法。通过断点调试可以看到,这个 request 对象中封装了很多有用的信息,我们可以利用这些信息来定义自己的信息体。
BasicErrorController
默认返回的信息如下图:
这个方法是属于类 DefaultErrorAttributes
,返回一个map,里面包含了异常发生的时间、异常状态、异常详细信息、发生异常的请求路径。如下图是一个异常发生的时候所获取到的异常信息:
那么,我们该如何抛弃掉或者覆盖掉Spring Boot默认的异常处理呢,Spring Boot开发指南上提供了以下四种方法:
1、自定义一个bean,实现 ErrorController
接口,那么默认的错误处理机制将不再生效。
2、自定义一个bean,继承 BasicErrorController
类,使用一部分现成的功能,自己也可以添加新的public方法,使用 @RequestMapping 及其produces属性指定新的地址映射。
3、自定义一个 ErrorAttribute
类型的bean,那么还是默认的两种响应方式,只不过改变了内容项而已。
4、定义一个bean,继承 AbstractErrorController
。
下面使用第2种方式。
public class MyErrorController extends BasicErrorController {
public MyErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorProperties, errorViewResolvers);
}
@SneakyThrows
@Override
protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
Map<String, Object> attributes = super.getErrorAttributes(request, includeStackTrace);
attributes.remove("timestamp");
attributes.remove("status");
attributes.remove("error");
attributes.remove("exception");
attributes.remove("path");
String messageCode = (String) attributes.get("message");
ErrorEnum errorEnum = null;
try {
errorEnum = ErrorEnum.getEnumByCode(messageCode);
} catch (Exception e) {
e.printStackTrace();
}
if (errorEnum == null){
attributes.put("type","0");
attributes.put("message","枚举类异常");
return attributes;
}
attributes.put("errorControllerType","basicErrorController");
attributes.put("type",errorEnum.getCode());
attributes.put("message",errorEnum.getMessage());
return attributes;
}
}
SpringBoot关于错误处理的配置类 ErrorMvcAutoConfiguration
,其中对 BasicErrorController
进行了配置,因此子类继承BasicErrorController后也应当对其进行配置。
ErrorMvcAutoConfiguration中的 BasicErrorController 配置如下:
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers);
}
参考这个配置,我们自定义的MyErrorController进行配置:
@Configuration
public class ErrorConfiguration {
@Bean
public MyErrorController basicErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties,
ObjectProvider<List<ErrorViewResolver>> errorViewResolversProvider) {
return new MyErrorController(errorAttributes, serverProperties.getError(),
errorViewResolversProvider.getIfAvailable());
}
}
Spring Boot的错误处理自动配置类:org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration(在spring-boot-autoconfigure.jar下),这个类注册了很多关于错误处理的组件,主要的有四个 DefaultErrorAttributes
、BasicErrorController
、ErrorPageCustomizer
、DefaultErrorViewResolver
。
public class ErrorMvcAutoConfiguration {
...
//注入DefaultErrorAttributes组件
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes(
this.serverProperties.getError().isIncludeException());
}
//注入BasicErrorController 组件
//@Bean放在方法上,如果参数类型所对应的实例在spring容器中只有一个,则默认选择这个实例。
//如果有多个,则需要根据参数名称来选择(参数名称就相当于是spring的配置文件中的bean的id)。
//errorAttributes从容器中注入的其实就是DefaultErrorAttributes(实现ErrorAttributes )。
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
this.errorViewResolvers);
}
//注入ErrorPageCustomizer 组件
@Bean
public ErrorPageCustomizer errorPageCustomizer() {
return new ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath);
}
@Configuration
static class DefaultErrorViewResolverConfiguration {
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
ResourceProperties resourceProperties) {
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
}
//注入DefaultErrorViewResolver 组件
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean
public DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext,
this.resourceProperties);
}
}
...
}
Spring boot错误处理步骤:
1、当程序发生异常或者错误,ErrorPageCustomizer
组件就会生效,它的作用是定制错误的响应规则。
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
...
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
//this.properties.getError().getPath())的值是"/error"
ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath
.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
...
}
这个方法说明当系统出现错误后就去到 /error
进行处理。
2、/error 请求就会被 BasicErrorController
处理,处理完后返回响应页面。
@Controller
//如果server.error.path获取不到就用${error.path:/error},error.path获取不到就用/error,说明这个controller的地址是/error
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
...
//MediaType.TEXT_HTML_VALUE="text/html"
//如果是浏览器的请求就走这个方法
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
//构建错误数据,保存到model中
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//构建ModelAndView即处理完返回的视图,也就是响应页面
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
//如果找不到返回error视图
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
//如果是客户端的请求就走这个方法
@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);
}
...
}
3、响应页面的规则,实际上是通过 DefaultErrorViewResolver
组件来处理的。上面代码中的 resolveErrorView
方法就是返回响应页面的,它是 BasicErrorController 的父类 AbstractErrorController
中的方法。
public abstract class AbstractErrorController implements ErrorController {
private final ErrorAttributes errorAttributes;
private final List<ErrorViewResolver> errorViewResolvers;
...
protected ModelAndView resolveErrorView(HttpServletRequest request,
HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
//获取所有的ErrorViewResolver,包括DefaultErrorViewResolver,所以我们可以自己定制自己的ErrorViewResolver组件
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
}
DefaultErrorViewResolver
类源码:
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new EnumMap<>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
//构建响应页面规则,HttpStatus 是错误的状态码:404、500等。
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
//调用resolve方法创建错误视图
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
//如果没有这个视图并且静态资源中也没有错误页面(具体状态码.html例如:404.html),而且状态码是4开头或者5开头。
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
//创建error/4xx或者error/5xx视图。
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//错误视图例:error/404
String errorViewName = "error/" + viewName;
//使用模板引擎找到这个视图
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
//如果能找到视图就创建视图对象
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
//没有找到则调用此方法,这个方法是去默认的静态资源文件夹下找
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
// this.resourceProperties.getStaticLocations()获取静态资源文件夹:public、static这些。
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
//错误页面,例:error/400.html
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
//如果存在这个html页面就用这个,否则返回null
return new ModelAndView(new HtmlResourceView(resource), model);
}
} catch (Exception ex) {
}
}
return null;
}
}
由上可知,视图名是状态码的值.html
,状态码是在 BasicErrorController
中通过 getStatus
方法获取的。 HttpStatus status = getStatus(request);
getStatus
是 BasicErrorController 的父类 AbstractErrorController
中的方法:
protected HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
try {
return HttpStatus.valueOf(statusCode);
} catch (Exception ex) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
所以设置状态码时设置名为"javax.servlet.error.status_code"
。
request.setAttribute("javax.servlet.error.status_code", 500);
DefaultErrorViewResolver
先去模板引擎(以thymeleaf为例)文件夹下(templates)找”error/状态码(404、500等).html"
文件,没有,则去Spring Boot默认静态文件夹下找,没有,再去找”error/4xx(5xx).html"
文件,找到了则创建对应的视图,没找到返回null。而在 BasicErrorController 的 resolveErrorView 方法的最后:
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
表示如果找不到视图,则创建默认视图 "error"
。而在ErrorMvcAutoConfiguration
中引入了error视图:
private final StaticView defaultErrorView = new StaticView();
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
error视图的类型是 StaticView
,这个类的作用就是生成Spring Boot默认的错误页面。
4、错误数据
通过 @Bean 注解,把 DefaultErrorAttributes
注入到了 BasicErrorController
中,而在 BasicErrorController 的 errorHtml
方法中,调用 getErrorAttributes
方法,此方法再调用 DefaultErrorAttributes
的 getErrorAttributes
方法获取了封装好的错误数据。
4xx.html/5xx.html
作为错误页面的文件名来匹配4类型和5类型的所有错误,但是精确优先(优先寻找精确的状态码.html)。没有模板引擎时直接去Spring Boot默认的静态资源文件夹下找。如果都没有找到,就生成默认的错误页面。 DefaultErrorAttributes
类来实现的,我们可以通过自定义一个bean继承 DefaultErrorAttributes 来扩展自己的错误数据处理,这时 DefaultErrorAttributes 会失效(@ConditionalOnMissingBean)。 @Component//将MyErrorAttributes加到容器中
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
// 在DefaultErrorAttributes的基础上做扩展。
Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
map.put("company", "comDMF"); //添加自己的错误数据
return map;
}
}
@ControllerAdvice
是 Spring 提供的 Controller 级的异常拦截注解,通过它可以实现全局异常拦截。@RestControllerAdvice
是 @ControllerAdvice
+ @ResponseBody
的语法糖。
这种统一异常处理,只需要在类上标注 @ControllerAdvice
注解,然后在类的方法上标注 @ExceptionHandler
注解并指出它要处理的异常类型。如下图:
@ControllerAdvice
public class ExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public Result exceptionHandler(Exception e){
e.printStackTrace();
return new Result(StatusCode.ERROR, e.getMessage());
}
}
在单个 Controller 中也可以定义被 @ExceptionHandler
标注的方法作为本 Controller 的异常处理方法,例如 BasicErrorController
中 mediaTypeNotAcceptable
方法。
优先级是,Controller 中的异常处理方法 > 全局的异常处理方法。定义的处理异常类型越详细优先级越高。比如:如果出现 RuntimeException,那么它会被处理 RuntimeException 异常的方法拦截,不会被处理 Exception 异常的方法拦截。但是如果处理 Exception 异常的方法定义在它自己的 Controller 中,那么它只会被本 Controller 中的异常拦截方法拦截。
统一异常处理的使用:大多数情况下,不要去继承重写 BasicErrorController,使用 @ControllerAdvice 已经够用,因为你的所有请求都会经过Controller。