我们在对比 过滤器与拦截器 一文中,知道请求过来,各种拦截处理的顺序:
- 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 即可。