優(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)部分逐一完善搔课。
\/ 程序員優(yōu)雅哥(youyacoder)胰柑,今日學(xué)習(xí)到此結(jié)束~~~