SpringBoot 统一异常处理

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

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

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

模拟异常

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

@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/login")
    public String login() {
        throw new RuntimeException("An exception happened");
    }
}

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

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 来创建全局的异常处理器。

@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 注解指定了可以处理的异常类型。

测试异常处理器

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

{
  "code": "4000",
  "msg": "An exception happened"
}

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

常见用途

自定义异常

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

自定义异常:

public class CustomException extends RuntimeException {
    public CustomException(String message) {
        super(message);
    }
}

异常处理器:

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

模拟异常:

@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/login")
    public String login() {
        throw new CustomException("这是一个自定义异常");
    }
}

访问结果:

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

自动验证参数

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

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

public class UserBean {
    @NotNull(message = "username 不能为空")
    String username;
    @NotNull(message = "password 不能为空")
    String password;

    // getters and setters
}

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

@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/login")
    public Message login(@Valid @RequestBody UserBean user) {
        System.out.println(user.getUsername());
        return ResultUtil.success(user);
    }
}

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

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

测试结果:

{
  "code": "4000",
  "msg": "password 不能为空"
}