Springboot項目全局異常統一處理


轉自https://blog.csdn.net/hao_kkkkk/article/details/80538955

最近在做項目時需要對異常進行全局統一處理,主要是一些分類入庫以及記錄日志等,因為項目是基於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;
}
}
經過測試,在執行切點中配置的路徑中的方法有異常時,可以被這里捕獲到。

 

以上是自己了解到並且親自測試可行的全局異常統一處理方案,如果各位博友有什么問題或者有什么新的方案可以一塊探討下

 

 

2018/11/28最新編輯

經過一段時間的使用,現在項目里已經統一使用AOP方式來做全局異常統一處理了,選用AOP方式主要是因為AOP不只可以做全局異常統一處理還可以統一打印接口請求入參和返回結果日志,打印接口訪問性能日志,處理sql注入攻擊以及處理入參特殊字符等問題

下面貼出代碼,供大家參考,也僅供參考

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Lists;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
* @Author: gmy
* @Description: 調用接口打印性能日志以及接口報錯之后記錄錯誤日志
* @Date: 2018/9/20
* @Time: 15:16
*/
@Component
@Aspect
public class InterfaceRequestErrrorAndPerformanceLog {

public static final Logger logger = LoggerFactory.getLogger(InterfaceRequestErrrorAndPerformanceLog.class);

@Value("${dc.log.bad.value:3000}")
private int performanceBadValue;

@Resource
private RabbitMQService rabbitMQService;
@Resource
private InterfaceErrorService interfaceErrorService;

@Pointcut("execution(* test.test.test.test.test.controller.*.*.*(..))")
public void pointCut(){}

@Around("pointCut()")
public APIResponse handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable{
Stopwatch stopwatch = Stopwatch.createStarted();

APIResponse apiResponse;
try {
logger.info("執行Controller開始: " + pjp.getSignature() + " 參數:" + Lists.newArrayList(pjp.getArgs()).toString());
//處理入參特殊字符和sql注入攻擊
checkRequestParam(pjp);
//執行訪問接口操作
apiResponse = (APIResponse) pjp.proceed(pjp.getArgs());
try{
logger.info("執行Controller結束: " + pjp.getSignature() + ", 返回值:" + JSONObject.toJSONString(apiResponse));
//此處將日志打印放入try-catch是因為項目中有些對象實體bean過於復雜,導致序列化為json的時候報錯,但是此處報錯並不影響主要功能使用,只是返回結果日志沒有打印,所以catch中也不做拋出異常處理
}catch (Exception ex){
logger.error(pjp.getSignature()+" 接口記錄返回結果失敗!,原因為:{}",ex.getMessage());
}
Long consumeTime = stopwatch.stop().elapsed(TimeUnit.MILLISECONDS);
logger.info("耗時:" + consumeTime + "(毫秒).");
//當接口請求時間大於3秒時,標記為異常調用時間,並記錄入庫
if(consumeTime > performanceBadValue){
DcPerformanceEntity dcPerformanceEntity = new DcPerformanceEntity();
dcPerformanceEntity.setInterfaceName(pjp.getSignature().toString());
dcPerformanceEntity.setRequestParam(Lists.newArrayList(pjp.getArgs()).toString());
dcPerformanceEntity.setConsumeTime(consumeTime + "毫秒");
RabbitMQMessageTarget mqTarget = RabbitMQMessageTarget.createFanoutTarget(ProjectConstants.DC_KEY_EXCHANGE_PERFORMANCE, new String[] { ProjectConstants.DC_KEY_QUEUE_PERFORMANCE});
rabbitMQService.send(mqTarget, JSON.toJSONString(dcPerformanceEntity));
}
} catch (Exception throwable) {
apiResponse = handlerException(pjp, throwable);
}

return apiResponse;
}

/**
* @Author: gmy
* @Description: 處理接口調用異常
* @Date: 15:13 2018/10/25
*/
private APIResponse handlerException(ProceedingJoinPoint pjp, Throwable e) {
APIResponse apiResponse;
if(e.getClass().isAssignableFrom(ProjectException.class) ){
//ProjectException為自定義異常類,項目中Controller層會把所有的異常都catch掉,並手工封裝成ProjectException拋出來,這樣做的目的是ProjectException會記錄拋出異常接口的路徑,名稱以及請求參數等等,有助於錯誤排查
ProjectException projectException = (ProjectException)e;
logger.error("捕獲到ProjectException異常:",JSONObject.toJSONString(projectException.getDcErrorEntity()));
RabbitMQMessageTarget mqTarget = RabbitMQMessageTarget.createFanoutTarget(ProjectConstants.DC_KEY_EXCHANGE_INTERFACE_ERROR, new String[] { ProjectConstants.DC_KEY_QUEUE_INTERFACE_ERROR});
rabbitMQService.send(mqTarget, JSON.toJSONString(dataCenterException.getDcErrorEntity()));
apiResponse = new APIResponse(APIResponse.FAIL,null,projectException.getDcErrorEntity().getErrorMessage());
} 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;
}

/**
* @Author: gmy
* @Description: 處理入參特殊字符和sql注入攻擊
* @Date: 15:37 2018/10/25
*/
private void checkRequestParam(ProceedingJoinPoint pjp){
String str = String.valueOf(pjp.getArgs());
if (!IllegalStrFilterUtil.sqlStrFilter(str)) {
logger.info("訪問接口:" + pjp.getSignature() + ",輸入參數存在SQL注入風險!參數為:" + Lists.newArrayList(pjp.getArgs()).toString());
DcErrorEntity dcErrorEntity = interfaceErrorService.processDcErrorEntity(pjp.getSignature() + "",Lists.newArrayList(pjp.getArgs()).toString(),"輸入參數存在SQL注入風險!");
throw new DataCenterException(dcErrorEntity);
}
if (!IllegalStrFilterUtil.isIllegalStr(str)) {
logger.info("訪問接口:" + pjp.getSignature() + ",輸入參數含有非法字符!,參數為:" + Lists.newArrayList(pjp.getArgs()).toString());
DcErrorEntity dcErrorEntity = interfaceErrorService.processDcErrorEntity(pjp.getSignature() + "",Lists.newArrayList(pjp.getArgs()).toString(),"輸入參數含有非法字符!");
throw new DataCenterException(dcErrorEntity);
}
}

}
 

代碼中使用了一些其他的工具類,比如IllegalStrFilterUtil等,我也把代碼貼出來

import org.slf4j.LoggerFactory;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* @Author: gmy
* @Description: 特殊字符檢測工具(防止傳入非法字符和sql注入攻擊)
* @Date: 2018/10/25
* @Time: 15:08
*/
public class IllegalStrFilterUtil {
private static final org.slf4j.Logger Logger = LoggerFactory.getLogger(IllegalStrFilterUtil.class);

private static final String REGX = "!|!|@|◎|#|#|(\\$)|¥|%|%|(\\^)|……|(\\&)|※|(\\*)|×|(\\()|(|(\\))|)|_|——|(\\+)|+|(\\|)|§ ";

/**
* 對常見的sql注入攻擊進行攔截
*
* @param sInput
* @return
* true 表示參數不存在SQL注入風險
* false 表示參數存在SQL注入風險
*/
public static Boolean sqlStrFilter(String sInput) {
if (sInput == null || sInput.trim().length() == 0) {
return false;
}
sInput = sInput.toUpperCase();

if (sInput.indexOf("DELETE") >= 0 || sInput.indexOf("ASCII") >= 0 || sInput.indexOf("UPDATE") >= 0 || sInput.indexOf("SELECT") >= 0
|| sInput.indexOf("'") >= 0 || sInput.indexOf("SUBSTR(") >= 0 || sInput.indexOf("COUNT(") >= 0 || sInput.indexOf(" OR ") >= 0
|| sInput.indexOf(" AND ") >= 0 || sInput.indexOf("DROP") >= 0 || sInput.indexOf("EXECUTE") >= 0 || sInput.indexOf("EXEC") >= 0
|| sInput.indexOf("TRUNCATE") >= 0 || sInput.indexOf("INTO") >= 0 || sInput.indexOf("DECLARE") >= 0 || sInput.indexOf("MASTER") >= 0) {
Logger.error("該參數怎么SQL注入風險:sInput=" + sInput);
return false;
}
Logger.info("通過sql檢測");
return true;
}

/**
* 對非法字符進行檢測
*
* @param sInput
* @return
* true 表示參數不包含非法字符
* false 表示參數包含非法字符
*/
public static Boolean isIllegalStr(String sInput) {

if (sInput == null || sInput.trim().length() == 0) {
return false;
}
sInput = sInput.trim();
Pattern compile = Pattern.compile(REGX, Pattern.CASE_INSENSITIVE);
Matcher matcher = compile.matcher(sInput);
Logger.info("通過字符串檢測");
return matcher.find();
}
}
以上代碼中涉及到真實項目信息的內容我都做了相應修改,代碼僅供技術交流使用。
---------------------
作者:hao_kkkkk
來源:CSDN
原文:https://blog.csdn.net/hao_kkkkk/article/details/80538955
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM