精品一区二区三区在线成人,欧美精产国品一二三区,Ji大巴进入女人66h,亚洲春色在线视频

告別混亂報錯!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. 使用注意事項

  1. 異常優先級:記住 “具體異常優先于籠統異?!?,比如BusinessException是RuntimeException的子類,所以handleBusinessException會先生效。不要把籠統的異常(比如Exception)放在前面處理,否則具體的異常處理方法會失效。
  2. 日志打?。核挟惓L幚矸椒ǘ家蛴≡敿毜娜罩荆òó惓6褩#奖闩挪閱栴}。但要注意日志級別,業務異常用error級別,參數異常也用error級別,系統異常同樣用error級別。
  3. 環境配置:開發環境和生產環境的配置要區分開,尤其是異常信息的返回,生產環境絕對不能暴露堆棧信息。
  4. 告警機制:生產環境下,如果發生未知異常(Exception),建議加一個告警機制(比如發送郵件、短信給開發人員),這樣能及時發現線上問題。
  5. 自定義異常的使用:業務邏輯中,能拋自定義BusinessException的,就不要拋RuntimeException或Exception,這樣能更精準地處理業務異常,也方便統計不同業務異常的發生頻率。

五、總結:全局異常處理的核心價值

看到這里,相信你已經徹底搞懂 SpringBoot 全局異常處理了。最后咱們來總結一下它的核心價值,幫你鞏固記憶:

  1. 代碼更簡潔:不用在每個 Controller、每個方法里寫 try-catch,把異常處理邏輯集中到一個地方,減少重復代碼,提高開發效率。
  2. 返回格式統一:不管是正常返回還是異常返回,都用統一的Result格式,前后端協作更順暢,前端不用再處理五花八門的返回格式。
  3. 異常信息規范:通過自定義異常和錯誤枚舉,統一管理錯誤碼和錯誤信息,避免異常信息混亂,方便問題排查和前端處理。
  4. 系統更安全:生產環境下隱藏敏感的堆棧信息,保護系統安全,同時給用戶友好的提示。
  5. 排查問題更快:詳細的異常日志(包括 URL、參數、堆棧信息)能幫我們快速定位問題,減少排查時間。

這套全局異常處理方案,我已經在多個實際項目中驗證過,從中小型項目到大型分布式項目,都能完美適配。你可以直接把代碼抄到自己的項目中,稍作調整就能使用,以后再也不用為異常處理頭疼了!

責任編輯:武曉燕 來源: 石杉的架構筆記
相關推薦

2022-03-04 08:31:07

Spring異常處理

2024-02-23 18:59:32

Python函數編程

2017-08-10 10:28:43

SpringBootSpring

2019-01-24 16:11:19

前端全局異常數據校驗

2025-08-28 02:15:00

WinFormNLog工具類

2023-12-13 13:28:00

Spring全局異常處理架構

2023-12-27 07:53:08

全局異常處理處理應用

2025-04-23 08:20:00

JavaScriptURLAPI

2009-02-06 14:11:36

ASP.NET.NET全局異常處理

2024-06-28 10:29:18

異常處理Python

2021-04-20 10:50:38

Spring Boot代碼Java

2019-08-29 14:30:16

代碼開發工具

2024-10-28 08:32:22

統一接口響應SpringBoot響應框架

2019-04-26 13:25:06

服務器開發工具

2023-06-15 14:09:00

解析器Servlet容器

2022-08-03 07:07:10

Spring數據封裝框架

2025-07-07 03:00:00

異常處理Result模式

2024-03-11 05:00:00

Python集合開發

2022-05-03 10:43:43

SpringJava
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 镶黄旗| 淮阳县| 芒康县| 横峰县| 任丘市| 新余市| 象州县| 安庆市| 九龙坡区| 大邑县| 临邑县| 敦煌市| 大渡口区| 常山县| 清水县| 棋牌| 邹城市| 缙云县| 宁安市| 神农架林区| 满洲里市| 胶南市| 渝中区| 黔江区| 阳信县| 临海市| 渭源县| 班玛县| 米脂县| 钦州市| 平顺县| 南岸区| 涿鹿县| 南城县| 长海县| 苍山县| 正镶白旗| 商城县| 图木舒克市| 梅州市| 太谷县|