springboot之全局處理異常封裝

springboot之全局處理異常封裝

簡介

在項目中常常出現系統異常的狀況,好比NullPointerException等等。若是默認未處理的狀況下,springboot會響應默認的錯誤提示,這樣對用戶體驗不是友好,系統層面的錯誤,用戶不能感知到,即便爲500的錯誤,能夠給用戶提示一個相似服務器開小差的友好提示等。java

在微服務裏,每一個服務中都會有異常狀況,幾乎全部服務的默認異常處理配置一致,致使不少重複編碼,咱們將這些重複默認異常處理能夠抽出一個公共starter包,各個服務依賴便可,定製化異常處理在各個模塊裏開發。git

配置

unified-dispose-springboot-starter

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

完整目錄結構以下:spring

├── 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 或者 @ControllerAdvicespring的異常處理註解。springboot

咱們先建立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參數異常等。網絡

這裏將默認的404405415等默認http狀態碼也重寫了。mvc

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

# 出現錯誤時, 直接拋出異常(便於異常統一處理,不然捕獲不到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包中不建議使用@Getterlombok註解,防止他人未使用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容器

最後將GlobalDefaultExceptionHandlerbean的方式注入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);
  }

}

GlobalDefaultConfigurationresources/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 {

}

使用@ImportGlobalDefaultConfiguration導入便可。

使用

添加依賴

<dependency>
  <groupId>com.purgeteam</groupId>
  <artifactId>unified-dispose-deepblueai-starter</artifactId>
  <version>0.1.1.RELEASE</version>
</dependency>

啓動類開啓@EnableGlobalDispose註解便可。

總結

項目裏不少重複的code,咱們能夠經過必定的方式去簡化,以達到必定目的減小開發量。

示例代碼地址: unified-dispose-springboot

做者GitHub:
Purgeyao 歡迎關注

相關文章
相關標籤/搜索