springboot之全局處理異常封裝

  • 2019 年 10 月 3 日
  • 筆記

springboot之全局處理異常封裝

簡介

在項目中經常出現系統異常的情況,比如NullPointerException等等。如果默認未處理的情況下,springboot會響應默認的錯誤提示,這樣對用戶體驗不是友好,系統層面的錯誤,用戶不能感知到,即使為500的錯誤,可以給用戶提示一個類似伺服器開小差的友好提示等。

在微服務里,每個服務中都會有異常情況,幾乎所有服務的默認異常處理配置一致,導致很多重複編碼,我們將這些重複默認異常處理可以抽出一個公共starter包,各個服務依賴即可,訂製化異常處理在各個模組里開發。

配置

unified-dispose-springboot-starter

這個模組里包含異常處理以及全局返回封裝等功能,下面。

完整目錄結構如下:

├── pom.xml  ├── src  │   ├── main  │   │   ├── java  │   │   │   └── com  │   │   │       └── purgetiem  │   │   │           └── starter  │   │   │               └── dispose  │   │   │                   ├── GlobalDefaultConfiguration.java  │   │   │                   ├── GlobalDefaultProperties.java  │   │   │                   ├── Interceptors.java  │   │   │                   ├── Result.java  │   │   │                   ├── advice  │   │   │                   │   └── CommonResponseDataAdvice.java  │   │   │                   ├── annotation  │   │   │                   │   ├── EnableGlobalDispose.java  │   │   │                   │   └── IgnorReponseAdvice.java  │   │   │                   └── exception  │   │   │                       ├── GlobalDefaultExceptionHandler.java  │   │   │                       ├── category  │   │   │                       │   └── BusinessException.java  │   │   │                       └── error  │   │   │                           ├── CommonErrorCode.java  │   │   │                           └── details  │   │   │                               └── BusinessErrorCode.java  │   │   └── resources  │   │       ├── META-INF  │   │       │   └── spring.factories  │   │       └── dispose.properties  │   └── test  │       └── java

異常處理

@RestControllerAdvice 或者 @ControllerAdvice為spring的異常處理註解。

我們先創建GlobalDefaultExceptionHandler 全局異常處理類:

@RestControllerAdvice  public class GlobalDefaultExceptionHandler {      private static final Logger log = LoggerFactory.getLogger(GlobalDefaultExceptionHandler.class);      /**     * NoHandlerFoundException 404 異常處理     */    @ExceptionHandler(value = NoHandlerFoundException.class)    @ResponseStatus(HttpStatus.NOT_FOUND)    public Result handlerNoHandlerFoundException(NoHandlerFoundException exception) {      outPutErrorWarn(NoHandlerFoundException.class, CommonErrorCode.NOT_FOUND, exception);      return Result.ofFail(CommonErrorCode.NOT_FOUND);    }      /**     * HttpRequestMethodNotSupportedException 405 異常處理     */    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)    public Result handlerHttpRequestMethodNotSupportedException(        HttpRequestMethodNotSupportedException exception) {      outPutErrorWarn(HttpRequestMethodNotSupportedException.class,          CommonErrorCode.METHOD_NOT_ALLOWED, exception);      return Result.ofFail(CommonErrorCode.METHOD_NOT_ALLOWED);    }      /**     * HttpMediaTypeNotSupportedException 415 異常處理     */    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)    public Result handlerHttpMediaTypeNotSupportedException(        HttpMediaTypeNotSupportedException exception) {      outPutErrorWarn(HttpMediaTypeNotSupportedException.class,          CommonErrorCode.UNSUPPORTED_MEDIA_TYPE, exception);      return Result.ofFail(CommonErrorCode.UNSUPPORTED_MEDIA_TYPE);    }      /**     * Exception 類捕獲 500 異常處理     */    @ExceptionHandler(value = Exception.class)    public Result handlerException(Exception e) {      return ifDepthExceptionType(e);    }      /**     * 二次深度檢查錯誤類型     */    private Result ifDepthExceptionType(Throwable throwable) {      Throwable cause = throwable.getCause();      if (cause instanceof ClientException) {        return handlerClientException((ClientException) cause);      }      if (cause instanceof FeignException) {        return handlerFeignException((FeignException) cause);      }      outPutError(Exception.class, CommonErrorCode.EXCEPTION, throwable);      return Result.ofFail(CommonErrorCode.EXCEPTION);    }      /**     * FeignException 類捕獲     */    @ExceptionHandler(value = FeignException.class)    public Result handlerFeignException(FeignException e) {      outPutError(FeignException.class, CommonErrorCode.RPC_ERROR, e);      return Result.ofFail(CommonErrorCode.RPC_ERROR);    }      /**     * ClientException 類捕獲     */    @ExceptionHandler(value = ClientException.class)    public Result handlerClientException(ClientException e) {      outPutError(ClientException.class, CommonErrorCode.RPC_ERROR, e);      return Result.ofFail(CommonErrorCode.RPC_ERROR);    }      /**     * BusinessException 類捕獲     */    @ExceptionHandler(value = BusinessException.class)    public Result handlerBusinessException(BusinessException e) {      outPutError(BusinessException.class, CommonErrorCode.BUSINESS_ERROR, e);      return Result.ofFail(e.getCode(), e.getMessage());    }      /**     * HttpMessageNotReadableException 參數錯誤異常     */    @ExceptionHandler(HttpMessageNotReadableException.class)    public Result handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {      outPutError(HttpMessageNotReadableException.class, CommonErrorCode.PARAM_ERROR, e);      String msg = String.format("%s : 錯誤詳情( %s )", CommonErrorCode.PARAM_ERROR.getMessage(),          e.getRootCause().getMessage());      return Result.ofFail(CommonErrorCode.PARAM_ERROR.getCode(), msg);    }      /**     * BindException 參數錯誤異常     */    @ExceptionHandler(BindException.class)    public Result handleMethodArgumentNotValidException(BindException e) {      outPutError(BindException.class, CommonErrorCode.PARAM_ERROR, e);      BindingResult bindingResult = e.getBindingResult();      return getBindResultDTO(bindingResult);    }      private Result getBindResultDTO(BindingResult bindingResult) {      List<FieldError> fieldErrors = bindingResult.getFieldErrors();      if (log.isDebugEnabled()) {        for (FieldError error : fieldErrors) {          log.error("{} -> {}", error.getDefaultMessage(), error.getDefaultMessage());        }      }        if (fieldErrors.isEmpty()) {        log.error("validExceptionHandler error fieldErrors is empty");        Result.ofFail(CommonErrorCode.BUSINESS_ERROR.getCode(), "");      }        return Result          .ofFail(CommonErrorCode.PARAM_ERROR.getCode(), fieldErrors.get(0).getDefaultMessage());    }      public void outPutError(Class errorType, Enum secondaryErrorType, Throwable throwable) {      log.error("[{}] {}: {}", errorType.getSimpleName(), secondaryErrorType, throwable.getMessage(),          throwable);    }      public void outPutErrorWarn(Class errorType, Enum secondaryErrorType, Throwable throwable) {      log.warn("[{}] {}: {}", errorType.getSimpleName(), secondaryErrorType, throwable.getMessage());    }    }  

大致內容處理了一些項目常見的異常Exception,BindException參數異常等。

這裡將默認的404、405、415等默認http狀態碼也重寫了。

重寫這個默認的狀態碼需要配置throw-exception-if-no-handler-found以及add-mappings。

# 出現錯誤時, 直接拋出異常(便於異常統一處理,否則捕獲不到404)  spring.mvc.throw-exception-if-no-handler-found=true  # 是否開啟默認的資源處理,默認為true  spring.resources.add-mappings=false

ps: 請注意這兩個配置會將靜態資源忽略。

請產考WebMvcAutoConfiguration#addResourceHandlers

Exception為了防止未知的異常沒有防護到,默認給用戶返回伺服器開小差,請稍後再試等提示。

具體異常默認會以小到大去匹配。

如果拋出BindException,自定義有BindException就會去這個處理器里處理。沒有就會走到它的父類去匹配,請參考java-異常體系。

img

其他已知異常可以自己用@ExceptionHandler註解進行捕獲處理。

通用異常枚舉

為了避免異常值不好維護,我們使用CommonErrorCode枚舉把常見的異常提示維護起來。

@Getter  public enum CommonErrorCode {      /**     * 404 Web 伺服器找不到您所請求的文件或腳本。請檢查URL 以確保路徑正確。     */    NOT_FOUND("CLOUD-404",        String.format("哎呀,無法找到這個資源啦(%s)", HttpStatus.NOT_FOUND.getReasonPhrase())),      /**     * 405 對於請求所標識的資源,不允許使用請求行中所指定的方法。請確保為所請求的資源設置了正確的 MIME 類型。     */    METHOD_NOT_ALLOWED("CLOUD-405",        String.format("請換個姿勢操作試試(%s)", HttpStatus.METHOD_NOT_ALLOWED.getReasonPhrase())),      /**     * 415 Unsupported Media Type     */    UNSUPPORTED_MEDIA_TYPE("CLOUD-415",        String.format("呀,不支援該媒體類型(%s)", HttpStatus.UNSUPPORTED_MEDIA_TYPE.getReasonPhrase())),      /**     * 系統異常 500 伺服器的內部錯誤     */    EXCEPTION("CLOUD-500", "伺服器開小差,請稍後再試"),      /**     * 系統限流     */    TRAFFIC_LIMITING("CLOUD-429", "哎呀,網路擁擠請稍後再試試"),      /**     * 服務調用異常     */    API_GATEWAY_ERROR("API-9999", "網路繁忙,請稍後再試"),      /**     * 參數錯誤     */    PARAM_ERROR("CLOUD-100", "參數錯誤"),      /**     * 業務異常     */    BUSINESS_ERROR("CLOUD-400", "業務異常"),      /**     * rpc調用異常     */    RPC_ERROR("RPC-510", "呀,網路出問題啦!");      private String code;      private String message;      CommonErrorCode(String code, String message) {      this.code = code;      this.message = message;    }  }

其實starter包中不建議使用@Getter等lombok註解,防止他人未使用lombok依賴該項目出現問題。

通用業務異常

這兩個類完成基本可以正常使用異常攔截了,不過為了業務方便,我們創建一個一般通用的業務異常。

BusinessException繼承RuntimeException即可。

@Getter  public class BusinessException extends RuntimeException {      private String code;    private boolean isShowMsg = true;      /**     * 使用枚舉傳參     *     * @param errorCode 異常枚舉     */    public BusinessException(BusinessErrorCode errorCode) {      super(errorCode.getMessage());      this.code = errorCode.getCode();    }      /**     * 使用自定義消息     *     * @param code 值     * @param msg 詳情     */    public BusinessException(String code, String msg) {      super(msg);      this.code = code;    }    }

將BusinessException加入GlobalDefaultExceptionHandler全局異常攔截。

/**   * BusinessException 類捕獲   */  @ExceptionHandler(value = BusinessException.class)  public Result handlerBusinessException(BusinessException e) {    outPutError(BusinessException.class, CommonErrorCode.BUSINESS_ERROR, e);    return Result.ofFail(e.getCode(), e.getMessage());  }

程式主動拋出異常可以通過下面方式:

throw new BusinessException(BusinessErrorCode.BUSINESS_ERROR);  // 或者  throw new BusinessException("CLOUD800","沒有多餘的庫存");

通常不建議直接拋出通用的BusinessException異常,應當在對應的模組里添加對應的領域的異常處理類以及對應的枚舉錯誤類型。

如會員模組:
創建UserException異常類、UserErrorCode枚舉、以及UserExceptionHandler統一攔截類。

UserException:

@Data  public class UserException extends RuntimeException {      private String code;    private boolean isShowMsg = true;      /**     * 使用枚舉傳參     *     * @param errorCode 異常枚舉     */    public UserException(UserErrorCode errorCode) {      super(errorCode.getMessage());      this.setCode(errorCode.getCode());    }    }

UserErrorCode:

@Getter  public enum UserErrorCode {      /**       * 許可權異常       */      NOT_PERMISSIONS("CLOUD401","您沒有操作許可權"),      ;        private String code;        private String message;        CommonErrorCode(String code, String message) {          this.code = code;          this.message = message;      }  }

UserExceptionHandler:

@Slf4j  @RestControllerAdvice  public class UserExceptionHandler {      /**     * UserException 類捕獲     */    @ExceptionHandler(value = UserException.class)    public Result handler(UserException e) {      log.error(e.getMessage(), e);      return Result.ofFail(e.getCode(), e.getMessage());    }    }

最後業務使用如下:

// 判斷是否有許可權拋出異常  throw new UserException(UserErrorCode.NOT_PERMISSIONS);

加入spring容器

最後將GlobalDefaultExceptionHandler以bean的方式注入spring容器。

@Configuration  @EnableConfigurationProperties(GlobalDefaultProperties.class)  @PropertySource(value = "classpath:dispose.properties", encoding = "UTF-8")  public class GlobalDefaultConfiguration {      @Bean    public GlobalDefaultExceptionHandler globalDefaultExceptionHandler() {      return new GlobalDefaultExceptionHandler();    }      @Bean    public CommonResponseDataAdvice commonResponseDataAdvice(GlobalDefaultProperties globalDefaultProperties){      return new CommonResponseDataAdvice(globalDefaultProperties);    }    }

將GlobalDefaultConfiguration在resources/META-INF/spring.factories文件下載入。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=    com.purgetime.starter.dispose.GlobalDefaultConfiguration

不過我們這次使用註解方式開啟。其他項目依賴包後,需要添加@EnableGlobalDispose才可以將全局攔截的特性開啟。

將剛剛創建的spring.factories注釋掉,創建EnableGlobalDispose註解。

@Retention(RetentionPolicy.RUNTIME)  @Target(ElementType.TYPE)  @Import(GlobalDefaultConfiguration.class)  public @interface EnableGlobalDispose {    }

使用@Import將GlobalDefaultConfiguration導入即可。

使用

添加依賴

<dependency>    <groupId>io.deepblueai</groupId>    <artifactId>unified-dispose-deepblueai-starter</artifactId>    <version>0.1.0.RELEASE</version>  </dependency>

啟動類開啟@EnableGlobalDispose註解即可。

總結

項目里很多重複的code,我們可以通過一定的方式去簡化,以達到一定目的減少開發量。

示例程式碼地址:unified-dispose-springboot

作者GitHub:
Purgeyao 歡迎關注

Tags: