全局異常處理及參數校驗-SpringBoot 2.7.2 實戰基礎 (建議收藏)

来源:https://www.cnblogs.com/youyacoder/archive/2022/08/17/16595511.html
-Advertisement-
Play Games

前後端分離開發非常普遍,後端處理業務,為前端提供介面。服務中總會出現很多運行時異常和業務異常,本文主要講解在 SpringBoot 實戰中如何進行異常統一處理和請求參數的校驗。 ...


優雅哥 SpringBoot 2.7 實戰基礎 - 08 - 全局異常處理及參數校驗

前後端分離開發非常普遍,後端處理業務,為前端提供介面。服務中總會出現很多運行時異常和業務異常,本文主要講解在 SpringBoot 實戰中如何進行異常統一處理和請求參數的校驗。

1 異常統一處理

所有異常處理相關的類,咱們都放到 com.yygnb.demo.common包中。

當後端發生異常時,需要按照一個約定的規則(結構)返回給前端,所以先定義一個發生異常時固定的結構。

1.1 錯誤響應結構

發生異常時的響應結構約定兩個欄位:code——錯誤編碼;msg——錯誤消息。創建類:

com.yygnb.demo.common.domain.ErrorResult

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErrorResult implements Serializable {

    private static final long serialVersionUID = -8738363760223125457L;

    /**
     * 錯誤碼
     */
    private String code;

    /**
     * 錯誤消息
     */
    private String msg;
  
    public static ErrorResult build(ErrorResult commonErrorResult, String msg) {
        return new ErrorResult(commonErrorResult.getCode(), commonErrorResult.getMsg() + " " + msg);
    }
}

1.2 通用錯誤響應常量

有些異常返回的 ErrorResult 是一樣的,如參數校驗錯誤、未查詢到對象、系統異常等,可以定義一些錯誤響應的常量:

com.yygnb.demo.common.exception.DefaultErrorResult

public interface DefaultErrorResult {

    ErrorResult SYSTEM_ERROR = new ErrorResult("C00001", "系統異常");
    ErrorResult CUSTOM_ERROR = new ErrorResult("C99999", "自定義異常");
    ErrorResult PARAM_BIND_ERROR = new ErrorResult("C00003", "參數綁定錯誤:");
    ErrorResult PARAM_VALID_ERROR = new ErrorResult("S00004", "參數校驗錯誤:");
    ErrorResult JSON_PARSE_ERROR = new ErrorResult("S00005", "JSON轉換異常");
    ErrorResult CODE_NOT_FOUND = new ErrorResult("S00006", "根據編碼沒有查詢到對象");
    ErrorResult ID_NOT_FOUND = new ErrorResult("S00007", "根據ID沒有查詢到對象");
}

1.3 通用異常類定義

定義一個通用的異常類 CommonException,繼承自 RuntimeException,當程式中捕獲到編譯時異常或業務異常時,就拋出這個通用異常,交給全局來處理。(隨著業務複雜度的增加,可以細分自定義異常,如 AuthExceptionUserExceptionCouponException 等,讓這些細分異常都繼承自 CommonException。)

com.yygnb.demo.common.exception.CommonException

@EqualsAndHashCode(callSuper = true)
@Data
public class CommonException extends RuntimeException {

    protected ErrorResult errorResult;

    public CommonException(String message) {
        super(message);
    }

    public CommonException(String message, Throwable cause) {
        super(message, cause);
    }

    public CommonException(Throwable cause) {
        super(cause);
    }

    protected CommonException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }

    public CommonException(String code, String msg) {
        super(msg + "(" + code + ")");
        this.errorResult = new ErrorResult(code, msg);
    }

    public CommonException(ErrorResult errorResult) {
        super(errorResult.getMsg() + "(" + errorResult.getCode() + ")");
        this.errorResult = errorResult;
    }
}

這個自定義異常類覆寫了父類的構造函數,同時定義了一個成員變數 ErrorResult,便於在同一異常處理時快速構造返回信息。

1.4 全局異常處理

Spring MVC 中提供了全局異常處理的註解:@ControllerAdvice@RestControllerAdvice。由於前後端分離開發 RESTful 介面,我們這裡就使用 @RestControllerAdvice

com.yygnb.demo.common.exception.CommonExceptionHandler

@Slf4j
@RestControllerAdvice
public class CommonExceptionHandler {

    /**
     * 通用業務異常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(value = CommonException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResult handleCommonException(CommonException e) {
        log.error("{}, {}", e.getMessage(), e);
        if (e.getErrorResult() != null) {
            return e.getErrorResult();
        }
        return new ErrorResult(DefaultErrorResult.CUSTOM_ERROR.getCode(), e.getMessage());
    }

    /**
     * 其他運行時異常
     * @param e
     * @return
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResult handleDefaultException(Exception e) {
        log.error("{}, {}", e.getMessage(), e);
        return DefaultErrorResult.SYSTEM_ERROR;
    }
}

上面捕獲了 CommonException 和 Exception,匹配順序為從上往下。假設 handleDefaultException 在前面,當發生一個異常 CommonException 時,一來就會被 handleDefaultException 捕獲,因為無論什麼異常,都屬於 Exception 的實例,不會執行 handleCommonException。所以越具體的異常處理,越要寫在前面。

1.5 測試統一異常處理

在 DemoController 中拋出一個 CommonException:

@GetMapping("hello")
public String hello(String msg) {
    String result = "Hello Spring Boot ! " + msg;
    if ("demo".equals(msg)) {
        throw new CommonException("發生錯誤----這是自定義異常");
    }
    return result;
}

啟動服務,訪問:

http://localhost:9099/demo/hello?msg=demo

結果返回:

{
  "code": "C99999",
  "msg": "發生錯誤----這是自定義異常"
}

可以看出全局統一異常處理已經生效了。

2 參數校驗

傳統參數校驗方式是通過多個 if/else 來進行,代碼量大,很沒有意義。Spring Boot 中有個 starter spring-boot-starter-validation 可以幫助咱們很方便的實現參數校驗。

2.1 添加依賴

有些文章中說 spring boot 2.3 還是多少版本以後不用手動加入這個 starter,我試了以後不行,需要手動引入該依賴才行。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

這個 starter 定義 Validator 以及 SmartValidator 介面,提供 @Validated,支持 spring 環境,支持驗證組的規範, 提供了一系列的工廠類以及適配器。底層依賴 hibernate-validator 包。

2.2 完善異常處理類

在 1.4 中只捕獲了 CommonException 和 Exception,此處要完善參數綁定、校驗等異常。補充後 CommonExceptionHandler 如下:

@Slf4j
@RestControllerAdvice
public class CommonExceptionHandler {

    @ExceptionHandler(value = BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResult handleBindException(BindException e) {
        log.error("{}", e.getMessage(), e);
        List<String> defaultMsg = e.getBindingResult().getAllErrors()
                .stream()
                .map(ObjectError::getDefaultMessage)
                .collect(Collectors.toList());
        return ErrorResult.build(DefaultErrorResult.PARAM_BIND_ERROR, defaultMsg.get(0));
    }

    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("{}", e.getMessage(), e);
        List<String> defaultMsg = e.getBindingResult().getFieldErrors()
                .stream()
                .map(fieldError -> "【" + fieldError.getField() + "】" + fieldError.getDefaultMessage())
                .collect(Collectors.toList());
        return ErrorResult.build(DefaultErrorResult.PARAM_VALID_ERROR, defaultMsg.get(0));
    }

    @ExceptionHandler(value = MissingServletRequestParameterException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResult handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
        log.error("{}", e.getMessage(), e);
        log.error("ParameterName: {}", e.getParameterName());
        log.error("ParameterType: {}", e.getParameterType());
        return ErrorResult.build(DefaultErrorResult.PARAM_VALID_ERROR, e.getMessage());
    }

    @ExceptionHandler(value = ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResult handleBindGetException(ConstraintViolationException e) {
        log.error("{}", e.getMessage(), e);
        List<String> defaultMsg = e.getConstraintViolations()
                .stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.toList());
        return ErrorResult.build(DefaultErrorResult.PARAM_VALID_ERROR, defaultMsg.get(0));
    }

    @ExceptionHandler(HttpMessageNotReadableException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResult error(HttpMessageNotReadableException e){
        log.error("{}", e.getMessage(), e);
        return DefaultErrorResult.JSON_PARSE_ERROR;
    }

    /**
     * 通用業務異常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(value = CommonException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResult handleCommonException(CommonException e) {
        log.error("{}, {}", e.getMessage(), e);
        if (e.getErrorResult() != null) {
            return e.getErrorResult();
        }
        return new ErrorResult(DefaultErrorResult.CUSTOM_ERROR.getCode(), e.getMessage());
    }

    /**
     * 其他運行時異常
     * @param e
     * @return
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResult handleDefaultException(Exception e) {
        log.error("{}, {}", e.getMessage(), e);
        return DefaultErrorResult.SYSTEM_ERROR;
    }
}

2.3 自定義校驗註解

javax.validation 中提供了很多用於校驗的註解,常見的如:@NotNull、@Min、@Max 等等,但可能這些註解不夠,需要自定義註解。例如咱們自定義一個註解 @OneOf,該註解對應欄位的值只能從 value 中選擇:使用方式為:

@OneOf(value = {"MacOS", "Windows", "Linux"})

首先定義一個註解

com.yygnb.demo.common.validator.OneOf

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = OneOfValidator.class)
public @interface OneOf {

    String message() default "只能從備選值中選擇";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String[] value();
}

定義註解時,@Constraint(validatedBy = OneOfValidator.class)表示校驗使用 OneOfValidator 類進行校驗,我們需要編寫這個類。

com.yygnb.demo.common.validator.OneOfValidator

public class OneOfValidator implements ConstraintValidator<OneOf, String> {

    private List<String> list;

    @Override
    public void initialize(OneOf constraintAnnotation) {
        list = Arrays.asList(constraintAnnotation.value());
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if (s == null) {
            return true;
        }
        return list.contains(s);
    }
}

這樣便實現一個自定義校驗註解了。

2.4 RequestBody 參數校驗

新增電腦介面為例,首先需要在方法的參數前面使用註解 @Valid@Validated 修飾。在此處使用兩者之一都可以,前者是 javax.validation 中提供的,後者是 springframework 中提供的:

@Operation(summary = "新增電腦")
@PostMapping()
public Computer save(@RequestBody @Validated Computer computer) {
    computer.setId(null);
    this.computerService.save(computer);
    return computer;
}

在 RequestBody 參數對應的 DTO / 實體中,對需要校驗的欄位加上校驗註解。例如 操作系統operation 只能從 "MacOS", "Windows", "Linux" 中選擇;年份year不能為空且長度為4:

@OneOf(value = {"MacOS", "Windows", "Linux"})
@Schema(title = "操作系統")
private String operation;

@NotNull(message = "不能為空")
@Length(min = 4, max = 4)
@Schema(title = "年份")
private String year;

此時重啟服務,調用新增電腦介面時,就會進行校驗。

2.5 路徑參數和RequestParam校驗

路徑參數和沒有封裝為實體的 RequestParam 參數,首先需要在參數前面加上校驗註解,然後需要在 Controller 類上面加上註解 @Validated 才會生效。如在分頁列表介面中,要求參數當前頁 page 大於 0:

public Page<Computer> findPage(@PathVariable @Min(1) Integer page, @PathVariable @Max(10) Integer size) {
...
}

本文簡單介紹了統一異常處理和參數校驗,本節的代碼還有很多優化空間,在後面的實戰部分逐一完善。

image
\/ 程式員優雅哥(youyacoder),今日學習到此結束~~~


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 使用$parent訪問父級組件實例 點擊打開視頻講解更詳細 和 $root 類似,$parent property 可以用來從一個子組件訪問父組件的實例。它提供了一種機會,可以在後期隨時觸達父級組件,以替代將數據以 prop 的方式傳入子組件的方式。 註意:在絕大多數情況下,觸達父級組件會使得你的應 ...
  • 前端周刊:2022-13 期 前端開發 Vue3 文檔更新 更新後的 Vue3 文檔分別提供了選項式和組合式兩個版本,內容豐富程度和細緻程度也有很大提升,推薦大家重讀一遍。 前端請求併發控制 所有請求都在一個數組中,以限定的併發數將請求發完 前端 API 請求的各種騷操作 併發控制 / 節流控制 / ...
  • 處理邊界情況之使用$root訪問根實例 點擊打開視頻教程 在每個 new Vue 實例的子組件中,其根實例可以通過 $root property 進行訪問。 例如,在這個根實例中: src\main.js import Vue from 'vue' import App from './App.vu ...
  • 本文是深入淺出 ahooks 源碼系列文章的第七篇,該系列已整理成文檔-地址。覺得還不錯,給個 star 支持一下哈,Thanks。 今天我們來聊聊定時器。 useInterval 和 useTimeout 看名稱,我們就能大概知道,它們的功能對應的是 setInterval 和 setTimeou ...
  • 行為型模式(Behavioral Pattern)是指對在不同對象之間劃分責任和演算法進行抽象化的設計模式,它不僅關註類和對象的結構,而且重點關註他們之間的相互作用。 對於一個系統來說,對象不是孤立運行的,對象之間可以通過相互通信和協作完成某些複雜的功能,對象之間是相互影響的。 ...
  • 性能優化屬於Java高級崗的必備技能,而且大廠特別喜歡考察,今天主要給大家介紹9種性能優化的方法@mikechen 1.代碼 之所以把代碼放到第一位,是因為這一點最容易引忽視,比如拿到一個性能優化的需求以後,言必稱緩存、非同步等。 實際上,第一步就應該是分析相關的代碼,找出相應的瓶頸,再來考慮具體的優 ...
  • 在搭建微服務框架的時候,離不開一個關鍵的微服務組件,應用程式網關,這個組件在微服務框架體系內至關重要。通過應用程式網關,可以將微服務框架內的服務進行重定向、限流、監控、故障轉移等整操作後,對外提供應用程式池中的服務,應用程式服務池是對外部不透明的,唯一的數據交換點就是微服務的應用程式網關。 應用程式 ...
  • 1. 瞭解Solr Solr是一個獨立的企業級搜索應用伺服器,對外提供API介面。用戶可以通過HTTP請求向搜索引擎伺服器提交一定格式的XML文件,生成索引;也可以通過HTTP GET操作提出查找請求, 並得到XML格式的返回結果。Solr現在支持多種返回結果。 2. 安裝配置Solr 2.1Sol ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...