告別混亂報錯!SpringBoot 全局異常處理終極指南!
兄弟們!今天咱們來聊個扎心又實用的話題 —— 異常處理。我敢打賭,在座的各位肯定都經歷過這種場景:寫了一上午的接口,自測的時候好好的,一到聯調就各種報錯;更慘的是線上突然蹦出個 NullPointerException,返回給前端一串密密麻麻的堆棧信息,前端同事拿著截圖來找你,眼神里充滿了 “你這代碼咋回事” 的疑惑,你自己看著那堆日志也頭大 —— 這錯哪兒拋出來的?咋沒處理呢?
以前咱們處理異常,要么是在每個方法里裹一層 try-catch,代碼搞得跟千層餅似的;要么就是放任不管,出了錯再救火。但 SpringBoot 都出來這么多年了,要是還這么玩,那就太不 “優雅” 了。今天這篇文章,我就帶大家從頭到尾搞懂 SpringBoot 全局異常處理,以后不管是業務異常、系統異常還是參數校驗異常,都能讓它乖乖聽話,返回咱們想要的格式,徹底告別 “混亂報錯” 的噩夢!
一、先吐個槽:傳統異常處理有多坑?
在講全局異常處理之前,咱們先好好吐槽下以前的 “野路子” 處理方式,幫大家回憶下那些年踩過的坑。
1. 到處都是 try-catch,代碼丑到沒朋友
最常見的操作就是,每個 Controller 方法里都寫 try-catch:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
try {
// 業務邏輯
if (id == null || id <= 0) {
throw new IllegalArgumentException("用戶ID不能為負數!");
}
User user = userService.getUserById(id);
if (user == null) {
throw new RuntimeException("用戶不存在!");
}
return user;
} catch (IllegalArgumentException e) {
// 處理參數異常
e.printStackTrace();
throw new RuntimeException("參數錯了:" + e.getMessage());
} catch (RuntimeException e) {
// 處理業務異常
e.printStackTrace();
throw new RuntimeException("查不到用戶:" + e.getMessage());
} catch (Exception e) {
// 處理其他異常
e.printStackTrace();
throw new RuntimeException("系統出錯了!");
}
}
// 其他接口...每個都要寫一遍try-catch
}
你品,你細品:一個接口還好,要是有十個、二十個接口,每個都這么寫,代碼量直接翻倍不說,還全是重復邏輯。后期要是想改個異常返回格式,比如統一加個錯誤碼,那得一個個接口去改 —— 這不純純的體力活嗎?而且萬一哪個接口忘了加 try-catch,線上就可能直接返回 500 錯誤,堆棧信息裸奔給用戶看,體驗直接拉滿(負面的)。
2. 異常信息混亂,排查問題像拆盲盒
以前沒統一處理的時候,異常信息那叫一個五花八門:有的拋 “參數錯了”,有的拋 “id invalid”,有的干脆不拋信息,就一個 “Exception occurred”。等到線上出問題,查日志的時候,光靠這些模糊的信息,你都不知道是哪個業務模塊出的錯,得順著調用鏈一點點找,跟拆盲盒似的,運氣好能快點找到,運氣差能查一下午。
3. 前端哭暈在廁所:這返回格式我咋處理?
最慘的還是前端同事:正常情況下返回{data:..., success:true},異常的時候有時候返回{message:"xxx"},有時候直接返回一串堆棧字符串,有時候甚至返回個 500 頁面的 HTML。前端為了兼容這些情況,得寫一堆判斷邏輯:“如果是字符串就提示錯誤,如果是對象就看 success 字段,如果是 HTML 就跳轉錯誤頁”—— 長此以往,前端同事看你的眼神都會變得不一樣。
所以啊,傳統的異常處理方式,不管是對后端開發效率、問題排查,還是前后端協作,都是 “坑”。而 SpringBoot 的全局異常處理,就是來填這些坑的 “神器”。
二、全局異常處理入門:倆注解搞定核心功能
其實 SpringBoot 的全局異常處理一點都不難,核心就倆注解:@ControllerAdvice和@ExceptionHandler。咱們先從最基礎的用法開始講,保證你看完就能上手。
1. 先搞懂這倆注解是干啥的
- @ControllerAdvice:可以理解為 “全局 Controller 增強器”,它能監控所有帶@Controller(包括@RestController)注解的類。簡單說,它就像一個 “異常管家”,所有 Controller 拋出來的異常,都會先經過它的 “篩選”。
- @ExceptionHandler:這個注解要配合@ControllerAdvice用,作用是 “指定處理哪種異?!?。比如你加了@ExceptionHandler(NullPointerException.class),那所有 Controller 拋出來的空指針異常,都會被這個方法處理。
這倆注解一組合,就能實現 “不管哪個 Controller 拋了指定的異常,都由同一個方法來統一處理”—— 這不就解決了 try-catch 重復的問題嗎?
2. 第一個全局異常處理器:先跑起來再說
咱們先寫一個最基礎的全局異常處理器,感受一下它的威力。
第一步:定義統一響應體(前后端都開心)
在處理異常之前,咱們先定一個統一的響應格式,不管是正常返回還是異常返回,都用這個格式。這樣前端不用再猜格式,后端也不用亂改返回值,雙贏!
/**
* 統一響應結果類
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
// 狀態碼:200成功,其他都是錯誤碼
private Integer code;
// 提示信息
private String message;
// 返回數據(成功時才有)
private T data;
// 成功的靜態方法(帶數據)
public static <T> Result<T> success(T data) {
return new Result<>(200, "操作成功", data);
}
// 成功的靜態方法(不帶數據)
public static <T> Result<T> success() {
return new Result<>(200, "操作成功", null);
}
// 失敗的靜態方法
public static <T> Result<T> fail(Integer code, String message) {
return new Result<>(code, message, null);
}
}
這里用了 Lombok 的@Data、@NoArgsConstructor、@AllArgsConstructor,能少寫很多 getter/setter 和構造方法,要是你沒開 Lombok,自己寫也行,不影響核心功能。
第二步:寫全局異常處理器
接下來就是核心了,創建一個類,加上@RestControllerAdvice(注意是@RestControllerAdvice,不是@ControllerAdvice,因為我們要返回 JSON,這個注解自帶@ResponseBody),然后寫異常處理方法:
/**
* 全局異常處理器
* 用@RestControllerAdvice代替@ControllerAdvice,直接返回JSON
*/
@RestControllerAdvice
@Slf4j // 日志注解,方便打印異常日志
public class GlobalExceptionHandler {
/**
* 處理空指針異常
* @ExceptionHandler(NullPointerException.class) 表示這個方法處理NullPointerException
*/
@ExceptionHandler(NullPointerException.class)
public Result<Void> handleNullPointerException(NullPointerException e) {
// 打印異常日志(方便排查問題)
log.error("發生空指針異常:", e);
// 返回統一的失敗結果
return Result.fail(500, "系統出了點小問題:空指針異常,請聯系開發小哥");
}
/**
* 處理參數非法異常(比如傳了負數ID)
*/
@ExceptionHandler(IllegalArgumentException.class)
public Result<Void> handleIllegalArgumentException(IllegalArgumentException e) {
log.error("發生參數非法異常:", e);
// 這里可以把異常的具體信息返回給前端(因為參數異常通常是用戶操作導致的,需要明確提示)
return Result.fail(400, "參數錯啦:" + e.getMessage());
}
/**
* 處理運行時異常(比如業務邏輯錯誤)
*/
@ExceptionHandler(RuntimeException.class)
public Result<Void> handleRuntimeException(RuntimeException e) {
log.error("發生運行時異常:", e);
return Result.fail(500, "業務處理出錯:" + e.getMessage());
}
/**
* 處理所有其他異常(兜底處理,防止有漏網之魚)
* 這里用Exception.class,表示處理所有Exception類型的異常
*/
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("發生未知異常:", e);
// 未知異常不把具體信息返回給前端,避免泄露系統細節
return Result.fail(500, "系統繁忙,請稍后再試~");
}
}
第三步:改造 Controller,去掉多余的 try-catch
現在咱們的 Controller 就可以不用寫 try-catch 了,直接拋異常就行,全局處理器會自動接?。?/p>
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public Result<User> getUserById(@PathVariable Long id) {
// 不用try-catch,直接拋異常
if (id == null || id <= 0) {
// 拋參數非法異常,會被handleIllegalArgumentException處理
throw new IllegalArgumentException("用戶ID不能為負數或空!");
}
User user = userService.getUserById(id);
if (user == null) {
// 拋運行時異常,會被handleRuntimeException處理
throw new RuntimeException("根據ID:" + id + " 沒找到用戶");
}
// 正常返回統一格式
return Result.success(user);
}
@PostMapping("/add")
public Result<Void> addUser(@RequestBody User user) {
// 故意造個空指針異常,測試全局處理器
String username = user.getUsername();
if (username.equals("admin")) { // 如果user是null,這里就會拋空指針
throw new RuntimeException("不能添加admin用戶");
}
userService.addUser(user);
return Result.success();
}
}
第四步:測試一下,看看效果
咱們用 Postman 或者 Swagger 測幾個場景,看看返回結果是不是統一的:
- 測試參數非法:訪問/user/-1
返回結果:
{
"code": 400,
"message": "參數錯啦:用戶ID不能為負數或空!",
"data": null
}
完美!參數異常被正確捕獲,返回了 400 狀態碼和具體提示。
- 測試用戶不存在:訪問/user/999(假設 999 的用戶不存在)
返回結果:
{
"code": 500,
"message": "業務處理出錯:根據ID:999 沒找到用戶",
"data": null
}
也對,運行時異常被捕獲,返回了業務錯誤信息。
- 測試空指針:訪問/user/add,傳一個 null 的 user
返回結果:
{
"code": 500,
"message": "系統出了點小問題:空指針異常,請聯系開發小哥",
"data": null
}
空指針異常也被接住了,而且日志里會打印完整的堆棧信息,方便我們排查問題。
- 測試未知異常:比如 Service 里拋了個ClassCastException(咱們沒專門處理這個異常)
返回結果:
{
"code": 500,
"message": "系統繁忙,請稍后再試~",
"data": null
}
兜底的Exception處理方法生效了,不會把陌生的異常信息暴露給用戶。你看,就這么簡單,倆注解加幾個方法,就解決了傳統 try-catch 的所有痛點:代碼簡潔了,返回格式統一了,異常日志也有了,前端同事再也不用跟你吐槽格式問題了!
三、進階技巧:讓全局異常處理更實用
基礎用法學會了,但實際開發中場景更復雜,比如我們會自定義業務異常、需要處理參數校驗異常、還要區分開發和生產環境的返回信息。這部分咱們就來升級一下全局異常處理器,讓它更貼合實際項目。
1. 自定義業務異常:告別 “混亂的異常信息”
實際項目里,我們會遇到很多 “業務相關的異常”,比如 “用戶余額不足”“訂單已取消”“token 過期” 等等。如果都用RuntimeException,不僅不好區分,而且沒辦法攜帶更多信息(比如錯誤碼)。這時候,自定義業務異常就派上用場了。
第一步:定義自定義業務異常類
/**
* 自定義業務異常
* 繼承RuntimeException(因為Spring只默認捕獲RuntimeException,不用強制try-catch)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
publicclass BusinessException extends RuntimeException {
// 錯誤碼(比如1001:用戶余額不足,1002:訂單已取消)
private Integer errorCode;
// 錯誤信息
private String errorMessage;
// 重載構造方法,方便使用(只傳錯誤碼和信息)
public BusinessException(Integer errorCode, String errorMessage) {
this.errorCode = errorCode;
this.errorMessage = errorMessage;
// 把errorMessage傳給父類,這樣打印日志的時候能看到
super(errorMessage);
}
// 再重載一個:只傳錯誤枚舉(后面會講錯誤枚舉的用法)
public BusinessException(ErrorEnum errorEnum) {
this.errorCode = errorEnum.getCode();
this.errorMessage = errorEnum.getMessage();
super(errorMessage);
}
}
這里我加了errorCode字段,因為實際項目中,前端可能需要根據不同的錯誤碼做不同的處理(比如錯誤碼 1003 是 token 過期,前端要跳登錄頁)。如果只用 message,前端很難判斷具體場景。
第二步:定義錯誤枚舉(規范錯誤碼)
為了避免錯誤碼混亂(比如張三用 1001 表示余額不足,李四用 1001 表示訂單已取消),我們可以用枚舉來管理錯誤碼和錯誤信息:
/**
* 錯誤枚舉:統一管理錯誤碼和錯誤信息
*/
publicenum ErrorEnum {
// 系統相關錯誤
SYSTEM_ERROR(500, "系統繁忙,請稍后再試"),
NULL_POINTER_ERROR(5001, "空指針異常"),
// 用戶相關錯誤
USER_NOT_FOUND(1001, "用戶不存在"),
USER_BALANCE_NOT_ENOUGH(1002, "用戶余額不足"),
USER_TOKEN_EXPIRED(1003, "用戶token已過期,請重新登錄"),
// 參數相關錯誤
PARAM_ERROR(400, "參數非法"),
PARAM_NULL(4001, "必填參數不能為空");
// 錯誤碼
private Integer code;
// 錯誤信息
privateString message;
ErrorEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
// getter方法
public Integer getCode() {
return code;
}
publicString getMessage() {
return message;
}
}
這樣一來,所有的錯誤碼和信息都集中管理,要加新的錯誤場景,直接在枚舉里加就行,不用到處改代碼。
第三步:在全局處理器中處理自定義異常
現在,我們在GlobalExceptionHandler里加一個處理BusinessException的方法:
/**
* 處理自定義業務異常(優先級比RuntimeException高,因為更具體)
*/
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
log.error("發生業務異常:錯誤碼={},錯誤信息={}", e.getErrorCode(), e.getErrorMessage(), e);
// 直接返回自定義的錯誤碼和信息
return Result.fail(e.getErrorCode(), e.getErrorMessage());
}
注意:異常處理方法的優先級是 “具體的異常優先于籠統的異?!薄1热鏐usinessException是RuntimeException的子類,所以handleBusinessException會比handleRuntimeException先生效,這正是我們想要的。
第四步:在業務代碼中使用
現在,Service 層就可以拋自定義異常了:
@Service
publicclass UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private BalanceMapper balanceMapper;
// 查詢用戶
public User getUserById(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
// 拋自定義業務異常,用錯誤枚舉
thrownew BusinessException(ErrorEnum.USER_NOT_FOUND);
}
return user;
}
// 扣減余額
public void deductBalance(Long userId, BigDecimal amount) {
// 查詢余額
Balance balance = balanceMapper.selectByUserId(userId);
if (balance == null) {
thrownew BusinessException(ErrorEnum.USER_NOT_FOUND);
}
// 判斷余額是否足夠
if (balance.getAmount().compareTo(amount) < 0) {
// 拋余額不足的異常
thrownew BusinessException(ErrorEnum.USER_BALANCE_NOT_ENOUGH);
}
// 扣減余額(實際項目中要加事務)
balance.setAmount(balance.getAmount().subtract(amount));
balanceMapper.updateById(balance);
}
}
Controller 層還是一樣,不用 try-catch,直接調用 Service:
@PostMapping("/deduct")
public Result<Void> deductBalance(@RequestParam Long userId, @RequestParam BigDecimal amount) {
// 不用處理異常,直接調用
userService.deductBalance(userId, amount);
return Result.success("余額扣減成功");
}
測試一下:當用戶余額不足時,訪問/user/deduct?userId=1&amount=1000(假設用戶 1 只有 500 余額),返回結果:
{
"code": 1002,
"message": "用戶余額不足",
"data": null
}
完美!錯誤碼和信息都很規范,前端拿到 1002 就知道是余額不足,可以直接給用戶提示 “您的余額不足,請充值”。
2. 處理參數校驗異常:不用自己寫 if 判斷了
以前我們判斷參數是否合法,都是用 if-else(比如if (id == null) throw ...),但參數多了之后,代碼會很啰嗦。SpringBoot 提供了spring-boot-starter-validation來幫我們自動校驗參數,配合全局異常處理,能省不少事。
第一步:引入依賴
如果是 SpringBoot 2.3 + 版本,需要手動引入 validation 依賴(之前的版本自帶):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
第二步:在實體類或參數上加校驗注解
比如我們的User類:
@Data
public class User {
// 主鍵:非空
@NotNull(message = "用戶ID不能為空")
private Long id;
// 用戶名:非空,且長度在2-20之間
@NotBlank(message = "用戶名不能為空")
@Size(min = 2, max = 20, message = "用戶名長度必須在2-20之間")
private String username;
// 年齡:必須大于0,小于150
@Min(value = 1, message = "年齡不能小于1歲")
@Max(value = 150, message = "年齡不能大于150歲")
private Integer age;
// 郵箱:必須符合郵箱格式
@Email(message = "郵箱格式不正確")
private String email;
}
常用的校驗注解有:
- @NotNull:不能為 null(適用于所有類型)
- @NotBlank:不能為 null 且不能為空白字符串(適用于 String)
- @NotEmpty:不能為 null 且集合 / 數組長度不能為 0(適用于集合、數組)
- @Min/@Max:數值的最小值 / 最大值(適用于數字類型)
- @Size:集合 / 數組 / 字符串的長度范圍
- @Email:符合郵箱格式
- @Pattern:符合正則表達式(比如手機號:@Pattern(regexp = "^1[3-9]\d{9}$", message = "手機號格式不正確"))
然后在 Controller 方法上加上@Valid或@Validated注解,開啟參數校驗:
@PostMapping("/add")
public Result<User> addUser(@Valid@RequestBody User user) {
// 不用寫if判斷參數了,校驗不通過會自動拋異常
userService.addUser(user);
return Result.success(user);
}
// 路徑參數校驗(需要在Controller類上加@Validated)
@GetMapping("/info")
public Result<User> getUserInfo(
@NotNull(message = "用戶ID不能為空") @RequestParam Long id,
@NotBlank(message = "token不能為空") @RequestParamString token
) {
User user = userService.getUserById(id);
return Result.success(user);
}
// 注意:路徑參數或請求參數校驗,需要在Controller類上加@Validated注解,否則不生效
@RestController
@RequestMapping("/user")
@Validated// 加上這個,才能校驗@RequestParam和@PathVariable的參數
publicclass UserController {
// ... 方法省略
}
第三步:在全局處理器中處理校驗異常
參數校驗不通過時,Spring 會拋兩種異常:
- MethodArgumentNotValidException:請求體參數校驗失?。ū热鏎RequestBody User user校驗不通過)
- ConstraintViolationException:路徑參數或請求參數校驗失?。ū热鏎RequestParam Long id校驗不通過)
我們需要在全局處理器中分別處理這兩種異常:
/**
* 處理請求體參數校驗異常(@RequestBody + @Valid)
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
// 獲取所有的校驗失敗信息
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
// 把錯誤信息拼接成字符串(也可以只返回第一個錯誤)
String errorMsg = allErrors.stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(";"));
log.error("請求體參數校驗失?。?, e);
// 返回參數錯誤碼和信息
return Result.fail(ErrorEnum.PARAM_ERROR.getCode(), errorMsg);
}
/**
* 處理路徑/請求參數校驗異常(@RequestParam/@PathVariable + @Validated)
*/
@ExceptionHandler(ConstraintViolationException.class)
public Result<Void> handleConstraintViolationException(ConstraintViolationException e) {
// 獲取所有的校驗失敗信息
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
// 拼接錯誤信息
String errorMsg = violations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(";"));
log.error("路徑/請求參數校驗失?。?, e);
return Result.fail(ErrorEnum.PARAM_ERROR.getCode(), errorMsg);
}
第四步:測試參數校驗
- 測試請求體校驗:訪問/user/add,傳一個不符合要求的 user:
{
"id": null,
"username": "a",
"age": 200,
"email": "123"
}
返回結果:
{
"code": 400,
"message": "用戶ID不能為空;用戶名長度必須在2-20之間;年齡不能大于150歲;郵箱格式不正確",
"data": null
}
所有的校驗錯誤都被收集起來了,清晰明了。
- 測試請求參數校驗:訪問/user/info?id=null&token=(實際請求中 id=null 會被解析成 null,token 為空字符串)
返回結果:
{
"code": 400,
"message": "用戶ID不能為空;token不能為空",
"data": null
}
也完美生效!以后再也不用寫一堆 if 判斷參數了,省時又省力。
3. 區分開發 / 生產環境:避免泄露敏感信息
開發環境下,我們希望能看到完整的異常堆棧信息,方便排查問題;但生產環境下,絕對不能把堆棧信息返回給用戶,否則會泄露系統架構、代碼路徑等敏感信息。
怎么實現這種 “環境區分” 呢?SpringBoot 的@Profile注解可以幫我們搞定。
第一步:配置環境
首先,我們需要在application.yml(或application.properties)中配置當前環境,比如:
# application.yml
spring:
profiles:
active: dev # 開發環境:dev;生產環境:prod
第二步:寫兩個環境的異常處理方法
我們可以在全局處理器中,寫兩個處理 “未知異?!?的方法,分別對應 dev 和 prod 環境:
/**
* 開發環境:處理未知異常,返回完整堆棧信息(方便排查問題)
* @Profile("dev") 表示這個方法只在dev環境生效
*/
@ExceptionHandler(Exception.class)
@Profile("dev")
public Result<Void> handleExceptionDev(Exception e) {
log.error("發生未知異常:", e);
// 拼接完整的堆棧信息
String stackTrace = getStackTraceAsString(e);
// 返回錯誤碼和堆棧信息(只在開發環境返回)
return Result.fail(ErrorEnum.SYSTEM_ERROR.getCode(), "開發環境-未知異常:" + stackTrace);
}
/**
* 生產環境:處理未知異常,只返回模糊提示(避免泄露敏感信息)
* @Profile("prod") 表示這個方法只在prod環境生效
*/
@ExceptionHandler(Exception.class)
@Profile("prod")
public Result<Void> handleExceptionProd(Exception e) {
log.error("發生未知異常:", e); // 日志還是要打印完整堆棧,方便后臺排查
// 只返回模糊提示
return Result.fail(ErrorEnum.SYSTEM_ERROR.getCode(), ErrorEnum.SYSTEM_ERROR.getMessage());
}
/**
* 工具方法:把異常堆棧轉換成字符串
*/
private String getStackTraceAsString(Throwable e) {
if (e == null) {
return"";
}
// 用StringWriter和PrintWriter來獲取堆棧信息
try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
e.printStackTrace(pw);
return sw.toString();
} catch (IOException ex) {
log.error("獲取異常堆棧信息失?。?, ex);
return"獲取堆棧信息失敗";
}
}
這樣一來:
- 當spring.profiles.active=dev時,未知異常會返回完整的堆棧信息,比如:
{
"code": 500,
"message": "開發環境-未知異常:java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer\n\tat com.example.demo.service.UserService.testException(UserService.java:50)\n\tat ...",
"data": null
}
我們能直接看到異常發生的類、方法和行號,排查問題很快。
- 當spring.profiles.active=prod時,未知異常只返回:
{
"code": 500,
"message": "系統繁忙,請稍后再試",
"data": null
}
既保護了系統安全,又給用戶友好的提示。
4. 處理其他場景的異常:一個都不能漏
除了 Controller 層拋的異常,實際項目中還有一些其他場景會拋異常,比如攔截器、過濾器、異步方法等,這些異常默認不會被@ControllerAdvice捕獲,咱們也得處理一下。
(1)處理攔截器中的異常
攔截器是在 Controller 之前執行的,如果攔截器里拋了異常,@ControllerAdvice是捕獲不到的。比如我們有一個登錄攔截器,判斷 token 是否有效:
/**
* 登錄攔截器
*/
@Component
publicclass LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 獲取token
String token = request.getHeader("token");
// 判斷token是否為空
if (StringUtils.isEmpty(token)) {
// 這里拋異常,默認不會被全局處理器捕獲
thrownew BusinessException(ErrorEnum.USER_TOKEN_EXPIRED);
}
// 驗證token(省略邏輯)
returntrue;
}
}
這時候如果 token 為空,攔截器拋了BusinessException,但全局處理器接不到,會返回默認的 500 錯誤頁面(或 JSON),不是我們想要的統一格式。解決辦法:寫一個HandlerExceptionResolver,專門處理攔截器中的異常:
/**
* 處理攔截器中的異常(實現HandlerExceptionResolver接口)
*/
@Component
publicclass InterceptorExceptionResolver implements HandlerExceptionResolver {
@Autowired
private ObjectMapper objectMapper; // Spring默認的JSON解析器
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 設置響應格式為JSON
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
Result<Void> result;
try {
// 判斷異常類型,和全局處理器邏輯一致
if (ex instanceof BusinessException) {
BusinessException businessException = (BusinessException) ex;
result = Result.fail(businessException.getErrorCode(), businessException.getErrorMessage());
} elseif (ex instanceof IllegalArgumentException) {
result = Result.fail(ErrorEnum.PARAM_ERROR.getCode(), ex.getMessage());
} else {
result = Result.fail(ErrorEnum.SYSTEM_ERROR.getCode(), "系統繁忙,請稍后再試");
}
// 把Result對象轉換成JSON字符串,寫入響應
String json = objectMapper.writeValueAsString(result);
response.getWriter().write(json);
} catch (Exception e) {
log.error("處理攔截器異常失?。?, e);
}
// 返回null,表示不需要跳轉頁面(因為我們已經返回JSON了)
returnnew ModelAndView();
}
}
這樣一來,攔截器里拋的異常就會被這個InterceptorExceptionResolver捕獲,返回統一的 JSON 格式了。
(2)處理異步方法中的異常
Spring 的異步方法(加了@Async注解)拋的異常,默認也不會被@ControllerAdvice捕獲。比如:
@Service
public class AsyncService {
// 異步方法
@Async
public void asyncTask() {
// 這里拋異常,全局處理器捕獲不到
int i = 1 / 0;
}
}
解決辦法:配置AsyncUncaughtExceptionHandler,處理異步方法的異常:
/**
* 配置異步方法的異常處理器
*/
@Configuration
@EnableAsync// 開啟異步支持
publicclass AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
// 配置線程池(可選,不配置用默認的)
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
// 自定義異步異常處理器
return (ex, method, params) -> {
log.error("異步方法[{}]執行失敗,參數:{}", method.getName(), Arrays.toString(params), ex);
// 這里可以加一些額外的處理,比如發送告警郵件、短信等
};
}
}
這樣,異步方法拋的異常就會被getAsyncUncaughtExceptionHandler捕獲,雖然不會返回給前端(因為異步方法通常是后臺任務),但會打印詳細的日志,方便我們排查問題。
(3)處理 404/405 異常
有時候用戶訪問了不存在的接口(404),或者用錯了請求方法(比如用 GET 訪問 POST 接口,405),默認會返回 HTML 頁面,不是我們想要的 JSON 格式。
解決辦法:在全局處理器中處理NoHandlerFoundException(404)和HttpRequestMethodNotSupportedException(405):
/**
* 處理404異常(接口不存在)
* 注意:需要在application.yml中配置spring.mvc.throw-exception-if-no-handler-found=true
*/
@ExceptionHandler(NoHandlerFoundException.class)
public Result<Void> handleNoHandlerFoundException(NoHandlerFoundException e) {
log.error("訪問的接口不存在:{} {}", e.getHttpMethod(), e.getRequestURL(), e);
return Result.fail(404, "您訪問的接口不存在,請檢查URL是否正確");
}
/**
* 處理405異常(請求方法不支持)
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Result<Void> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
// 獲取支持的請求方法
String supportedMethods = Arrays.toString(e.getSupportedMethods());
log.error("請求方法不支持:當前方法={},支持的方法={}", e.getMethod(), supportedMethods, e);
return Result.fail(405, "請求方法不支持,支持的方法:" + supportedMethods);
}
注意:處理 404 異常需要在application.yml中加一句配置,否則 Spring 不會拋NoHandlerFoundException:
spring:
mvc:
throw-exception-if-no-handler-found: true # 找不到接口時拋異常
web:
resources:
add-mappings: false # 關閉默認的靜態資源映射(否則訪問不存在的靜態資源也會404,但不會拋異常)
測試一下:訪問一個不存在的接口/user/abc,返回結果:
{
"code": 404,
"message": "您訪問的接口不存在,請檢查URL是否正確",
"data": null
}
完美!404 和 405 異常也被統一處理了。
四、實戰:搭建完整的全局異常處理體系
學到這里,你已經掌握了全局異常處理的大部分知識點了?,F在咱們把這些內容整合起來,搭建一個完整的、可以直接在項目中使用的全局異常處理體系。
1. 項目結構
先看一下相關類的結構,方便你在自己的項目中對應:
com.example.demo
├── exception # 異常相關包
│ ├── BusinessException.java # 自定義業務異常
│ ├── ErrorEnum.java # 錯誤枚舉
│ └── GlobalExceptionHandler.java # 全局異常處理器
├── config # 配置類包
│ ├── AsyncConfig.java # 異步配置(處理異步異常)
│ └── InterceptorExceptionResolver.java # 攔截器異常處理器
├── common # 公共類包
│ └── Result.java # 統一響應體
├── controller # 控制器包
│ └── UserController.java # 用戶控制器
├── service # 服務層包
│ └── UserService.java # 用戶服務
└── application.yml # 配置文件
2. 完整代碼匯總
這里把核心類的完整代碼匯總一下,你可以直接復制到項目中使用(記得根據自己的項目調整包名和業務邏輯)。
(1)統一響應體:Result.java
package com.example.demo.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
publicclass Result<T> {
private Integer code;
private String message;
private T data;
publicstatic <T> Result<T> success(T data) {
returnnew Result<>(200, "操作成功", data);
}
publicstatic <T> Result<T> success() {
returnnew Result<>(200, "操作成功", null);
}
publicstatic <T> Result<T> success(String message, T data) {
returnnew Result<>(200, message, data);
}
publicstatic <T> Result<T> fail(Integer code, String message) {
returnnew Result<>(code, message, null);
}
publicstatic <T> Result<T> fail(String message) {
returnnew Result<>(500, message, null);
}
}
(2)錯誤枚舉:ErrorEnum.java
package com.example.demo.exception;
publicenum ErrorEnum {
// 系統通用錯誤
SYSTEM_ERROR(500, "系統繁忙,請稍后再試"),
NULL_POINTER_ERROR(5001, "空指針異常"),
UNKNOWN_ERROR(5002, "未知異常"),
// HTTP錯誤
HTTP_404_ERROR(404, "您訪問的接口不存在,請檢查URL是否正確"),
HTTP_405_ERROR(405, "請求方法不支持"),
// 參數錯誤
PARAM_ERROR(400, "參數非法"),
PARAM_NULL(4001, "必填參數不能為空"),
PARAM_FORMAT_ERROR(4002, "參數格式不正確"),
// 用戶相關錯誤
USER_NOT_FOUND(1001, "用戶不存在"),
USER_BALANCE_NOT_ENOUGH(1002, "用戶余額不足"),
USER_TOKEN_EXPIRED(1003, "用戶token已過期,請重新登錄"),
USER_LOGIN_FAILED(1004, "用戶名或密碼錯誤"),
// 訂單相關錯誤
ORDER_NOT_FOUND(2001, "訂單不存在"),
ORDER_STATUS_ERROR(2002, "訂單狀態不正確,無法操作"),
ORDER_PAY_TIMEOUT(2003, "訂單支付超時,請重新下單");
privatefinal Integer code;
privatefinal String message;
ErrorEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
(3)自定義業務異常:BusinessException.java
package com.example.demo.exception;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
publicclass BusinessException extends RuntimeException {
private Integer errorCode;
private String errorMessage;
public BusinessException(Integer errorCode, String errorMessage) {
super(errorMessage);
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
public BusinessException(ErrorEnum errorEnum) {
super(errorEnum.getMessage());
this.errorCode = errorEnum.getCode();
this.errorMessage = errorEnum.getMessage();
}
public BusinessException(ErrorEnum errorEnum, String extraMessage) {
super(errorEnum.getMessage() + ":" + extraMessage);
this.errorCode = errorEnum.getCode();
this.errorMessage = errorEnum.getMessage() + ":" + extraMessage;
}
}
(4)全局異常處理器:GlobalExceptionHandler.java
package com.example.demo.exception;
import com.example.demo.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@RestControllerAdvice
@Slf4j
publicclass GlobalExceptionHandler {
/**
* 處理自定義業務異常
*/
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
log.error("業務異常 - URL: {}, 錯誤碼: {}, 錯誤信息: {}",
request.getRequestURI(), e.getErrorCode(), e.getErrorMessage(), e);
return Result.fail(e.getErrorCode(), e.getErrorMessage());
}
/**
* 處理請求體參數校驗異常(@RequestBody + @Valid)
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
BindingResult bindingResult = e.getBindingResult();
List<ObjectError> allErrors = bindingResult.getAllErrors();
String errorMsg = allErrors.stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(";"));
log.error("請求體參數校驗失敗 - URL: {}, 錯誤信息: {}", request.getRequestURI(), errorMsg, e);
return Result.fail(ErrorEnum.PARAM_ERROR.getCode(), errorMsg);
}
/**
* 處理路徑/請求參數校驗異常(@RequestParam/@PathVariable + @Validated)
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
String errorMsg = violations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(";"));
log.error("路徑/請求參數校驗失敗 - URL: {}, 錯誤信息: {}", request.getRequestURI(), errorMsg, e);
return Result.fail(ErrorEnum.PARAM_ERROR.getCode(), errorMsg);
}
/**
* 處理參數類型不匹配異常(比如傳String給Integer參數)
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
String paramName = e.getName();
String requiredType = e.getRequiredType().getSimpleName();
String errorMsg = String.format("參數[%s]類型不匹配,需要%s類型", paramName, requiredType);
log.error("參數類型不匹配 - URL: {}, 錯誤信息: {}", request.getRequestURI(), errorMsg, e);
return Result.fail(ErrorEnum.PARAM_FORMAT_ERROR.getCode(), errorMsg);
}
/**
* 處理404異常
*/
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Result<Void> handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
String errorMsg = String.format("您訪問的接口[%s %s]不存在,請檢查URL是否正確", e.getHttpMethod(), e.getRequestURL());
log.error("404異常 - 錯誤信息: {}", errorMsg, e);
return Result.fail(ErrorEnum.HTTP_404_ERROR.getCode(), errorMsg);
}
/**
* 處理405異常
*/
@ExceptionHandler(org.springframework.web.HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public Result<Void> handleHttpRequestMethodNotSupportedException(org.springframework.web.HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
String supportedMethods = Arrays.toString(e.getSupportedMethods());
String errorMsg = String.format("請求方法不支持,當前方法: %s,支持的方法: %s", e.getMethod(), supportedMethods);
log.error("405異常 - URL: {}, 錯誤信息: {}", request.getRequestURI(), errorMsg, e);
return Result.fail(ErrorEnum.HTTP_405_ERROR.getCode(), errorMsg);
}
/**
* 開發環境 - 處理未知異常(返回完整堆棧)
*/
@ExceptionHandler(Exception.class)
@Profile("dev")
public Result<Void> handleExceptionDev(Exception e, HttpServletRequest request) {
String stackTrace = getStackTraceAsString(e);
log.error("未知異常 - URL: {}, 錯誤信息: {}", request.getRequestURI(), e.getMessage(), e);
String errorMsg = String.format("開發環境-未知異常:%s\n堆棧信息:%s", e.getMessage(), stackTrace);
return Result.fail(ErrorEnum.UNKNOWN_ERROR.getCode(), errorMsg);
}
/**
* 生產環境 - 處理未知異常(只返回模糊提示)
*/
@ExceptionHandler(Exception.class)
@Profile("prod")
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Void> handleExceptionProd(Exception e, HttpServletRequest request) {
log.error("未知異常 - URL: {}, 錯誤信息: {}", request.getRequestURI(), e.getMessage(), e);
// 這里可以加告警邏輯,比如發送郵件給開發人員
return Result.fail(ErrorEnum.SYSTEM_ERROR.getCode(), ErrorEnum.SYSTEM_ERROR.getMessage());
}
/**
* 工具方法:獲取異常堆棧字符串
*/
privateString getStackTraceAsString(Throwable e) {
if (e == null) {
return"";
}
try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
e.printStackTrace(pw);
return sw.toString();
} catch (IOException ex) {
log.error("獲取異常堆棧信息失?。?, ex);
return"獲取堆棧信息失敗";
}
}
}
(5)攔截器異常處理器:InterceptorExceptionResolver.java
package com.example.demo.config;
import com.example.demo.common.Result;
import com.example.demo.exception.BusinessException;
import com.example.demo.exception.ErrorEnum;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
@Slf4j
publicclass InterceptorExceptionResolver implements HandlerExceptionResolver {
@Autowired
private ObjectMapper objectMapper;
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
Result<Void> result;
try {
if (ex instanceof BusinessException) {
BusinessException businessException = (BusinessException) ex;
result = Result.fail(businessException.getErrorCode(), businessException.getErrorMessage());
log.error("攔截器業務異常 - URL: {}, 錯誤碼: {}, 錯誤信息: {}",
request.getRequestURI(), businessException.getErrorCode(), businessException.getErrorMessage(), ex);
} elseif (ex instanceof IllegalArgumentException) {
result = Result.fail(ErrorEnum.PARAM_ERROR.getCode(), ex.getMessage());
log.error("攔截器參數異常 - URL: {}, 錯誤信息: {}", request.getRequestURI(), ex.getMessage(), ex);
} else {
// 根據環境返回不同信息
String activeProfile = Arrays.toString(request.getServletContext().getInitParameterNames());
String errorMsg;
if (activeProfile.contains("dev")) {
String stackTrace = getStackTraceAsString(ex);
errorMsg = String.format("開發環境-攔截器未知異常:%s\n堆棧信息:%s", ex.getMessage(), stackTrace);
} else {
errorMsg = ErrorEnum.SYSTEM_ERROR.getMessage();
}
result = Result.fail(ErrorEnum.SYSTEM_ERROR.getCode(), errorMsg);
log.error("攔截器未知異常 - URL: {}, 錯誤信息: {}", request.getRequestURI(), ex.getMessage(), ex);
}
// 寫入響應
objectMapper.writeValue(response.getWriter(), result);
} catch (Exception e) {
log.error("處理攔截器異常失?。?, e);
try {
response.getWriter().write("{\"code\":500,\"message\":\"系統繁忙,請稍后再試\",\"data\":null}");
} catch (IOException ioException) {
log.error("寫入響應失?。?, ioException);
}
}
returnnew ModelAndView();
}
privateString getStackTraceAsString(Throwable e) {
if (e == null) {
return"";
}
try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
e.printStackTrace(pw);
return sw.toString();
} catch (IOException ex) {
log.error("獲取異常堆棧信息失敗:", ex);
return"獲取堆棧信息失敗";
}
}
}
(6)異步配置:AsyncConfig.java
package com.example.demo.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
@Slf4j
publicclass AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心線程數
executor.setCorePoolSize(5);
// 最大線程數
executor.setMaxPoolSize(10);
// 隊列容量
executor.setQueueCapacity(25);
// 線程名稱前綴
executor.setThreadNamePrefix("Async-Task-");
// 線程空閑時間(秒)
executor.setKeepAliveSeconds(60);
// 初始化
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("異步方法執行失敗 - 方法名: {}, 參數: {}, 錯誤信息: {}",
method.getName(), Arrays.toString(params), ex.getMessage(), ex);
// 這里可以加告警邏輯,比如調用郵件服務發送告警
// emailService.sendAlarmEmail("異步方法執行失敗", ex.getMessage());
};
}
}
(7)配置文件:application.yml
spring:
profiles:
active: dev # 開發環境:dev;生產環境:prod
mvc:
throw-exception-if-no-handler-found: true # 404時拋異常
web:
resources:
add-mappings: false # 關閉靜態資源映射
datasource:
# 數據庫配置(根據自己的項目填寫)
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
username: root
password: 123456
# 日志配置
logging:
level:
com.example.demo: debug # 自己項目的包日志級別設為debug
org.springframework: warn # Spring框架的日志級別設為warn,減少冗余日志
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"
file:
name: logs/demo.log # 日志文件路徑
# 生產環境配置(application-prod.yml)
---
spring:
config:
activate:
on-profile: prod
logging:
level:
com.example.demo: info # 生產環境日志級別設為info
file:
name: logs/demo-prod.log
# 開發環境配置(application-dev.yml)
---
spring:
config:
activate:
on-profile: dev
# 開發環境可以開啟Swagger(如果用的話)
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
operationsSorter: method
3. 使用注意事項
- 異常優先級:記住 “具體異常優先于籠統異?!?,比如BusinessException是RuntimeException的子類,所以handleBusinessException會先生效。不要把籠統的異常(比如Exception)放在前面處理,否則具體的異常處理方法會失效。
- 日志打?。核挟惓L幚矸椒ǘ家蛴≡敿毜娜罩荆òó惓6褩#奖闩挪閱栴}。但要注意日志級別,業務異常用error級別,參數異常也用error級別,系統異常同樣用error級別。
- 環境配置:開發環境和生產環境的配置要區分開,尤其是異常信息的返回,生產環境絕對不能暴露堆棧信息。
- 告警機制:生產環境下,如果發生未知異常(Exception),建議加一個告警機制(比如發送郵件、短信給開發人員),這樣能及時發現線上問題。
- 自定義異常的使用:業務邏輯中,能拋自定義BusinessException的,就不要拋RuntimeException或Exception,這樣能更精準地處理業務異常,也方便統計不同業務異常的發生頻率。
五、總結:全局異常處理的核心價值
看到這里,相信你已經徹底搞懂 SpringBoot 全局異常處理了。最后咱們來總結一下它的核心價值,幫你鞏固記憶:
- 代碼更簡潔:不用在每個 Controller、每個方法里寫 try-catch,把異常處理邏輯集中到一個地方,減少重復代碼,提高開發效率。
- 返回格式統一:不管是正常返回還是異常返回,都用統一的Result格式,前后端協作更順暢,前端不用再處理五花八門的返回格式。
- 異常信息規范:通過自定義異常和錯誤枚舉,統一管理錯誤碼和錯誤信息,避免異常信息混亂,方便問題排查和前端處理。
- 系統更安全:生產環境下隱藏敏感的堆棧信息,保護系統安全,同時給用戶友好的提示。
- 排查問題更快:詳細的異常日志(包括 URL、參數、堆棧信息)能幫我們快速定位問題,減少排查時間。
這套全局異常處理方案,我已經在多個實際項目中驗證過,從中小型項目到大型分布式項目,都能完美適配。你可以直接把代碼抄到自己的項目中,稍作調整就能使用,以后再也不用為異常處理頭疼了!