在項目中常常出現系統異常的狀況,好比NullPointerException
等等。若是默認未處理的狀況下,springboot
會響應默認的錯誤提示,這樣對用戶體驗不是友好,系統層面的錯誤,用戶不能感知到,即便爲500
的錯誤,能夠給用戶提示一個相似服務器開小差
的友好提示等。java
在微服務裏,每一個服務中都會有異常狀況,幾乎全部服務的默認異常處理配置一致,致使不少重複編碼,咱們將這些重複默認異常處理能夠抽出一個公共starter
包,各個服務依賴便可,定製化異常處理在各個模塊裏開發。git
unified-dispose-springboot-startergithub
這個模塊裏包含異常處理以及全局返回封裝等功能,下面。spring
完整目錄結構以下:springboot
├── 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
複製代碼
異常處理bash
@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
參數異常等。mvc
這裏將默認的404
、405
、415
等默認http
狀態碼也重寫了。app
重寫這個默認的狀態碼須要配置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-異常體系
。
其餘已知異常能夠本身用@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>com.purgeteam</groupId>
<artifactId>unified-dispose-deepblueai-starter</artifactId>
<version>0.1.1.RELEASE</version>
</dependency>
複製代碼
啓動類開啓@EnableGlobalDispose
註解便可。
項目裏不少重複的code,咱們能夠經過必定的方式去簡化,以達到必定目的減小開發量。
示例代碼地址:unified-dispose-springboot
做者GitHub: Purgeyao 歡迎關注