Springboot專案全域性異常統一處理
最近在做專案時需要對異常進行全域性統一處理,主要是一些分類入庫以及記錄日誌等,因為專案是基於Springboot的,所以去網路上找了一些部落格文件,然後再結合專案本身的一些特殊需求做了些許改造,現在記錄下來便於以後檢視。
在網路上找到關於Springboot全域性異常統一處理的文件部落格主要是兩種方案:
1、基於@ControllerAdvice註解的Controller層的全域性異常統一處理
以下是網上一位博主給出的程式碼示例,該部落格地址為:https://www.cnblogs.com/magicalSam/p/7198420.html
import org.springframework.ui.Model; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; /** * controller 增強器 * * @author sam * @since 2017/7/17 */ @ControllerAdvice public class MyControllerAdvice { /** * 全域性異常捕捉處理 * @param ex * @return */ @ResponseBody @ExceptionHandler(value = Exception.class) public Map errorHandler(Exception ex) { Map map = new HashMap(); map.put("code", 100); map.put("msg", ex.getMessage()); return map; } /** * 攔截捕捉自定義異常 MyException.class * @param ex * @return */ @ResponseBody @ExceptionHandler(value = MyException.class) public Map myErrorHandler(MyException ex) { Map map = new HashMap(); map.put("code", ex.getCode()); map.put("msg", ex.getMsg()); return map; } }
這個程式碼示例寫的非常淺顯易懂,但是需要注意的是:基於@ControllerAdvice註解的全域性異常統一處理只能針對於Controller層的異常,意思是隻能捕獲到Controller層的異常,在service層或者其他層面的異常都不能捕獲。
根據這段示例程式碼以及結合專案本身的實際需求,對該例項程式碼做了稍微改造(其實幾乎沒做改造,只是業務處理不一樣而已):
@ControllerAdvice public class AdminExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(AdminExceptionHandler.class); /** * @Author: gmy * @Description: 系統異常捕獲處理 * @Date: 16:07 2018/5/30 */ @ResponseBody @ExceptionHandler(value = Exception.class) public APIResponse javaExceptionHandler(Exception ex) {//APIResponse是專案中對外統一的出口封裝,可以根據自身專案的需求做相應更改 logger.error("捕獲到Exception異常",ex); //異常日誌入庫 return new APIResponse(APIResponse.FAIL,null,ex.getMessage()); } /** * @Author: gmy * @Description: 自定義異常捕獲處理 * @Date: 16:08 2018/5/30 */ @ResponseBody @ExceptionHandler(value = MessageCenterException.class)//MessageCenterException是自定義的一個異常 public APIResponse messageCenterExceptionHandler(MessageCenterException ex) { logger.error("捕獲到MessageCenterException異常",ex.getException()); //異常日誌入庫 return ex.getApiResponse(); } }
public class MessageCenterException extends RuntimeException { public MessageCenterException(APIResponse apiResponse, Exception exception){ this.apiResponse = apiResponse; this.exception = exception; } private Exception exception; private APIResponse apiResponse; public Exception getException() { return exception; } public void setException(Exception exception) { this.exception = exception; } public APIResponse getApiResponse() { return apiResponse; } public void setApiResponse(APIResponse apiResponse) { this.apiResponse = apiResponse; } }
經過測試發現可以捕獲到Controller層的異常,當前前提是Controller層沒有對異常進行catch處理,如果Controller層對異常進行了catch處理,那麼在這裡就不會捕獲到Controller層的異常了,所以這一點要特別注意。
在實際測試中還發現,如果在Controller中不做異常catch處理,在service中丟擲異常(service中也不錯異常catch處理),那麼也是可以在這裡捕獲到異常的。
2、基於Springboot自身的全域性異常統一處理,主要是實現ErrorController介面或者繼承AbstractErrorController抽象類或者繼承BasicErrorController類
以下是網上一位博主給出的示例程式碼,部落格地址為:https://blog.csdn.net/king_is_everyone/article/details/53080851
@Controller
@RequestMapping(value = "error")
@EnableConfigurationProperties({ServerProperties.class})
public class ExceptionController implements ErrorController {
private ErrorAttributes errorAttributes;
@Autowired
private ServerProperties serverProperties;
/**
* 初始化ExceptionController
* @param errorAttributes
*/
@Autowired
public ExceptionController(ErrorAttributes errorAttributes) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.errorAttributes = errorAttributes;
}
/**
* 定義404的ModelAndView
* @param request
* @param response
* @return
*/
@RequestMapping(produces = "text/html",value = "404")
public ModelAndView errorHtml404(HttpServletRequest request,
HttpServletResponse response) {
response.setStatus(getStatus(request).value());
Map<String, Object> model = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.TEXT_HTML));
return new ModelAndView("error/404", model);
}
/**
* 定義404的JSON資料
* @param request
* @return
*/
@RequestMapping(value = "404")
@ResponseBody
public ResponseEntity<Map<String, Object>> error404(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.TEXT_HTML));
HttpStatus status = getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status);
}
/**
* 定義500的ModelAndView
* @param request
* @param response
* @return
*/
@RequestMapping(produces = "text/html",value = "500")
public ModelAndView errorHtml500(HttpServletRequest request,
HttpServletResponse response) {
response.setStatus(getStatus(request).value());
Map<String, Object> model = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.TEXT_HTML));
return new ModelAndView("error/500", model);
}
/**
* 定義500的錯誤JSON資訊
* @param request
* @return
*/
@RequestMapping(value = "500")
@ResponseBody
public ResponseEntity<Map<String, Object>> error500(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.TEXT_HTML));
HttpStatus status = getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status);
}
/**
* Determine if the stacktrace attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the stacktrace attribute should be included
*/
protected boolean isIncludeStackTrace(HttpServletRequest request,
MediaType produces) {
ErrorProperties.IncludeStacktrace include = this.serverProperties.getError().getIncludeStacktrace();
if (include == ErrorProperties.IncludeStacktrace.ALWAYS) {
return true;
}
if (include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM) {
return getTraceParameter(request);
}
return false;
}
/**
* 獲取錯誤的資訊
* @param request
* @param includeStackTrace
* @return
*/
private Map<String, Object> getErrorAttributes(HttpServletRequest request,
boolean includeStackTrace) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
return this.errorAttributes.getErrorAttributes(requestAttributes,
includeStackTrace);
}
/**
* 是否包含trace
* @param request
* @return
*/
private boolean getTraceParameter(HttpServletRequest request) {
String parameter = request.getParameter("trace");
if (parameter == null) {
return false;
}
return !"false".equals(parameter.toLowerCase());
}
/**
* 獲取錯誤編碼
* @param request
* @return
*/
private HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request
.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
try {
return HttpStatus.valueOf(statusCode);
}
catch (Exception ex) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
/**
* 實現錯誤路徑,暫時無用
* @see ExceptionMvcAutoConfiguration#containerCustomizer()
* @return
*/
@Override
public String getErrorPath() {
return "";
}
}
該示例寫的也是非常簡單明瞭的,但是結合本身專案的實際需求,也是不能直接拿來用的,需要做相應的改造,改造主要有以下方面:
1、因為專案是前後端分離的,所以Controller層不會有ModelAndView返回型別,需要返回自身的APIResponse返回型別
2、專案需要統計全部的異常,而不只是404或者500的異常
3、捕獲到異常之後需要做特殊化的業務處理
所以基於以上幾方面對示例程式碼做了改造,具體改造程式碼如下:
/**
* @Author: gmy
* @Description: Springboot全域性異常統一處理
* @Date: 2018/5/30
* @Time: 16:41
*/
@RestController
@EnableConfigurationProperties({ServerProperties.class})
public class ExceptionController implements ErrorController {
private ErrorAttributes errorAttributes;
@Autowired
private ServerProperties serverProperties;
/**
* 初始化ExceptionController
* @param errorAttributes
*/
@Autowired
public ExceptionController(ErrorAttributes errorAttributes) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.errorAttributes = errorAttributes;
}
@RequestMapping(value = "/error")
@ResponseBody
public APIResponse error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new APIResponse(APIResponse.FAIL,null,body.get("message").toString());
}
/**
* Determine if the stacktrace attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the stacktrace attribute should be included
*/
protected boolean isIncludeStackTrace(HttpServletRequest request,
MediaType produces) {
ErrorProperties.IncludeStacktrace include = this.serverProperties.getError().getIncludeStacktrace();
if (include == ErrorProperties.IncludeStacktrace.ALWAYS) {
return true;
}
if (include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM) {
return getTraceParameter(request);
}
return false;
}
/**
* 獲取錯誤的資訊
* @param request
* @param includeStackTrace
* @return
*/
private Map<String, Object> getErrorAttributes(HttpServletRequest request,
boolean includeStackTrace) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
return this.errorAttributes.getErrorAttributes(requestAttributes,
includeStackTrace);
}
/**
* 是否包含trace
* @param request
* @return
*/
private boolean getTraceParameter(HttpServletRequest request) {
String parameter = request.getParameter("trace");
if (parameter == null) {
return false;
}
return !"false".equals(parameter.toLowerCase());
}
/**
* 獲取錯誤編碼
* @param request
* @return
*/
private HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request
.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
try {
return HttpStatus.valueOf(statusCode);
}
catch (Exception ex) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
/**
* 實現錯誤路徑,暫時無用
* @return
*/
@Override
public String getErrorPath() {
return "";
}
}
經過測試,可以捕獲到所有層面上的異常,當前前提仍然是沒有對異常進行catch處理,否則這裡也是捕獲不到
以上為網路上常用的兩種全域性異常統一處理方案,經過實際測試發現都可以實現滿足要求。
其實基於AOP也可以實現異常的全域性處理,自己相應的做了測試發現也滿足要求,相應的程式碼如下:
/**
* @Author: gmy
* @Description: 基於AOP的全域性異常統一處理
* @Date: 2018/6/1
* @Time: 13:46
*/
@Component
@Aspect
public class ExceptionAspectController {
public static final Logger logger = LoggerFactory.getLogger(ExceptionAspectController.class);
@Pointcut("execution(* com.test.test.*.*(..))")//此處基於自身專案的路徑做具體的設定
public void pointCut(){}
@Around("pointCut()")
public Object handleControllerMethod(ProceedingJoinPoint pjp) {
Stopwatch stopwatch = Stopwatch.createStarted();
APIResponse<?> apiResponse;
try {
logger.info("執行Controller開始: " + pjp.getSignature() + " 引數:" + Lists.newArrayList(pjp.getArgs()).toString());
apiResponse = (APIResponse<?>) pjp.proceed(pjp.getArgs());
logger.info("執行Controller結束: " + pjp.getSignature() + ", 返回值:" + apiResponse.toString());
logger.info("耗時:" + stopwatch.stop().elapsed(TimeUnit.MILLISECONDS) + "(毫秒).");
} catch (Throwable throwable) {
apiResponse = handlerException(pjp, throwable);
}
return apiResponse;
}
private APIResponse<?> handlerException(ProceedingJoinPoint pjp, Throwable e) {
APIResponse<?> apiResponse = null;
if(e.getClass().isAssignableFrom(MessageCenterException.class) ){
MessageCenterException messageCenterException = (MessageCenterException)e;
logger.error("RuntimeException{方法:" + pjp.getSignature() + ", 引數:" + pjp.getArgs() + ",異常:" + messageCenterException.getException().getMessage() + "}", e);
apiResponse = messageCenterException.getApiResponse();
} else if (e instanceof RuntimeException) {
logger.error("RuntimeException{方法:" + pjp.getSignature() + ", 引數:" + pjp.getArgs() + ",異常:" + e.getMessage() + "}", e);
apiResponse = new APIResponse(APIResponse.FAIL,null,e.getMessage());
} else {
logger.error("異常{方法:" + pjp.getSignature() + ", 引數:" + pjp.getArgs() + ",異常:" + e.getMessage() + "}", e);
apiResponse = new APIResponse(APIResponse.FAIL,null,e.getMessage());
}
return apiResponse;
}
}
經過測試,在執行切點中配置的路徑中的方法有異常時,可以被這裡捕獲到。
以上是自己瞭解到並且親自測試可行的全域性異常統一處理方案,如果各位博友有什麼問題或者有什麼新的方案可以一塊探討下