前後端分離開發非常普遍,後端處理業務,為前端提供介面。服務中總會出現很多運行時異常和業務異常,本文主要講解在 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
,當程式中捕獲到編譯時異常或業務異常時,就拋出這個通用異常,交給全局來處理。(隨著業務複雜度的增加,可以細分自定義異常,如 AuthException
、UserException
、CouponException
等,讓這些細分異常都繼承自 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) {
...
}
本文簡單介紹了統一異常處理和參數校驗,本節的代碼還有很多優化空間,在後面的實戰部分逐一完善。
\/ 程式員優雅哥(youyacoder),今日學習到此結束~~~