SpringBoot 全局异常处理的几种常见姿势
|字数总计:3.1k|阅读时长:12分钟|阅读量:
异常处理方式的发展
web.xml
在还没有Spring的时候,开发使用的是原生的Servlet + tomcat容器。那个时候就提供了通用的异常的处理配置方式的:在web.xml里进行如下配置:
1 2 3 4 5 6 7 8 9 10 11
| <error-page> <error-code>500</error-code> <location>/500.jsp</location> </error-page>
<error-page> <exception-type>java.lang.RuntimeException</exception-type> <location>/500.jsp</location> </error-page>
|
SpringMVC
Spring MVC提供处理异常的方式主要分为两种:
- 实现
HandlerExceptionResolver
方式
@ExceptionHandler
注解方式。注解方式也有两种用法:
- 使用在Controller内部
- 配置
@ControllerAdvice
一起使用实现全局处理
这几种方式的优先级递减,被一个捕获处理了就不会去执行其他的了。
异常处理方式 |
说明 |
@Controller+@ExceptionHandler |
单个 Controller 异常处理方法 |
@ControllerAdvice + @ExceptionHandler |
全局 Controller 异常处理类 |
HandlerExceptionResolver |
异常处理器实现类(内部有优先级规则) |
error-page |
Tomcat 错误页 |
第一种方式:HandlerExceptionResolver
HandlerExceptionResolver是Spring提供的一种异常处理机制,它允许我们在应用程序中以统一的方式处理控制器方法引发的异常。
要使用HandlerExceptionResolver,我们需要创建一个实现该接口的类,并在其中定义如何处理异常。例如:
1 2 3 4 5 6 7 8 9 10
|
@Order(-100) public class ForumExceptionHandler implements HandlerExceptionResolver {
@Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { } }
|
其中:
@Order
注解用于指定Spring 中组件的加载顺序。它接受一个整数值,数值越小,组件的优先级越高,加载顺序越靠前。
- 在
resolveException
方法中,我们可以自定义异常处理逻辑,根据异常类型返回不同的ModelAndView。
可以看到,方法resolveException
接收的参数是Exception
类型,说明它能接收所有类型的异常,具体的处理逻辑由我们自己实现,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { if (ex instanceof ForumException) { } else if (ex instanceof AsyncRequestTimeoutException) { } else if (ex instanceof HttpMediaTypeNotAcceptableException) { } else { } return null; }
|
HandlerExceptionResolver 的工作原理主要基于Spring MVC的异常处理流程。当一个请求进入SpringMVC后,它会根据请求信息找到对应的处理器(handler,也就是Controller)。在Controller执行过程中,如果抛出了异常,Spring MVC就会启动异常处理流程。
- 异常发生:当Controller执行过程中抛出异常,Spring MVC捕获到这个异常后,会进入异常处理流程。
- 查找异常解析器: Spring MVC会遍历所有已注册的 HandlerExceptionResolver实现。比如说我们自定义的ForumExceptionHandler,Spring MVC本身也提供了一些默认的实现,比如
DefaultHandlerExceptionResolver、ExceptionHandlerExceptionResolver。
- 执行异常解析器:对于每个HandlerExceptionResolver实现,Spring MVC 会调用它的
resolveException方法,并传入请求、响应、处理器和异常对象。如果解析器能处理这个异常,它会返回一个非空的ModelAndView对象。这个对象封装了异常处理后的视图和模型数据。
- 处理返回结果:当resolveException方法返回一个非空的ModelAndView对象时,Spring MVC会将这个对象用于生成最终的响应。可能渲染一个错误视图、设置响应状态码等。如果所有的
HandlerExceptionResolver都无法处理这个异常(即都返回了空的ModelAndView对象),那么Spring MVC 会将异常重新抛出,以便其他异常处理器(如Servlet容器)进行处理。
通过这个流程,HandlerExceptionResolver能够在Spring MVC中统一管理和处理异常。最后记得在SpringBoot的启动类中将自定义的 HandlerExceptionResolver添加到Spring配置中。
1 2 3 4 5 6
| public class QuickForumApplication implements WebMvcConfigurer, ApplicationRunner { @Override public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) { resolvers.add(0, new ForumExceptionHandler()); } }
|
但是,HandlerExceptionResolver其实比较古老了,使用起来有一个很难受的地方。提个问题:
观察方法resolveException
的返回类型,发现是ModelAndView
,这在前后端分离的项目中就有点难办了。大概有两种解决思路:
- response直接输出json:
response.getWriter().print(jsonStr);
- 借助MappingJackson2JsonView
不过总归不是很舒服的,下面的第二种方式则没有这个问题了。
第二种方式:@ControllerAdvice + @ExceptionHandler
除了HandlerExceptionResolver
,全局异常还可以采用@ControllerAdvice
注解的方式。它可以将通用的操作和逻辑抽离出来,避免在每个控制器中重复相同的操作。
此注解是Spring 3.0
后提供的处理异常的注解,整个Spring
在3.0+
中新增了大量的能力来对REST
应用提供支持,此注解便是其中之一。 它(只能)标注在方法上,可以使得这个方法成为一个异常处理器,处理指定的异常类型。
1 2 3 4 5 6 7 8
| @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ExceptionHandler { Class<? extends Throwable>[] value() default {}; }
|
具体做法:新建一个全局异常控制器GlobalExceptionHandler,内容如下所示.
1 2 3 4 5 6 7 8 9 10
| @RestControllerAdvice public class GlobalExceptionHandler {
@ExceptionHandler(value = ForumAdviceException.class) public ResVo<String> handleForumAdviceException(ForumAdviceException e) { return ResVo.fail(e.getStatus()); } }
|
相关的注解解释如下:
@ControllerAdvice 捕获 Controller
层抛出的异常,如果添加 @ResponseBody
返回信息则为JSON
格式。
@RestControllerAdvice 相当于 @ControllerAdvice
与 @ResponseBody
的结合体。
@ExceptionHandler 统一处理一种类的异常,减少代码重复率,降低复杂度。
OK,完事了,是不是很简单?
另外,要注意一点,ExceptionHandler
也可以单独使用,这种情况下只对Controller
本类生效。
@ExceptionHandler
只能标注在方法上。既能标注在Controller
本类内的方法上(只对本类生效),也可配合@ControllerAdvice
一起使用(对全局生效)
- 执行时的匹配顺序如下:优先匹配本类(本
Controller
),再匹配全局的。
@ExceptionHandler
的处理和执行是由本类完成的,同一个Class上的所有@ExceptionHandler
方法对应着同一个ExceptionHandlerExceptionResolver
,不同Class上的对应着不同的~
具体的源码解析可以参考:https://cloud.tencent.com/developer/article/1525171。
在项目中,通常的做法就是新建一个GlobalExceptionHandler
(名字大同小异)的类,它的作用一般被标注上了@ControllerAdvice/@RestControllerAdvice
用于处理全局异常。
@ControllerAdvice注解的其他作用
@ControllerAdvice,是Spring3.2提供的新注解,它是一个Controller增强器,可对controller中被 @RequestMapping注解的方法加一些逻辑处理。主要作用有一下三种
- 通过@ControllerAdvice注解可以将对于控制器的全局配置放在同一个位置。
- 注解了@ControllerAdvice的类的方法可以使用@ExceptionHandler、@InitBinder、@ModelAttribute注解到方法上。
- @ExceptionHandler:用于全局处理控制器里的异常,进行全局异常处理
- @InitBinder:用来设置WebDataBinder,用于自动绑定前台请求参数到Model中,全局数据预处理。
- @ModelAttribute:本来作用是绑定键值对到Model中,此处让全局的@RequestMapping都能获得在此处设置的键值对 ,全局数据绑定。
- @ControllerAdvice注解将作用在所有注解了@RequestMapping的控制器的方法上。
实现一个全局异常处理类
首先,自定义业务异常,该类中记录一个Status
字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import lombok.Getter;
public class ForumAdviceException extends RuntimeException { @Getter private Status status;
public ForumAdviceException(Status status) { this.status = status; }
public ForumAdviceException(int code, String msg) { this.status = Status.newStatus(code, msg); }
public ForumAdviceException(StatusEnum statusEnum, Object... args) { this.status = Status.newStatus(statusEnum, args); } }
|
Status
类包含两个变量code
和msg
,分别是业务状态码和描述信息。业务状态码可以自己传入时指定,也有一些规定,在StatusEnum
枚举变量中定义,格式是业务 - 状态 - code。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;
@Data @NoArgsConstructor @AllArgsConstructor public class Status {
@ApiModelProperty(value = "状态码, 0表示成功返回,其他异常返回", required = true, example = "0") private int code;
@ApiModelProperty(value = "正确返回时为ok,异常时为描述文案", required = true, example = "ok") private String msg;
public static Status newStatus(int code, String msg) { return new Status(code, msg); }
public static Status newStatus(StatusEnum status, Object... msgs) { String msg; if (msgs.length > 0) { msg = String.format(status.getMsg(), msgs); } else { msg = status.getMsg(); } return newStatus(status.getCode(), msg); }
public static void main(String[] args) { Status status = Status.newStatus(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "报错了", "666"); System.out.println(status); } }
|
StatusEnum
类如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| import lombok.Getter;
@Getter public enum StatusEnum { SUCCESS(0, "OK"),
ILLEGAL_ARGUMENTS(100_400_001, "参数异常"), ILLEGAL_ARGUMENTS_MIXED(100_400_002, "参数异常:%s"),
FORBID_ERROR(100_403_001, "无权限"),
FORBID_ERROR_MIXED(100_403_002, "无权限:%s"), FORBID_NOTLOGIN(100_403_003, "未登录"),
RECORDS_NOT_EXISTS(100_404_001, "记录不存在:%s"),
UNEXPECT_ERROR(100_500_001, "非预期异常:%s"),
UPLOAD_PIC_FAILED(100_500_002, "图片上传失败!"),
ARTICLE_NOT_EXISTS(200_404_001, "文章不存在:%s"), COLUMN_NOT_EXISTS(200_404_002, "教程不存在:%s"), COLUMN_QUERY_ERROR(200_500_003, "教程查询异常:%s"), COLUMN_ARTICLE_EXISTS(200_500_004, "专栏教程已存在:%s"), ARTICLE_RELATION_TUTORIAL(200_500_006, "文章已被添加为教程:%s"),
COMMENT_NOT_EXISTS(300_404_001, "评论不存在:%s"),
LOGIN_FAILED_MIXED(400_403_001, "登录失败:%s"), USER_NOT_EXISTS(400_404_001, "用户不存在:%s"), USER_EXISTS(400_404_002, "用户已存在:%s"), USER_LOGIN_NAME_REPEAT(400_404_003, "用户登录名重复:%s"), USER_PWD_ERROR(400_500_002, "用户名or密码错误");
private int code;
private String msg;
StatusEnum(int code, String msg) { this.code = code; this.msg = msg; }
public static boolean is5xx(int code) { return code % 1000_000 / 1000 >= 500; }
public static boolean is403(int code) { return code % 1000_000 / 1000 == 403; }
public static boolean is4xx(int code) { return code % 1000_000 / 1000 < 500; } }
|
返回给前端的结果类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import io.swagger.annotations.ApiModelProperty; import lombok.Data;
import java.io.Serializable;
@Data public class ResVo<T> implements Serializable { private static final long serialVersionUID = -510306209659393854L; @ApiModelProperty(value = "返回结果说明", required = true) private Status status;
@ApiModelProperty(value = "返回的实体结果", required = true) private T result;
public ResVo() { }
public ResVo(Status status) { this.status = status; }
public ResVo(T t) { status = Status.newStatus(StatusEnum.SUCCESS); this.result = t; }
public static <T> ResVo<T> ok(T t) { return new ResVo<T>(t); }
@SuppressWarnings("unchecked") public static <T> ResVo<T> fail(StatusEnum status, Object... args) { return new ResVo<>(Status.newStatus(status, args)); }
public static <T> ResVo<T> fail(Status status) { return new ResVo<>(status); } }
|
有了上面的准备,我们就可以开始使用@ControllerAdvice
写异常处理相关的逻辑了,新建GlobalExceptionHandler
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @RestControllerAdvice public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class) public ResVo<String> handleException(Exception e) { return ResVo.fail(StatusEnum.UNEXPECT_ERROR); }
@ExceptionHandler(value = ForumAdviceException.class) public ResVo<String> handleForumAdviceException(ForumAdviceException e) { return ResVo.fail(e.getStatus()); }
@ExceptionHandler(value = NoHandlerFoundException.class) public ResVo<String> handleNoHandlerFoundException(NoHandlerFoundException e) { return ResVo.fail(StatusEnum.RECORDS_NOT_EXISTS, e.getRequestURL()); } }
|
最后,新建一个Controller,做个测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @RestController @RequestMapping("test") public class testControllerAdvice {
@RequestMapping("111") public String testControllerAdvice(Model model) { throw new ForumAdviceException(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "非法的邮箱接收人"); }
@RequestMapping("222") public String testControllerAdvice2(Model model) { throw new ForumAdviceException(StatusEnum.UNEXPECT_ERROR, "超时未登录"); }
@RequestMapping("333") public String testControllerAdvice3(Model model) { throw new ForumAdviceException(StatusEnum.COMMENT_NOT_EXISTS, "评论ID=1001"); } }
|
浏览器访问http://localhost:8080/111111111111
,结果如下,看起来表现良好,结束!
参考