在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);
}
}
测试
总结
一般来说,全局异常处理只是一种兜底的异常处理策略,也就是说提倡自己处理异常。但现在其实很多人都喜欢直接在代码中抛异常,全部交给@RestControllerAdvice处理:
这个异常抛到@RestControllerAdvice后,其实还是被封装成Result返回了。
所以Result和@ResultControllerAdvice两种方式归根结底是一样的:
Original: https://www.cnblogs.com/zhangzhixi/p/16114857.html
Author: Java小白的搬砖路
Title: SpringBoot-异常处理
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/612331/
转载文章受原作者版权保护。转载请注明原作者出处!