Java Spring Boot 自定义异常与全局异常处理

发布时间 2023-12-14 17:03:36作者: 进击的davis

我们在对比 过滤器与拦截器 一文中,知道请求过来,各种拦截处理的顺序:

  • 1.过滤器
  • 2.拦截器
  • 3.controllerAdvice
  • 4.AOP
  • 5.controller
  • 6.AOP
  • 7.controllerAdvice
  • 8.拦截器
  • 9.过滤器

今天我们学习的 自定义异常与异常处理 这块内容,恰好就是 ControllerAdvice/RestControllerAdvice 部分了。

在日常开发中,对于异常的处理我们要么是主要 try...catch... 或者是 throw new xxxException(msg) 几种方式,比如我们主要捕捉异常可以捕捉文件读写时可能存在的 IOException 等,或者是 throw Exception("这是某种异常"),但在 Spring Boot web 开发中,我们还是希望能和业务尽量结合起来,一起优雅通过统一的响应结构输出。

在捕捉异常的时候,我们往往希望 最好是具体的某种异常,再是一般的某种异常,所以这里自然就会涉及到 异常处理的顺序问题,后面我们在应用中也会提到。

接下来的学习中,主要分通用异常处理、自定义异常处理,其中内容含统一响应部分,可看上篇文档,传送门:Java Spring Boot 规范统一响应体结构

通用异常处理

package com.example.springbootexceptiondemo.params;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;


/**
 * @ExceptionHandler(value = MyException.class) -- 注解类型
 * @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) -- 错误码
 * @ResponseBody --  返回json
 * @ControllerAdvice -- 顾名思义,这是一个增强的 Controller。使用这个 Controller ,可以实现三个方面的功能:
 * 1.全局异常处理
 * 2.全局数据绑定
 * 3.全局数据预处理
 */
@Slf4j
@ControllerAdvice // @RestControllerAdvice = @ControllerAdvice + @ResponseBody
@ResponseBody
public class GlobalExceptionAdvice {

    /**
     * 通用异常
     */
    @ExceptionHandler(value = Exception.class)
    public RespInfo exception(Exception ex) {
        log.error("服务器异常: ", ex);
        return RespInfo.fail(RespCodeEnums.INNER_SEVER_ERROR.getCode(), ex.getMessage());
    }
}

如果我们要用到异常处理拦截,在 Spring Boot 中,只需要加上 @ControllerAdvice 注解,或者 @RestControllerAdvice 注解,二者的区别就是是否通过 Json 格式返回,或者说 @RestControllerAdvice = @ControllerAdvice + @ResponseBody ,然后在我们处理的方法中,指定通过哪种 异常类 来匹配,即 @ExceptionHandler注解,通过 value 的值指定匹配的异常类,这里我们通过 Exception类,大部分的异常类通过继承自该类。

如上面代码,一个通用的异常类就实现了。

自定义异常处理

自定义异常,首先要明确,我们这个异常的功能,这里为了演示方便,只是简单继承自 Exception类,实际用的时候,请结合自己的项目。

定义自己的异常类

package com.example.springbootexceptiondemo.exception;

public class MyException extends RuntimeException{
    // 使用时传入错误信息
    public MyException(String msg) {
        super(msg);
    }
}

实际使用时,我们只需要通过传入 错误msg 来构造实例。

在异常处理中添加

package com.example.springbootexceptiondemo.params;

import com.example.springbootexceptiondemo.exception.MyException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class MyExceptionAdvice {

    /**
     * 自定义异常
     * @param ex
     * @return
     */
    @ExceptionHandler(value = MyException.class)
    public RespInfo bizExceptionHandler(MyException ex) {
        log.error("自定义业务异常: ", ex);
        return RespInfo.fail(RespCodeEnums.BIZ_EXCEPTION.getCode(), ex.getMessage());
    }
}

注意我们此处用的注解是 @RestControllerAdvice,我们在 @ExceptionHandler 指定相应的异常类是自定义的异常类,如果应用中哪里触发了该异常,应该就要匹配到这个异常处理的。

当然很多情况下,Java 自带的异常类已经可以满足一定的需求,比如我们有这样的业务场景,对于上传的请求参数,如果在校验validate参数时发生异常,在异常处理中,我们就可以捕捉到,进而返回响应,如下。

Java自带的异常类

package com.example.springbootexceptiondemo.params;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Objects;

@Slf4j
@ControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE - 2) // 异常处理器顺序,越小越优先,-2保证比全局更优先
public class ArgumentsValidateAdvice {

    @ResponseBody
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public RespInfo methodArgsNotValidExceptionHandler(MethodArgumentNotValidException ex) {
        log.error("参数异常,msg ->", ex);
        return RespInfo.fail(RespCodeEnums.PARAMS_NOT_VALID_ERROR.getCode(), Objects.requireNonNull(ex.getBindingResult().getFieldError().getDefaultMessage()));
    }
}

这里其实就是用到参数校验不合规异常,相应的我们的 User bean 如下:

package com.example.springbootexceptiondemo.model;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度在 min ~ max 之间")
    private String password;

    @NotNull(message = "id不能为空")
    @NotBlank(message = "id不能为空")
    private int id;

    @NotBlank(message = "姓名不能为空")
    @Size(min = 2, max = 8, message = "username 长度在 min ~ max 之间")
    private String username;

    private int age;
}

测试异常处理

结合上面的异常处理,我们编写个 controller,一是测试通用异常,二是测试自定义的异常。

package com.example.springbootexceptiondemo.controller;

import com.example.springbootexceptiondemo.exception.MyException;
import com.example.springbootexceptiondemo.model.User;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
public class UserLoginController {

    @GetMapping("/test")
    public User testLogin() throws MyException {
        throw new MyException("测试自定义异常"); // 主动抛出异常
    }

    @PostMapping("/add")
    public User addUser(@RequestBody @Validated User user) {
        return user;
    }

    @GetMapping("/haha")
    public Object haha() {
        int i = 1 / 0;
        return "haha";
    }
}

测试情况:

通用异常处理

image.png

自定义异常处理

image.png

Java自带异常类-参数校验不合规

image.png

注:在多个异常处理中,我们可以通过 @Order注解 来限定异常的处理优先级:

  • Ordered.HIGHEST_PRECEDENCE,级别最高,最优先处理,实际就是 int.MIN 的值
  • Ordered.LOWEST_PRECEDENCE,级别最低,最后处理,实际就是 int.MAX 的值

实际在用的时候如下:

@Slf4j
@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE) 
public class ArgumentsValidateAdvice {

    @ResponseBody
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public RespInfo methodArgsNotValidExceptionHandler(MethodArgumentNotValidException ex) {
        log.error("参数异常,msg ->", ex);
        return RespInfo.fail(RespCodeEnums.PARAMS_NOT_VALID_ERROR.getCode(), Objects.requireNonNull(ex.getBindingResult().getFieldError().getDefaultMessage()));
    }
}

我们在用的时候,要比 Exception类 更优先,只需要调整这个 value 即可。