SpringBoot 统一异常处理

SpringBoot 统一异常管理

前言

在使用 SpringBoot 开发接口时,经常会遇到异常,比如数据库操作错误导致的异常。如果不对这些异常进行处理,异常日志就会直接传递到前端,一方面会泄露系统内部的信息;另一方面也会影响前端判断接口调用是否成功(不是统一的响应格式)。

但是如果在每个接口中处理异常的话,不仅很麻烦,而且产生了大量的冗余代码,不容易维护。因此,我们可以建立一个全局的异常处理器。

SpringWeb 提供了 @ControllerAdvice 注解,可以用来创建统一的异常处理器。

模拟异常

我们新建一个 UserController,再创建 /user/login 接口,直接返回一个 RuntimeException 来模拟异常情况。

1
2
3
4
5
6
7
8
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public String login() {
throw new RuntimeException("An exception happened");
}
}

此时直接访问此接口会得到以下页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Thu Apr 23 15:53:45 CST 2020
There was an unexpected error (type=Internal Server Error, status=500).
an exception happened
java.lang.RuntimeException: an exception happened
at com.example.demo.UserController.login(UserController.java:11)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:879)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
......

创建异常处理器

我们使用 @ControllerAdvice@ExceptionHandler 来创建全局的异常处理器。

1
2
3
4
5
6
7
8
9
10
11
12
@ControllerAdvice
public class GlobalExceptionHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(RuntimeException.class)
@ResponseBody
Message handleRuntimeException(RuntimeException e) {
LOGGER.error(e.getMessage(), e);
return ResultUtil.error(e.getMessage());
}
}

其中 @ControllerAdvice 注解指定了处理器的作用范围,@ExceptionHandler 注解指定了可以处理的异常类型。

测试异常处理器

此时再访问接口就可以得到经过处理的结果:

1
2
3
4
{
"code": "4000",
"msg": "An exception happened"
}

这样既不会泄露系统运行轨迹也方便了前端调用接口。

常见用途

自定义异常

我们可以自定义异常,再利用全局异常处理器进行处理,就可以根据自己的业务情况,方便地在需要的地方直接抛出异常。

自定义异常:

1
2
3
4
5
public class CustomException extends RuntimeException {
public CustomException(String message) {
super(message);
}
}

异常处理器:

1
2
3
4
5
6
@ExceptionHandler(CustomException.class)
@ResponseBody
Message handleCustomException(CustomException e) {
LOGGER.error(e.getMessage(), e);
return ResultUtil.error(e.getMessage());
}

模拟异常:

1
2
3
4
5
6
7
8
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public String login() {
throw new CustomException("这是一个自定义异常");
}
}

访问结果:

1
2
3
4
{
"code": "4000",
"msg": "这是一个自定义异常"
}

自动验证参数

我们可以将其和 @Valid 注解结合起来使用,实现自动验证请求参数。

定义 Bean:在需要验证的属性上添加相应的注解

1
2
3
4
5
6
7
8
public class UserBean {
@NotNull(message = "username 不能为空")
String username;
@NotNull(message = "password 不能为空")
String password;

// getters and setters
}

自动验证:使用 @Valid 注解,要求进行验证

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public Message login(@Valid @RequestBody UserBean user) {
System.out.println(user.getUsername());
return ResultUtil.success(user);
}
}

异常处理器:自动捕获验证异常,并处理

1
2
3
4
5
6
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
Message handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
LOGGER.error(e.getMessage(), e);
return ResultUtil.error(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
}

测试结果:

1
2
3
4
{
"code": "4000",
"msg": "password 不能为空"
}