1. 程式人生 > >Springboot異常資訊統一處理

Springboot異常資訊統一處理

前景描述: 最近在做專案時發現後臺程式的異常拋到前端頁面上報出一大段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,"更新資料失敗,請稍後在試......");
    }
}

需注意的是 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;
    }
}
經過測試,在執行切點中配置的路徑中的方法有異常時,可以被這裡捕獲到。