SpringBoot 统一异常处理
1.前言
在做项目的时候,常常会遇到这些情况,
(一)、繁多的if..else 造成大量的参数代码判断,于是就在实体类字段中添加@NotBlank,@NotNull等注解代替,可是注解上自定义的message 消息无法规范的返回到前端;
(二)、在业务层代码中,当方法层层嵌套,对最深处的代码进行不满足的参数做判断时,直接返回响应体并不是很合适(这个时候就需要抛出自定义异常)
(三)、通过Assert 断言去除冗余的if..else 时,发现断言抛出的异常也没有规范的返回前端;
为了解决这些情况,我们需要做一个统一异常处理,无论是注解的异常抛出,自定义异常抛出、以及断言的异常抛出都需要统一获取其中的message。这个统一异常处理即今天所要说的 @ExceptionHandler+@ControllerAdvice ,统一异常处理。
2.直接实战
原理也不解释,废话也不多说,直接记录怎么配置,怎么使用。想知道原理的,自行百度。
2.1.创建项目
通过IDEA 中的SpringInitializr 创建springboot项目,并一次创建各个层的文件夹,controller、service、entity、model、exception、handler等文件夹。如下所示:
2.2 统一异常配置
pom.xml 添加 以下依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.1.Final</version>
</dependency>
2.2.1 Result 响应实体类,用来返回前端的实体类。
/**
* @Author: DZBiao
* @Date : 2021/12/11
* @Description : 描述:
**/
public class Result {
/**
* 响应状态码
*/
private Integer code;
/**
* 响应成功与否
*/
private boolean success;
/**
* 响应消息
*/
private String msg;
/**
* 响应数据
*/
private Object data;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public Result() {
}
public Result(Integer code, boolean success, String msg) {
this.code = code;
this.success = success;
this.msg = msg;
}
public Result(Integer code, boolean success, String msg, Object data) {
this.code = code;
this.success = success;
this.msg = msg;
this.data = data;
}
/**
* 成功 返回默认成功信息
*
* @return
*/
public static Result SUCCESS() {
return new Result(1, true, "操作成功", null);
}
/**
* 成功 返回(data数据)成功信息
*
* @param data
* @return
*/
public static Result SUCCESS(Object data) {
return new Result(1, true, "操作成功", data);
}
/**
* 成功 返回自定义(消息、data数据)成功信息
*
* @param msg
* @param data
* @return
*/
public static Result SUCCESS(String msg, Object data) {
return new Result(1, true, msg, data);
}
/**
* 失败 返回默认失败信息
*
* @return
*/
public static Result ERROR() {
return new Result(-1, false, "操作失败", null);
}
/**
* 失败 返回自定义(消息)失败信息
*
* @param msg
* @return
*/
public static Result ERROR(String msg) {
return new Result(-1, false, msg, null);
}
/**
* 失败 返回自定义(消息、状态码)失败信息
*
* @param code
* @param msg
* @return
*/
public static Result ERROR(Integer code, String msg) {
return new Result(code, false, msg, null);
}
}
2.2.2 @Controller 和 @RestControllerAdvice 配置
在controller层中新建IndexController类,传参实体类使用简单的User类;
@RestController
@RequestMapping("/index")
public class IndexController {
@PostMapping("/list")
public Result index(@Valid @RequestBody User user) throws Exception {
return Result.SUCCESS();
}
}
实体类User ,当我想要在传参时进行非空判断,则添加@NotBlank注解,并在Controller层 添加@Valid和@RequestBody 注解。(@Data 注解为Lombok 插件,idea中安装,如果没有直接在实体类中生成get和set 方法代替.)
@Data
public class User {
private String id ; // ID
@NotBlank(message = "用户名不能为空.")
private String username ; // 用户名
@NotBlank(message = "性别不能为空")
private String sex ; // 性别
@NotBlank(message = "地址不能为空")
private String address ; // 地址
private String status ;
}
在handler 文件夹中新建GlobalExceptionController类,并配置@RestControllerAdvice注解。对于实体类参数上的注解的异常捕获,我们在统一异常处理类中,通过MethodArgumentNotValidException进行捕获处理。
@RestControllerAdvice
public class GlobalExceptionController {
/**
* 处理方法参数异常,如 实体类上的@NotBlank注解异常,@NotNull注解等异常信息返回。
* @param ex
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleMethodArgumentException(MethodArgumentNotValidException ex){
return Result.ERROR(ex.getBindingResult().getFieldError().getDefaultMessage()) ;
}
}
至此我们解决了三个问题中的其中一个问题。下面解决抛出自定义异常 和断言异常的捕获。
在GlobalExceptionController中再添加 处理自定义异常和断言异常的捕获方法。
@RestControllerAdvice
public class GlobalExceptionController {
/**
* 处理方法参数异常,如 实体类上的@NotBlank注解异常,@NotNull注解等异常信息返回。
* @param ex
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleMethodArgumentException(MethodArgumentNotValidException ex){
return Result.ERROR(ex.getBindingResult().getFieldError().getDefaultMessage()) ;
}
/**
* 断言、自定义异常等处理
* @param ex
* @return
*/
@ExceptionHandler(Exception.class)
public Result handleException(Exception ex){
return Result.ERROR(ex.getMessage()) ;
}
}
3.测试
我们在exception文件夹中创建自定义异常类:CustomException,并继承RuntimeException
public class CustomException extends RuntimeException {
private String message ;
public CustomException(String message){
this.message = message ;
}
@Override
public String getMessage() {
return message;
}
public CustomException setMessage(String message) {
this.message = message;
return this;
}
}
在controller中添加一些判断进行观察,看看我们配置的情况。
@RestController
@RequestMapping("/index")
public class IndexController {
@PostMapping("/list")
public Result index(@Valid @RequestBody User user) throws Exception {
// 抛出Exception异常
if (user.getUsername().equals("xxx")){
throw new Exception("用户名错误.");
}
// 自定义异常
if (user.getUsername().equals("xxxxx")){
throw new CustomException("自定义异常");
}
// 断言异常
Assert.notNull(user.getStatus(), "状态不能为空");
return Result.SUCCESS();
}
}
我们通过Postman或者ApiPost观察。当我们什么参数都不传时,就会捕获注解上的异常message,当username传“xxx”,就会捕获用户名错误.异常,同理,可以观察到其他的情况,不多赘述。
统一异常处理配置很简单。至此配置完成。
通过统一异常处理的配置,我们可以去除大量的if..else 冗余代码,全部交由注解或者Assert断言,或者自定义异常来解决。
需要注意的一点是:该@Valid 注解 和@NotBlank 注解 是在springboot3.6以下版本基础之上的。本地demo使用的版本是2.6.1,springboot3.6 需要使用 @Validated 注解 。