1. 程式人生 > >Springboot專案全域性異常統一處理

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;
    }
}

經過測試,在執行切點中配置的路徑中的方法有異常時,可以被這裡捕獲到。

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