前景描述: 最近在做項目時發現背景程式的異常抛到前端頁面上報出一大段sql異常,是以考慮需要對異常進行全局統一處理,并将日志進行分類入庫以及記錄接口請求的日志資訊等,記錄日志資訊在之前的文章已經有記錄了這裡不再重複有需要的請移步到Spring Boot 使用AOP切面實作背景日志管理子產品。
因為項目是基于Springboot做的前後端分離的項目,需要結合項目本身的一些特殊需求做些許改造。
在網絡上講述的大緻這兩種方案:
1、基于@ControllerAdvice注解的Controller層的全局異常統一處理(最常用,使用較多)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.isoftstone.common.utils.web.JsonResult;
import com.isoftstone.common.utils.web.StatusUtil;
/**
* 全局異常處理類
* @ClassName::ControllerExceptionHandler
* @author leon
* @createDate 2018年9月20日 下午3:39:19
* @version v1.0
* @classRemarks TODO
*/
@ControllerAdvice
public class ControllerExceptionHandler {
private static Logger logger = LoggerFactory.getLogger(ControllerExceptionHandler.class);
@ExceptionHandler(Exception.class)
@ResponseBody
public JsonResult handleException(Exception e){
//異常日志入庫
logger.info("=================exception===============");
e.printStackTrace();
return new JsonResult(StatusUtil.ERROR,"系統正忙,請稍後在試......");
}
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public JsonResult handleException(RuntimeException e){
//異常日志入庫
logger.info("=================runtime exception===============");
e.printStackTrace();
return new JsonResult(StatusUtil.ERROR,"小服正在努力嘗試重連,請稍後在試......");
}
@ExceptionHandler(SaveRuntimeException.class)
@ResponseBody
public JsonResult handleException(SaveRuntimeException e){
//異常日志入庫
logger.info("=================Save exception===============");
e.printStackTrace();
return new JsonResult(StatusUtil.ERROR,"新增資料失敗,請稍後在試......");
}
@ExceptionHandler(UpdateRuntimeException.class)
@ResponseBody
public JsonResult handleException(UpdateRuntimeException e){
//異常日志入庫
logger.info("=================Update exception===============");
e.printStackTrace();
return new JsonResult(StatusUtil.ERROR,"更新資料失敗,請稍後在試......");
}
}
/**
* 資料新增時異常
* @author Leon
*
*/
public class SaveRuntimeException extends RuntimeException {
private static final long serialVersionUID = 2323446669041126322L;
public SaveRuntimeException() {
super();
}
public SaveRuntimeException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public SaveRuntimeException(String message, Throwable cause) {
super(message, cause);
}
public SaveRuntimeException(String message) {
super(message);
}
public SaveRuntimeException(Throwable cause) {
super(cause);
}
}
/**
* 資料更新時的異常
* @author Loen
*
*/
public class UpdateRuntimeException extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = 18794756827824687L;
public UpdateRuntimeException() {
super();
}
public UpdateRuntimeException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public UpdateRuntimeException(String message, Throwable cause) {
super(message, cause);
}
public UpdateRuntimeException(String message) {
super(message);
}
public UpdateRuntimeException(Throwable cause) {
super(cause);
}
}
需注意的是 SaveRuntimeException 和 UpdateRuntimeException是我自定義的異常處理類,基于RuntimeException處理的,
這個代碼示例寫的非常淺顯易懂,但是需要注意的是:基于@ControllerAdvice注解的全局異常統一處理隻能針對于Controller層的異常,意思是隻能捕獲到Controller層的異常,在service層或者其他層面的異常不能捕獲,當然也可以通過自定義來實作,那我們就來說說第二種方式.
2、基于Springboot自身的全局異常統一處理,主要是實作ErrorController接口或者繼承AbstractErrorController抽象類或者繼承BasicErrorController類, 以下是網上一位部落客給出的示例代碼
@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: leon
* @Description: Springboot全局異常統一處理
* @Date: 2018/9/20
* @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也可以實作異常的全局處理,自己相應的做了測試發現也滿足要求,相應的代碼如下:
@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;
}
}
經過測試,在執行切點中配置的路徑中的方法有異常時,可以被這裡捕獲到。