SpringBoot-异常处理

在SpringBoot中,无论是请求不存在的路径、@Valid校验,还是业务代码(Controller、Service、Dao)抛出异常,SpringBoot对错误的默认处理机制是:

那么,SpringBoot是如何判断一个请求到底来自浏览器还是APP的呢?其实,主要是看HTTP的一个请求头: Accept

SpringBoot默认的异常处理机制有什么不好呢?主要还是两点:

  • 样式或数据格式不统一
  • 对外暴露的信息不可控

以JSON格式为例,通常我们希望不论接口请求是否正常,都返回以下格式:

csharp;gutter:true; { "data": {} "success": true, "massage": "" }</p> <pre><code> 如果请求失败,希望把错误信息转化为指定内容(比如" **系统正在繁忙**")放在message中返回,给前端/客户端一个友好提示。 这样一来,不论请求成功还是失败,响应格式都是统一的,对外暴露的信息也可控。 自定义异常处理可以大致分为两类: * 自定义错误页面 * 自定义异常JSON ## 二、自定义错误页面 在 **resources/erro**r下存放404.html和500.html,当本次请求状态码为404或500时,SpringBoot就会读取我们自定义的html返回,否则返回默认的错误页面。 现在一般都是前后端分离,所以关于自定义错误页面就略过了。 ## 三、**自定义异常JSON** SpringBoot默认的异常JSON格式是这样的: ;gutter:true;
{
"timestamp": "2021-01-31T01:36:12.187+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/insertUser"
}

而我们希望响应格式是这样的:

csharp;gutter:true; { "data": {} "success": true, "massage": "" }</p> <pre><code> 一般有两种方式,并且通常会组合使用: * 在代码中使用工具类封装( **ApiResultTO/Result**) * 用全局异常处理兜底 为了方便模拟异常情况,下面案例中我们会直接抛出自定义异常,然后考虑如何处理它。 在此之前,我们先准备通用枚举类和自定义的业务异常: ### 1、先定义通用枚举类和自定义的业务异常 **ExceptionCodeEnum.java** ;gutter:true;
import lombok.Getter;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
* @ClassName ExceptionCodeEnum
* @Author zhangzhixi
* @Description 通用错误枚举(不同类型的错误也可以拆成不同的Enum细分)
* @Date 2022-4-7 18:19
* @Version 1.0
*/
@Getter
public enum ExceptionCodeEnum {
/**
* 通用结果
*/
ERROR(-1, "网络错误"),
SUCCESS(200, "成功"),

/**
* 用户登录
*/
NEED_LOGIN(900, "用户未登录"),

/**
* 参数校验
*/
ERROR_PARAM(10000, "参数错误"),
EMPTY_PARAM(10001, "参数为空"),
ERROR_PARAM_LENGTH(10002, "参数长度错误");

private final Integer code;
private final String desc;

ExceptionCodeEnum(Integer code, String desc) {
this.code = code;
this.desc = desc;
}

private static final Map ENUM_CACHE = new HashMap<>();

static {
for (ExceptionCodeEnum exceptionCodeEnum : ExceptionCodeEnum.values()) {
ENUM_CACHE.put(exceptionCodeEnum.code, exceptionCodeEnum);
}
}

public static String getDesc(Integer code) {
return Optional.ofNullable(ENUM_CACHE.get(code))
.map(ExceptionCodeEnum::getDesc)
.orElseThrow(() -> new IllegalArgumentException("invalid exception code!"));
}
}

BizException.java

csharp;gutter:true; import lombok.Getter;</p> <p>/*<em> * @ClassName BizException * @Author zhangzhixi * @Description 业务异常 biz是business的缩写 * @Date 2022-4-7 18:21 * @Version 1.0 </em>/ @Getter public class BizException extends RuntimeException {</p> <pre><code>private static final long serialVersionUID = -3229475403587709519L; private ExceptionCodeEnum error; /** * 构造器,有时我们需要将第三方异常转为自定义异常抛出,但又不想丢失原来的异常信息,此时可以传入cause * * @param error * @param cause */ public BizException(ExceptionCodeEnum error, Throwable cause) { super(cause); this.error = error; } /** * 构造器,只传入错误枚举 * * @param error */ public BizException(ExceptionCodeEnum error) { this.error = error; } </code></pre> <p>}</p> <pre><code> 下面演示两种处理异常的方式。 ### 2、第一种:Result手动封装 先封装一个Result,用来统一返回格式: ;gutter:true;
/**
* @ClassName Result
* @Author zhangzhixi
* @Description 一般返回实体
* @Date 2022-4-7 18:24
* @Version 1.0
*/
@Data
@NoArgsConstructor
public class Result implements Serializable {

private static final long serialVersionUID = -687690141206758604L;
private Integer code;
private String message;
private T data;

private Result(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}

private Result(Integer code, String message) {
this.code = code;
this.message = message;
this.data = null;
}

/**
* 带数据成功返回
*
* @param data
* @param
* @return
*/
public static Result success(T data) {
return new Result<>(ExceptionCodeEnum.SUCCESS.getCode(), ExceptionCodeEnum.SUCCESS.getDesc(), data);
}

/**
* 不带数据成功返回
*
* @return
*/
public static Result success() {
return success(null);
}

/**
* 通用错误返回,传入指定的错误枚举
*
* @param exceptionCodeEnum
* @return
*/
public static Result error(ExceptionCodeEnum exceptionCodeEnum) {
return new Result<>(exceptionCodeEnum.getCode(), exceptionCodeEnum.getDesc());
}

/**
* 通用错误返回,传入指定的错误枚举,但支持覆盖message
*
* @param exceptionCodeEnum
* @param msg
* @return
*/
public static Result error(ExceptionCodeEnum exceptionCodeEnum, String msg) {
return new Result<>(exceptionCodeEnum.getCode(), msg);
}

/**
* 通用错误返回,只传入message
*
* @param msg
* @param
* @return
*/
public static Result error(String msg) {
return new Result<>(ExceptionCodeEnum.ERROR.getCode(), msg);
}
}

使用自定义异常后,Controller层写法的区别

原本的Controller层插入用户的写法:

csharp;gutter:true; /*<em> * 插入用户 * * @param userPojo 用户信息 * @return 是否成功 </em>/ @PostMapping("insertUser") public boolean insertUser(@RequestBody User userPojo) { return userService.save(userPojo); }</p> <pre><code> 现在的写法: ;gutter:true;
/**
* 插入用户
*
* @param userPojo 用户信息
* @return 是否成功
*/
@PostMapping("insertUser")
public Result insertUser(@RequestBody User userPojo) {
if (userPojo == null) {
// 只传入定义好的错误
return Result.error(ExceptionCodeEnum.EMPTY_PARAM);
}
if (userPojo.getUserName().contains("死")) {
// 抛出自定义的错误信息
return Result.error(ExceptionCodeEnum.ERROR_PARAM, "用户名不能包含特殊字符");
}
if (userPojo.getUserAge() < 18) {
// 抛出自定义的错误信息
return Result.error("年龄不能小于18");
}
if (!"男".equals(userPojo.getUserSex()) && !"女".equals(userPojo.getUserSex())) {
// 抛出自定义的错误信息 可以自定义错误码
return Result.error("性别只能是男或女");
}
return Result.success(userService.save(userPojo));
}

测试:

失败案例:

POST localhost/user/insertUser

请求体JSON:

csharp;gutter:true; { "userName": "张三", "userAge": "23", "userSex": "男的" }</p> <pre><code> ![SpringBoot-异常处理](https://johngo-pic.oss-cn-beijing.aliyuncs.com/articles/20230605/2126720-20220408094110515-1884964143.png) 成功案例: ![SpringBoot-异常处理](https://johngo-pic.oss-cn-beijing.aliyuncs.com/articles/20230605/2126720-20220408094545661-832979261.png) ### 3、第二种:@RestControllerAdvice全局异常处理兜底 异常还有一种处理方式,就是利用Spring/SpringBoot提供的@RestControllerAdvice进行兜底处理, ;gutter:true;
/**
* @ClassName GlobalExceptionHandler
* @Author zhangzhixi
* @Description 全局异常处理
* @Date 2022-4-8 9:49
* @Version 1.0
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

/**
* 业务异常
*
* @param
* @return
*/
@ExceptionHandler(BizException.class)
public Result handleBizException(BizException bizException) {
log.error("业务异常:{}", bizException.getMessage(), bizException);
return Result.error(bizException.getError());
}

/**
* 运行时异常
*
* @param e
* @return
*/
@ExceptionHandler(RuntimeException.class)
public Result handleRunTimeException(RuntimeException e) {
log.error("运行时异常: {}", e.getMessage(), e);
return Result.error(ExceptionCodeEnum.ERROR);
}

}

测试

SpringBoot-异常处理

总结

一般来说,全局异常处理只是一种兜底的异常处理策略,也就是说提倡自己处理异常。但现在其实很多人都喜欢直接在代码中抛异常,全部交给@RestControllerAdvice处理:

这个异常抛到@RestControllerAdvice后,其实还是被封装成Result返回了。

所以Result和@ResultControllerAdvice两种方式归根结底是一样的:

SpringBoot-异常处理

Original: https://www.cnblogs.com/zhangzhixi/p/16114857.html
Author: Java小白的搬砖路
Title: SpringBoot-异常处理

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/612331/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球