几种异常处理原理
# 440.几种异常处理原理
接下来讲讲如何定制 错误处理逻辑
# 概述
有如下几种处理逻辑:
- 自定义错误页:例如 error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就使用 SpringBoot 默认的错误页面。
@ControllerAdvice+@ExceptionHandler
处理全局异常;底层是ExceptionHandlerExceptionResolver
支持的,也推荐使用这种方式。@ResponseStatus
+自定义异常 ;底层是ResponseStatusExceptionResolver
支持的,把 responsestatus 注解的信息底层调用 response.sendError(statusCode, resolvedReason);tomcat 发送的/error- Spring 底层的异常,如参数类型转换异常;由 DefaultHandlerExceptionResolver 处理框架底层的异常
- ErrorViewResolver :一般不会自定义,这里不表
# @ControllerAdvice + @ExceptionHandler
根据 ControllerAdvice
的源码可知,它也是一个 @Component
,最后也会放到容器中:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
2
3
4
5
我们新建一个类,加上 @ControllerAdvice
注解:此时该类就可以处理整个 web 中,controller 的异常了。
package com.peterjxl.learnspringbootwebadmin.exception;
import org.springframework.web.bind.annotation.ControllerAdvice;
@ControllerAdvice
public class GlobalExceptionHandler {
}
2
3
4
5
6
7
由于我们自定义的方法中,可能有很多方法都会抛出异常,我们可以写一个方法,统一处理这些异常:
package com.peterjxl.learnspringbootwebadmin.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})
public String handlerArithmeticException(Exception e) {
log.error("异常是:{}", e);
return "login";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@ExceptionHandler
表明该方法是一个异常处理器,里面的值表明其能处理什么异常。最后返回一个字符串;根据之前的源码分析可知,返回的都是一个 ModelAndView 对象,因此我们返回的字符串是视图的地址,最后也会转为该类型的对象。
也可以直接返回 ModelAndView 对象,只需设置好数据和 View 即可,参考 SpringMVC 教程的 相关部分 (opens new window)
接下来重启,访问相关页面,就会跳转到登录页。
现在我们通过 debug 的方式,来分析。
步骤和之前类似,首先是执行目标方法,然后发生了异常,因此会将异常赋值给 dispatchException
,然后调用 processDispatchResult
方法,
步入 processDispatchResult
,可以看到其会调用 processHandlerException
方法,得到 ModelAndView
对象:
因此我们步入 processHandlerException
方法,循环遍历 HandlerExceptionResolver
:
在 HandlerExceptionResolver
中,ExceptionHandlerExceptionResolver
(如下图)就包含了所有用@ExceptionHandler
注解的方法:
因此,最后会调用我们自己写的 ExceptionHandler
,得到 login,并包装为 ModelAndView
。
# @ResponseStatus
+ 自定义异常
有时候我们会自定义异常,那么就可以结合 @ResponseStatus
注解。
例如,我们在校验一个参数(比如用户数量),如果太多就抛出异常;
首先,我们自定义一个异常
package com.peterjxl.learnspringbootwebadmin.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "用户数量太多")
public class UserTooManyException extends RuntimeException {
public UserTooManyException() {
}
public UserTooManyException(String message) {
super(message);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
然后我们抛出一个异常:
@GetMapping("/dynamic_table")
public String dynamic_table(Model model){
List<User> users = Arrays.asList(new User("zhangsan", "123456"),
new User("lisi", "123444"),
new User("haha", "aaaaa"),
new User("hehe", "aaddd"));
model.addAttribute("users", users);
if (users.size() > 3){
throw new UserTooManyException();
}
return "table/dynamic_table";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
重启,访问 localhost: 9999/dynamic_table (opens new window):
接下来我们以 debug 方式启动,来看其原理。我们直接来到 resolveException
方法,ResponseStatusExceptionResolver
就是能处理我们自定义的异常:
我们步入 resolveException
:
在步入 doResolveException
:会通过注解工具类,获取注解信息,是否包含了 ResponseStatus
,然后调用 resolveResponseStatus
方法,获取状态码和错误信息:
resolveResponseStatus
源码:
protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception {
int statusCode = responseStatus.code().value();
String reason = responseStatus.reason();
return applyStatusAndReason(statusCode, reason, response);
}
2
3
4
5
6
在最后一行的 applyStatusAndReason
方法中,还有直接调用 response.sendError
方法,直接跳转到一个错误页(会发送一个 /error
的请求):
protected ModelAndView applyStatusAndReason(int statusCode, @Nullable String reason, HttpServletResponse response) throws IOException {
if (!StringUtils.hasLength(reason)) {
response.sendError(statusCode);
}
else {
String resolvedReason = (this.messageSource != null ? this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) : reason);
response.sendError(statusCode, resolvedReason);
}
return new ModelAndView();
}
2
3
4
5
6
7
8
9
10
11
注意,此时请求就相当于直接结束了,没有封装 ModelAndView
对象。
# Spring 底层的异常
比如,我们需要前端传一个 a 的参数:
@GetMapping("/basic_table")
public String basic_table(@RequestParam("a") int a){
int i = 10 / 0;
return "table/basic_table";
}
2
3
4
5
但是,前端没有传,此时就会报错:
此时发生的异常就是 Spring 自带的异常了,MissingServletRequestParameterException
:
我们既没有定义 ExceptionHandler 来处理该异常,也没有定义 ResponseStatus 来处理该异常,那么默认就由
DefaultHandlerExceptionResolver
来处理;
我们步入其 doResolveException
方法:
它里面就是一堆 if 分支,判断该异常是什么类型的,然后设置相应的错误信息:
其中就有我们的 MissingServletRequestParameterException
:
else if (ex instanceof MissingServletRequestParameterException) {
return handleMissingServletRequestParameter(
(MissingServletRequestParameterException) ex, request, response, handler);
}
2
3
4
然后我们步入进 handleMissingServletRequestParameter
:可以看到其底层还是 response.sendError
:
protected ModelAndView handleMissingServletRequestParameter(MissingServletRequestParameterException ex, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView();
}
2
3
4
至此,我们对 Spring 的异常解析器都做了一个简单的介绍。
其实这 3 个异常解析器,都实现了 HandlerExceptionResolver
接口,因此我们也可以自定义一个。
# 自定义 HandlerExceptionResolver
我们可以自定义一个异常解析器:
package com.peterjxl.learnspringbootwebadmin.exception;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try{
response.sendError(511, "我喜欢的错误");
}catch (Exception e){
e.printStackTrace();
}
return new ModelAndView();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
注意,即使我们定义了一个 HandlerExceptionResolver
,但是由于默认的 DefaultHandlerExceptionResolver
也能处理,并且其排序是比较靠前的:
所以 for 循环遍历到该处理器就回 break,结束循环。因此我们定义的异常解析器不会生效;想要修改其排序,可以加个 @Order
注解:
package com.peterjxl.learnspringbootwebadmin.exception;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try{
response.sendError(511, "我喜欢的错误");
}catch (Exception e){
e.printStackTrace();
}
return new ModelAndView();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Order
中,里面的值越小,优先级越高。而 Ordered 的源码如下:可以看到就是 Integer 的最小值
public interface Ordered {
int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
int getOrder();
}
2
3
4
5
修改后,就能执行我们自己的异常解析器了:
- 01
- 中国网络防火长城简史 转载10-12
- 03
- 公告:博客近期 RSS 相关问题10-02