异常处理
默认规则
1、默认情况下,Spring Boot 提供 /error 处理所有错误的映射
2、对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息
3、对于浏览器客户端,响应一个“ whitelabel”错误视图,以 HTML 格式呈现相同的数据
4、要对其进行自定义,添加 View 解析为 error
5、要完全替换默认行为,可以实现 ErrorController,并注册该类型的 Bean 定义,或添加 ErrorAttributes 类型的组件,以使用现有机制但替换其内容
底层组件
1、ErrorMvcAutoConfiguration:自动配置异常处理规则
2、DefaultErrorAttributes
(1)id:errorAttributes
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
(2)定义错误页面中可以包含哪些数据
3、BasicErrorController
(1)id:basicErrorController
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
(2)处理默认 /error 路径的请求,适配响应:JSON + 白页
@RequestMapping("${server.error.path:${error.path:/error}}")
(3)页面响应
@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);
}
(4)JSON 响应
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
4、View
(1)id:error
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
(2)响应默认错误页
(3)StaticView:error 视图,默认为白页
private final StaticView defaultErrorView = new StaticView();
5、BeanNameViewResolver
(1)id:beanNameViewResolver
@Bean
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
(2)视图解析器:按照返回的视图名,作为组件的 id,查找容器中 View 对象
6、DefaultErrorViewResolver
(1)id:conventionErrorViewResolver
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}
(2)如果发生错误,以 HTTP 的状态码作为视图页地址(viewName),查找在 /error 对应页面
流程
1、第一次请求
(1)执行目标方法,捕获目标方法运行期间任何异常,标志当前请求结束,并且使用 dispatchException 记录
(2)进入视图解析流程(页面渲染)
(3)处理 handler 发生的异常,处理完成返回 ModelAndView
(4)遍历 handlerExceptionResolvers,查找 HandlerExceptionResolver(处理器异常解析器)并处理当前异常
(5)DefaultErrorAttributes 先处理异常,把异常信息保存到请求域,并且返回 null
(6)默认没有任何处理器异常解析器能处理异常,则抛出异常
2、第二次请求
(1)最终底层发送 /error 请求,由底层 BasicErrorController 处理
(2)遍历所有 errorViewResolvers,查找 ErrorViewResolver(错误视图解析器)并解析错误视图
(3)默认错误视图解析器:DefaultErrorViewResolver,把响应状态码作为错误页的地址,如:error/404.html
(4)根据错误状态码(例如 404、500、400 等),生成一个错误视图 error/status,例如 error/404、error/500、error/400
(5)尝试使用模板引擎解析 error/status 视图,即尝试从 classpath 类路径下的 templates 目录下,查找 error/status.html,例如 error/404.html、error/500.html、error/400.html
(6)若模板引擎能够解析到 error/status 视图,则将视图和数据封装成 ModelAndView 返回并结束整个解析流程,否则跳转到(7)
(7)依次从各个静态资源文件夹中查找 error/status.html,若在静态文件夹中找到了该错误页面,则返回并结束整个解析流程,否则跳转到(8)
(8)将错误状态码(例如 404、500、400 等)转换为 4xx 或 5xx,然后重复前 4 个步骤,若解析成功则返回并结束整个解析流程,否则跳转(9)
(9)处理默认的 “/error ”请求,使用 Spring Boot 默认的错误页面(Whitelabel Error Page)
源码
1、DispatcherServlet 类
(1)doDispatch 方法
//使用 dispatchException 记录异常
Exception dispatchException = null;
//执行目标方法
//底层发送/error请求,由BasicErrorController处理
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
//捕获目标方法运行期间任何异常
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
(2)processDispatchResult 方法
//进入视图解析流程(页面渲染),处理handler发生的异常,处理完成返回ModelAndView
mv = processHandlerException(request, response, handler, exception);
(3)processHandlerException 方法
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// Success and error responses may use different content types
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
//遍历所有HandlerExceptionResolver,查找 HandlerExceptionResolver(处理器异常解析器)并处理当前异常
//DefaultErrorAttributes 先处理异常,把异常信息保存到请求域,并且返回 null
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
// We might still need view name translation for a plain error model...
if (!exMv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
exMv.setViewName(defaultViewName);
}
}
if (logger.isTraceEnabled()) {
logger.trace("Using resolved error view: " + exMv, ex);
}
else if (logger.isDebugEnabled()) {
logger.debug("Using resolved error view: " + exMv);
}
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
}
//默认没有任何处理器异常解析器能处理异常,则抛出异常
throw ex;
}
2、DefaultErrorViewResolver 类
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
//尝试以错误状态码作为错误页面名进行解析
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
//尝试以 4xx 或 5xx 作为错误页面页面进行解析
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//错误模板页面,例如 error/404、error/4xx、error/500、error/5xx
String errorViewName = "error/" + viewName;
//当模板引擎可以解析这些模板页面时,就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
//在模板能够解析到模板页面的情况下,返回 errorViewName 指定的视图
return new ModelAndView(errorViewName, model);
}
//若模板引擎不能解析,则去静态资源文件夹下查找 errorViewName 对应的页面
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
//遍历所有静态资源文件夹
for (String location : this.resources.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
//静态资源文件夹下的错误页面,例如error/404.html、error/4xx.html、error/500.html、error/5xx.html
resource = resource.createRelative(viewName + ".html");
//若静态资源文件夹下存在以上错误页面,则直接返回
if (resource.exists()) {
return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
}
} catch (Exception ex) {
}
}
return null;
}
定制错误处理逻辑
1、自定义错误页
(1)如果要显示给定状态代码的自定义 HTML 错误页面,可以将文件添加到 /error 文件夹
(2)错误页面可以是静态 HTML,即添加到任何静态资源文件夹下,也可以使用模板构建,在 /resources/templates 文件夹下
(3)文件名应该是确切的状态代码或系列掩码
(4)仍使用 DefaultErrorViewResolver 解析错误视图
(5)若存在精确错误状态码页面则匹配;若不存在模糊查询,如 4xx.html、5xx.html;若都没有就触发白页
2、@ControllerAdvice + @ExceptionHandler
(1)处理全局异常
(2)底层由 ExceptionHandlerExceptionResolver 处理异常
(3)@ExceptionHandler
public @interface ExceptionHandler {
//处理方法被注解的异常。如果为空,将默认为方法参数列表中列出的任何异常
Class<? extends Throwable>[] value() default {};
}
(4)示例
@ControllerAdvice
public class GlobalExceptionHandler {
//捕获符合@ExceptionHandler中的异常
@ExceptionHandler({ArithmeticException.class,NullPointerException.class})
public String handleArithException(Exception e){
//处理异常逻辑
return "exception";
}
}
3、@ResponseStatus + 自定义异常
(1)底层是 ResponseStatusExceptionResolver 处理异常
(2)获取 @ResponseStatus 注解信息,由 Tomcat 发送 /error,若不能处理 /error,使用 Tomcat 默认错误页
//当前请求立即结束
response.sendError(statusCode, resolvedReason);
(3)@ResponseStatus
public @interface ResponseStatus {
@AliasFor("code")
HttpStatus value() default HttpStatus.INTERNAL_SERVER_ERROR;
@AliasFor("value")
HttpStatus code() default HttpStatus.INTERNAL_SERVER_ERROR;
String reason() default "";
}
(4)示例
@ResponseStatus(value= HttpStatus.FORBIDDEN, reason = "用户数量太多")
public class UserTooManyException extends RuntimeException {
public UserTooManyException(){
}
public UserTooManyException(String message){
super(message);
}
}
4、Spring 底层异常
(1)如:参数类型转换异常
(2)由 DefaultHandlerExceptionResolver 处理框架底层异常
(3)当前请求立刻结束,Tomcat 发送 /error,若不能处理 /error,使用 Tomcat 默认错误页
5、自定义异常解析器
(1)实现 HandlerExceptionResolver 接口
(2)使用 @Component 注册
(3)可以使用 @Order 调整优先级,数字越小,优先级越高,最高优先级为默认全局异常处理规则
6、自定义错误视图解析器(不使用)
(1)实现 ErrorViewResolver 接口
(2)异常解析器主动调用 response.sendError;或异常没有任何异常解析器能处理,Tomcat 底层调用 response.sendError
(3)error 请求转到 Controller,Spring 提供 BasicErrorController 处理 error 请求
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战