前言
異常的處理在我們的日常開發中是一個繞不過去的坎,在Spring Boot 項目中如何優雅的去處理異常,正是我們這一節課需要研究的方向。
異常的分類
在一個Spring Boot項目中,我們可以把異常分為兩種,第一種是請求到達Controller
層之前,第二種是到達Controller層
之后項目代碼中發生的錯誤。而第一種又可以分為兩種錯誤類型:1. 路徑錯誤 2. 類似於請求方式錯誤,參數類型不對等類似錯誤。
定義ReturnVO和ReturnCode
為了保持返回值的統一,我們這里定義了統一返回的類ReturnVO
,以及一個記錄錯誤返回碼和錯誤信息的枚舉類ReturnCode
,而具體的錯誤信息和錯誤代碼保存到了response.properties
中,使用流進行讀取。
ReturnVO
public class ReturnVO {
private static Properties properties = ReadPropertiesUtil.getProperties(System.getProperty("user.dir") + CommonUrl.RESPONSE_PROP_URL);
/**
* 返回代碼
*/
private String code;
/**
* 返回信息
*/
private String message;
/**
* 返回數據
*/
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
/**
* 默認構造,返回操作正確的返回代碼和信息
*/
public ReturnVO() {
this.setCode(properties.getProperty(ReturnCode.SUCCESS.val()));
this.setMessage(properties.getProperty(ReturnCode.SUCCESS.msg()));
}
/**
* 返回代碼,這里需要在枚舉中去定義
* @param code
*/
public ReturnVO(ReturnCode code) {
this.setCode(properties.getProperty(code.val()));
this.setMessage(properties.getProperty(code.msg()));
}
/**
* 返回數據,默認返回正確的code和message
* @param data
*/
public ReturnVO(Object data) {
this.setCode(properties.getProperty(ReturnCode.SUCCESS.val()));
this.setMessage(properties.getProperty(ReturnCode.SUCCESS.msg()));
this.setData(data);
}
/**
* 返回錯誤的代碼,以及自定義的錯誤信息
* @param code
* @param message
*/
public ReturnVO(ReturnCode code, String message) {
this.setCode(properties.getProperty(code.val()));
this.setMessage(message);
}
/**
* 返回自定義的code,message,以及data
* @param code
* @param message
* @param data
*/
public ReturnVO(ReturnCode code, String message, Object data) {
this.setCode(code.val());
this.setMessage(message);
this.setData(data);
}
@Override
public String toString() {
return "ReturnVO{" +
"code='" + code + '\'' +
", message='" + message + '\'' +
", data=" + data +
'}';
}
}
ReturnCode
其他的錯誤處理只需要在枚舉類中添加對應的異常即可,枚舉的名稱要定義為異常的名稱,這樣可以直接不用對其他的代碼進行修改,添加一個新的異常時,僅僅添加枚舉類中的字段和properties文件中的屬性。
public enum ReturnCode {
/** 操作成功 */
SUCCESS("SUCCESS_CODE", "SUCCESS_MSG"),
/** 操作失敗 */
FAIL("FAIL_CODE", "FAIL_MSG"),
/** 空指針異常 */
NullPointerException("NPE_CODE", "NPE_MSG"),
/** 自定義異常之返回值為空 */
NullResponseException("NRE_CODE", "NRE_MSG"),
/** 運行時異常 */
RuntimeException("RTE_CODE","RTE_MSG"),
/** 請求方式錯誤異常 */
HttpRequestMethodNotSupportedException("REQUEST_METHOD_UNSUPPORTED_CODE","REQUEST_METHOD_UNSUPPORTED_MSG"),
/** INTERNAL_ERROR */
BindException("BIND_EXCEPTION_CODE","BIND_EXCEPTION_MSG"),
/** 頁面路徑不對 */
UrlError("UE_CODE","UE_MSG");
private ReturnCode(String value, String msg){
this.val = value;
this.msg = msg;
}
public String val() {
return val;
}
public String msg() {
return msg;
}
private String val;
private String msg;
}
response.properties
這里我自定義了一些異常用於后面的測試,在我們實際的項目中需要定義很多的異常去完善。
SUCCESS_CODE=2000
SUCCESS_MSG=操作成功
FAIL_CODE=5000
FAIL_MSG=操作失敗
NPE_CODE=5001
NPE_MSG=空指針異常
NRE_CODE=5002
NRE_MSG=返回值為空
RTE_CODE=5001
RTE_MSG=運行時異常
UE_CODE=404
UE_MSG=頁面路徑有誤
REQUEST_METHOD_UNSUPPORTED_CODE=4000
REQUEST_METHOD_UNSUPPORTED_MSG=請求方式異常
BIND_EXCEPTION_CODE=4001
BIND_EXCEPTION_MSG=請求參數綁定失敗
路徑錯誤處理
這里的路徑錯誤處理方式是采用了實現ErrorController
接口,然后實現了getErrorPath()
方法:
/**
* 請求路徑有誤
* @author yangwei
* @since 2019-01-02 18:13
*/
@RestController
public class RequestExceptionHandler implements ErrorController {
@Override
public String getErrorPath() {
return "/error";
}
@RequestMapping("/error")
public ReturnVO errorPage(){
return new ReturnVO(ReturnCode.UrlError);
}
}
這里可以進行測試一下:
使用ControllerAdvice對其他類型的異常進行處理
類似於到達Controller
之前的請求參數錯誤,請求方式錯誤,數據格式不對等等錯誤都歸類為一種,這里僅僅展示請求方式錯誤的處理方式。
/**
* 全局異常處理類
* @author yangwei
*
* 用於全局返回json,如需返回ModelAndView請使用ControllerAdvice
* 繼承了ResponseEntityExceptionHandler,對於一些類似於請求方式異常的異常進行捕獲
*/
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private static Properties properties = ReadPropertiesUtil.getProperties(System.getProperty("user.dir") + CommonUrl.RESPONSE_PROP_URL);
/**
* 重寫handleExceptionInternal,自定義處理過程
**/
@Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
//這里將異常直接傳給handlerException()方法進行處理,返回值為OK保證友好的返回,而不是出現500錯誤碼。
return new ResponseEntity<>(handlerException(ex), HttpStatus.OK);
}
/**
* 異常捕獲
* @param e 捕獲的異常
* @return 封裝的返回對象
**/
@ExceptionHandler(Exception.class)
public ReturnVO handlerException(Throwable e) {
ReturnVO returnVO = new ReturnVO();
String errorName = e.getClass().getName();
errorName = errorName.substring(errorName.lastIndexOf(".") + 1);
//如果沒有定義異常,而是直接拋出一個運行時異常,需要進入以下分支
if (e.getClass() == RuntimeException.class) {
returnVO.setMessage(properties.getProperty(valueOf("RuntimeException").msg()) +": "+ e.getMessage());
returnVO.setCode(properties.getProperty(valueOf("RuntimeException").val()));
} else {
returnVO.setMessage(properties.getProperty(valueOf(errorName).msg()));
returnVO.setCode(properties.getProperty(valueOf(errorName).val()));
}
return returnVO;
}
}
這里我們可以進行測試:
@RestController
@RequestMapping(value = "/user")
public class UserController {
@Autowired
private IUserService userService;
@PostMapping(value = "/findAll")
public Object findAll() {
throw new RuntimeException("ddd");
}
@RequestMapping(value = "/findAll1")
public ReturnVO findAll1(UserDO userDO) {
System.out.println(userDO);
return new ReturnVO(userService.findAll1());
}
@RequestMapping(value = "/test")
public ReturnVO test() {
throw new RuntimeException("測試非自定義運行時異常");
}
}
直接在瀏覽器訪問findAll,默認為get方法,這里按照我們期望會拋出請求方式異常的錯誤:
訪問findAll1?id=123ss,這里由於我們接受的UserDO
中id
屬性是Integer
類型,所以這里報一個參數綁定異常:
訪問test,測試非自定義運行時異常:
結合AOP使用,放入公用模塊減少代碼的重復
我們上節課使用AOP對於全局異常處理進行了一次簡單的操作,這節課進行了完善,並將其放入到我們的公用模塊,使用時只需導入jar包,然后在啟動類配置掃描包路徑即可
/**
* 統一封裝返回值和異常處理
*
* @author vi
* @since 2018/12/20 6:09 AM
*/
@Slf4j
@Aspect
@Order(5)
@Component
public class ResponseAop {
@Autowired
private GlobalExceptionHandler exceptionHandler;
/**
* 切點
*/
@Pointcut("execution(public * indi.viyoung.viboot.*.controller..*(..))")
public void httpResponse() {
}
/**
* 環切
*/
@Around("httpResponse()")
public ReturnVO handlerController(ProceedingJoinPoint proceedingJoinPoint) {
ReturnVO returnVO = new ReturnVO();
try {
Object proceed = proceedingJoinPoint.proceed();
if (proceed instanceof ReturnVO) {
returnVO = (ReturnVO) proceed;
} else {
returnVO.setData(proceed);
}
} catch (Throwable throwable) {
// 這里直接調用剛剛我們在handler中編寫的方法
returnVO = exceptionHandler.handlerException(throwable);
}
return returnVO;
}
}
做完這些准備工作,以后我們在進行異常處理的時候只需要進行以下幾步操作:
- 引入公用模塊jar包
- 在啟動類上配置掃描包路徑
- 如果新增異常的話,在枚舉類中新增后,再去properties中進行返回代碼和返回信息的編輯即可(注意:枚舉類的變量名一定要和異常名保持一致)