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

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

  • 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 规范统一响应体结构

通用异常处理

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.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类,实际用的时候,请结合自己的项目。

定义自己的异常类

java 复制代码
package com.example.springbootexceptiondemo.exception;

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

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

在异常处理中添加

java 复制代码
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自带的异常类

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 如下:

java 复制代码
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,一是测试通用异常,二是测试自定义的异常。

java 复制代码
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";
    }
}

测试情况:

通用异常处理

自定义异常处理

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

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

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

实际在用的时候如下:

java 复制代码
@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 即可。

相关推荐
追逐时光者3 小时前
推荐 12 款开源美观、简单易用的 WPF UI 控件库,让 WPF 应用界面焕然一新!
后端·.net
Jagger_3 小时前
敏捷开发流程-精简版
前端·后端
苏打水com4 小时前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
间彧5 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧5 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧5 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧5 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧5 小时前
Spring Cloud Gateway详解与应用实战
后端
EnCi Zheng7 小时前
SpringBoot 配置文件完全指南-从入门到精通
java·spring boot·后端
烙印6017 小时前
Spring容器的心脏:深度解析refresh()方法(上)
java·后端·spring