前后端全局異常處理及參數(shù)校驗(yàn)-SpringBoot 2.7.2 實(shí)戰(zhàn)基礎(chǔ)

優(yōu)雅哥 SpringBoot 2.7.2 實(shí)戰(zhàn)基礎(chǔ) - 08 - 全局異常處理及參數(shù)校驗(yàn)

前后端分離開(kāi)發(fā)非常普遍疲扎,后端處理業(yè)務(wù)粒蜈,為前端提供接口。服務(wù)中總會(huì)出現(xiàn)很多運(yùn)行時(shí)異常和業(yè)務(wù)異常淌哟,本文主要講解在 SpringBoot 實(shí)戰(zhàn)中如何進(jìn)行異常統(tǒng)一處理和請(qǐng)求參數(shù)的校驗(yàn)吏恭。

1 異常統(tǒng)一處理

所有異常處理相關(guān)的類兜蠕,咱們都放到 com.yygnb.demo.common包中绊袋。

當(dāng)后端發(fā)生異常時(shí),需要按照一個(gè)約定的規(guī)則(結(jié)構(gòu))返回給前端哀九,所以先定義一個(gè)發(fā)生異常時(shí)固定的結(jié)構(gòu)辜昵。

1.1 錯(cuò)誤響應(yīng)結(jié)構(gòu)

發(fā)生異常時(shí)的響應(yīng)結(jié)構(gòu)約定兩個(gè)字段:code——錯(cuò)誤編碼荸镊;msg——錯(cuò)誤消息。創(chuàng)建類:

com.yygnb.demo.common.domain.ErrorResult

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

    private static final long serialVersionUID = -8738363760223125457L;

    /**
     * 錯(cuò)誤碼
     */
    private String code;

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

1.2 通用錯(cuò)誤響應(yīng)常量

有些異常返回的 ErrorResult 是一樣的路鹰,如參數(shù)校驗(yàn)錯(cuò)誤贷洲、未查詢到對(duì)象、系統(tǒng)異常等晋柱,可以定義一些錯(cuò)誤響應(yīng)的常量:

com.yygnb.demo.common.exception.DefaultErrorResult

public interface DefaultErrorResult {

    ErrorResult SYSTEM_ERROR = new ErrorResult("C00001", "系統(tǒng)異常");
    ErrorResult CUSTOM_ERROR = new ErrorResult("C99999", "自定義異常");
    ErrorResult PARAM_BIND_ERROR = new ErrorResult("C00003", "參數(shù)綁定錯(cuò)誤:");
    ErrorResult PARAM_VALID_ERROR = new ErrorResult("S00004", "參數(shù)校驗(yàn)錯(cuò)誤:");
    ErrorResult JSON_PARSE_ERROR = new ErrorResult("S00005", "JSON轉(zhuǎn)換異常");
    ErrorResult CODE_NOT_FOUND = new ErrorResult("S00006", "根據(jù)編碼沒(méi)有查詢到對(duì)象");
    ErrorResult ID_NOT_FOUND = new ErrorResult("S00007", "根據(jù)ID沒(méi)有查詢到對(duì)象");
}

1.3 通用異常類定義

定義一個(gè)通用的異常類 CommonException优构,繼承自 RuntimeException,當(dāng)程序中捕獲到編譯時(shí)異逞憔海或業(yè)務(wù)異常時(shí)钦椭,就拋出這個(gè)通用異常,交給全局來(lái)處理碑诉。(隨著業(yè)務(wù)復(fù)雜度的增加彪腔,可以細(xì)分自定義異常,如 AuthException进栽、UserException德挣、CouponException 等,讓這些細(xì)分異常都繼承自 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;
    }
}

這個(gè)自定義異常類復(fù)寫了父類的構(gòu)造函數(shù)格嗅,同時(shí)定義了一個(gè)成員變量 ErrorResult番挺,便于在同一異常處理時(shí)快速構(gòu)造返回信息。

1.4 全局異常處理

Spring MVC 中提供了全局異常處理的注解:@ControllerAdvice@RestControllerAdvice屯掖。由于前后端分離開(kāi)發(fā) RESTful 接口玄柏,我們這里就使用 @RestControllerAdvice

com.yygnb.demo.common.exception.CommonExceptionHandler

@Slf4j
@RestControllerAdvice
public class CommonExceptionHandler {

    /**
     * 通用業(yè)務(wù)異常
     *
     * @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());
    }

    /**
     * 其他運(yùn)行時(shí)異常
     * @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贴铜,匹配順序?yàn)閺纳贤路嗾<僭O(shè) handleDefaultException 在前面,當(dāng)發(fā)生一個(gè)異常 CommonException 時(shí)绍坝,一來(lái)就會(huì)被 handleDefaultException 捕獲徘意,因?yàn)闊o(wú)論什么異常,都屬于 Exception 的實(shí)例陷嘴,不會(huì)執(zhí)行 handleCommonException映砖。所以越具體的異常處理,越要寫在前面灾挨。

1.5 測(cè)試統(tǒng)一異常處理

在 DemoController 中拋出一個(gè) CommonException:

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

啟動(dòng)服務(wù)邑退,訪問(wèn):

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

結(jié)果返回:

{
  "code": "C99999",
  "msg": "發(fā)生錯(cuò)誤----這是自定義異常"
}

可以看出全局統(tǒng)一異常處理已經(jīng)生效了。

2 參數(shù)校驗(yàn)

傳統(tǒng)參數(shù)校驗(yàn)方式是通過(guò)多個(gè) if/else 來(lái)進(jìn)行劳澄,代碼量大地技,很沒(méi)有意義。Spring Boot 中有個(gè) starter spring-boot-starter-validation 可以幫助咱們很方便的實(shí)現(xiàn)參數(shù)校驗(yàn)秒拔。

2.1 添加依賴

有些文章中說(shuō) spring boot 2.3 還是多少版本以后不用手動(dòng)加入這個(gè) starter莫矗,我試了以后不行,需要手動(dòng)引入該依賴才行砂缩。

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

這個(gè) starter 定義 Validator 以及 SmartValidator 接口作谚,提供 @Validated,支持 spring 環(huán)境庵芭,支持驗(yàn)證組的規(guī)范, 提供了一系列的工廠類以及適配器妹懒。底層依賴 hibernate-validator 包。

2.2 完善異常處理類

在 1.4 中只捕獲了 CommonException 和 Exception双吆,此處要完善參數(shù)綁定眨唬、校驗(yàn)等異常。補(bǔ)充后 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;
    }

    /**
     * 通用業(yè)務(wù)異常
     *
     * @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());
    }

    /**
     * 其他運(yùn)行時(shí)異常
     * @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 自定義校驗(yàn)注解

javax.validation 中提供了很多用于校驗(yàn)的注解好乐,常見(jiàn)的如:@NotNull匾竿、@Min、@Max 等等蔚万,但可能這些注解不夠岭妖,需要自定義注解。例如咱們自定義一個(gè)注解 @OneOf,該注解對(duì)應(yīng)字段的值只能從 value 中選擇:使用方式為:

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

首先定義一個(gè)注解

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();
}

定義注解時(shí)昵慌,@Constraint(validatedBy = OneOfValidator.class)表示校驗(yàn)使用 OneOfValidator 類進(jìn)行校驗(yàn)苔巨,我們需要編寫這個(gè)類。

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);
    }
}

這樣便實(shí)現(xiàn)一個(gè)自定義校驗(yàn)注解了废离。

2.4 RequestBody 參數(shù)校驗(yàn)

新增電腦接口為例,首先需要在方法的參數(shù)前面使用注解 @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 參數(shù)對(duì)應(yīng)的 DTO / 實(shí)體中柿扣,對(duì)需要校驗(yàn)的字段加上校驗(yàn)注解肖方。例如 操作系統(tǒng)operation 只能從 "MacOS", "Windows", "Linux" 中選擇;年份year不能為空且長(zhǎng)度為4:

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

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

此時(shí)重啟服務(wù)未状,調(diào)用新增電腦接口時(shí)俯画,就會(huì)進(jìn)行校驗(yàn)。

2.5 路徑參數(shù)和RequestParam校驗(yàn)

路徑參數(shù)和沒(méi)有封裝為實(shí)體的 RequestParam 參數(shù)司草,首先需要在參數(shù)前面加上校驗(yàn)注解艰垂,然后需要在 Controller 類上面加上注解 @Validated 才會(huì)生效。如在分頁(yè)列表接口中埋虹,要求參數(shù)當(dāng)前頁(yè) page 大于 0:

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

本文簡(jiǎn)單介紹了統(tǒng)一異常處理和參數(shù)校驗(yàn)猜憎,本節(jié)的代碼還有很多優(yōu)化空間,在后面的實(shí)戰(zhàn)部分逐一完善搔课。

插圖1.png

\/ 程序員優(yōu)雅哥(youyacoder)胰柑,今日學(xué)習(xí)到此結(jié)束~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市爬泥,隨后出現(xiàn)的幾起案子柬讨,更是在濱河造成了極大的恐慌,老刑警劉巖袍啡,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件踩官,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡葬馋,警方通過(guò)查閱死者的電腦和手機(jī)卖鲤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)畴嘶,“玉大人蛋逾,你說(shuō)我怎么就攤上這事〈懊酰” “怎么了区匣?”我有些...
    開(kāi)封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我亏钩,道長(zhǎng)莲绰,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任姑丑,我火速辦了婚禮蛤签,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘栅哀。我一直安慰自己震肮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布留拾。 她就那樣靜靜地躺著戳晌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪痴柔。 梳的紋絲不亂的頭發(fā)上沦偎,一...
    開(kāi)封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天,我揣著相機(jī)與錄音咳蔚,去河邊找鬼豪嚎。 笑死,一個(gè)胖子當(dāng)著我的面吹牛屹篓,可吹牛的內(nèi)容都是我干的疙渣。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼堆巧,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼妄荔!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起谍肤,我...
    開(kāi)封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤啦租,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后荒揣,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體篷角,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年系任,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了恳蹲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡俩滥,死狀恐怖嘉蕾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情霜旧,我是刑警寧澤错忱,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響以清,放射性物質(zhì)發(fā)生泄漏儿普。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一掷倔、第九天 我趴在偏房一處隱蔽的房頂上張望眉孩。 院中可真熱鬧,春花似錦勒葱、人聲如沸勺像。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至篮洁,卻和暖如春涩维,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背袁波。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工瓦阐, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人篷牌。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓睡蟋,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親枷颊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子戳杀,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容