Spring Boot統一處理全局異常的實戰教程

註解的介紹

@ControllerAdvice

@ControllerAdvice註解是Spring3.2中新增的註解,學名是Controller增強器,作用是給Controller控制器添加統一的操作或處理。

這裡ControllerAdvice也可以這麼理解,其抽象級別應該是用於對Controller進行切面環繞的,而具體的業務織入方式則是通過結合其他的註解來實現的。@ControllerAdvice是在類上聲明的註解,其用法主要有三點:

1.結合方法型註解@ExceptionHandler,用於捕獲Controller中拋出的指定類型的異常,從而達到不同類型的異常區別處理的目的。

2.結合方法型註解@InitBinder,用於request中自定義參數解析方式進行註冊,從而達到自定義指定格式參數的目的。

3.結合方法型註解@ModelAttribute,表示其註解的方法將會在目標Controller方法執行之前執行。

從上面的講解可以看出,@ControllerAdvice的用法基本是將其聲明在某個bean上,然後在該bean的方法上使用其他的註解來指定不同的織入邏輯。不過這裡@ControllerAdvice並不是使用AOP的方式來織入業務邏輯的,而是Spring內置對其各個邏輯的織入方式進行瞭內置支持。

針對聲明@ExceptionHandler 、 @InitBinder或@ModelAttribute方法的類的@Component @ExceptionHandler , @InitBinder在多個@Controller類之間共享。

使用@ControllerAdvice註解的類可以明確聲明為 Spring bean 或通過類路徑掃描自動檢測。 所有此類 bean 都根據Ordered語義或@Order / @Priority聲明進行Ordered , Ordered語義優先於@Order / @Priority聲明。 然後在運行時按該順序應用@ControllerAdvice bean。 但是請註意,實現PriorityOrdered @ControllerAdvice bean 的PriorityOrdered不高於實現Ordered @ControllerAdvice bean。 此外, Ordered不適用於范圍內的@ControllerAdvice例如,如果這樣的 bean 已被配置為請求范圍或會話范圍的 bean。 對於處理異常, @ExceptionHandler將在第一個具有匹配異常處理程序方法的通知中被選擇。 對於模型的屬性和數據綁定初始化, @ModelAttribute和@InitBinder方法將遵循@ControllerAdvice秩序。

註意:對於@ExceptionHandler方法,在特定建議 bean 的處理程序方法中,根異常匹配將優先於僅匹配當前異常的原因。 但是,與較低優先級建議 bean 上的任何匹配(無論是根還是原因級別)相比,更高優先級建議上的原因匹配仍然是首選。 因此,請在具有相應順序的優先建議 bean 上聲明您的主要根異常映射。

默認情況下, @ControllerAdvice ControllerAdvice 中的方法全局應用於所有控制器。 使用諸如annotations 、 basePackageClasses和basePackages (或其別名value )之類的選擇器來定義目標控制器的更窄子集。 如果聲明瞭多個選擇器,則應用佈爾OR邏輯,這意味著所選控制器應至少匹配一個選擇器。 請註意,選擇器檢查是在運行時執行的,因此添加許多選擇器可能會對性能產生負面影響並增加復雜性。

@ExceptionHandler攔截異常並統一處理

配合 @ExceptionHandler註解結合使用,當異常拋到controller層時,可以對異常進行統一的處理,規定返回的json格式或者跳轉到指定的錯誤頁面等.

@ExceptionHandler的作用主要在於聲明一個或多個類型的異常,當符合條件的Controller拋出這些異常之後將會對這些異常進行捕獲,然後按照其標註的方法的邏輯進行處理,從而改變返回的視圖信息。

用於處理特定處理程序類和/或處理程序方法中的異常的註解。

使用此註解註釋的處理程序方法允許具有非常靈活的簽名。 它們可能具有以下類型的參數,按任意順序排列:

異常參數:聲明為一般異常或更具體的異常。 如果註解本身沒有通過其value()縮小異常類型,這也可用作映射提示

代碼實現

自定義異常

/**
 * 自定義一個異常類,用於處理我們發生的業務異常
 *
 * @author Promsing(張有博)
 * @version 1.0.0
 * @since 2021/11/27 - 20:14
 */
public class BizException extends RuntimeException {
 
    private static final long serialVersionUID = 1L;
 
    /**
     * 錯誤碼
     */
    protected String errorCode;
    /**
     * 錯誤信息
     */
    protected String errorMsg;
 
    public BizException() {
        super();
    }
 
    public BizException(FrontResult errorInfoInterface) {
        super(errorInfoInterface.getCode());
        this.errorCode = errorInfoInterface.getMessage();
        this.errorMsg = errorInfoInterface.getMessage();
    }
 
    public BizException(FrontResult errorInfoInterface, Throwable cause) {
        super(errorInfoInterface.getCode(), cause);
        this.errorCode = errorInfoInterface.getCode();
        this.errorMsg = errorInfoInterface.getMessage();
    }
 
    public BizException(String errorMsg) {
        super(errorMsg);
        this.errorMsg = errorMsg;
    }
 
    public BizException(String errorCode, String errorMsg) {
        super(errorCode);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }
 
    public BizException(String errorCode, String errorMsg, Throwable cause) {
        super(errorCode, cause);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }
 
 
    public String getErrorCode() {
        return errorCode;
    }
 
    public void setErrorCode(String errorCode) {
        this.errorCode = errorCode;
    }
 
    public String getErrorMsg() {
        return errorMsg;
    }
 
    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }
 
    public String getMessage() {
        return errorMsg;
    }
 
    @Override
    public Throwable fillInStackTrace() {
        return this;
    }
 
}

統一異常處理

 
import com.tfjy.arbackend.enumtool.ResultCodeEnum;
import com.tfjy.arbackend.enumtool.ResutlMsgEnum;
import com.tfjy.arbackend.util.FrontResult;
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 javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.sql.SQLException;
 
/**
 * 統一異常處理
 *
 * @author Promsing(張有博)
 * @version 1.0.0
 * @since 2021/11/27 - 20:14
 */
@ControllerAdvice//使用該註解表示開啟瞭全局異常的捕獲
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
 
    /**
     * 處理自定義的業務異常
     * @param req
     * @param e
     * @return
     */
    @ExceptionHandler(value = BizException.class)
    @ResponseBody
    public  FrontResult bizExceptionHandler(HttpServletRequest req, BizException e){
        logger.error("URL : " + req.getRequestURL().toString());
        logger.error("HTTP_METHOD : " + req.getMethod());
        logger.error("發生業務異常!原因是:{}",e.getErrorMsg());
        return FrontResult.getExceptionResult(e.getErrorCode(),e.getErrorMsg());
    }
 
    /**
     * 處理空指針的異常
     * @param req
     * @param e
     * @return
     */
    @ExceptionHandler(value =NullPointerException.class)
    @ResponseBody
    public FrontResult exceptionHandler(HttpServletRequest req, NullPointerException e)  {
        logger.error("URL : " + req.getRequestURL().toString());
        logger.error("HTTP_METHOD : " + req.getMethod());
        logger.error("發生空指針異常!原因是:",e);
        return FrontResult.getExceptionResult(ResultCodeEnum.FAIL.getCode(), ResutlMsgEnum.EXECUTE_FAIL.getMsg());
    }
 
    /**
     * 處理索引越界異常
     * @param req
     * @param e
     * @return
     */
    @ExceptionHandler(value =IndexOutOfBoundsException.class)
    @ResponseBody
    public FrontResult exceptionHandler(HttpServletRequest req, IndexOutOfBoundsException e){
        logger.error("URL : " + req.getRequestURL().toString());
        logger.error("HTTP_METHOD : " + req.getMethod());
        logger.error("索引越界異常!原因是:",e);
        return FrontResult.getExceptionResult(ResultCodeEnum.FAIL.getCode(), ResutlMsgEnum.EXECUTE_FAIL.getMsg());
    }
 
    /**
     * 處理類未找到異常
     * @param req
     * @param e
     * @return
     */
    @ExceptionHandler(value =ClassNotFoundException.class)
    @ResponseBody
    public FrontResult exceptionHandler(HttpServletRequest req, ClassNotFoundException e)  {
        logger.error("URL : " + req.getRequestURL().toString());
        logger.error("HTTP_METHOD : " + req.getMethod());
        logger.error("發生類未找到異常!原因是:",e);
        return FrontResult.getExceptionResult(ResultCodeEnum.FAIL.getCode(), ResutlMsgEnum.EXECUTE_FAIL.getMsg());
    }
 
 
    /**
     * 處理SQL異常
     * @param req
     * @param e
     * @return
     */
    @ExceptionHandler(value = SQLException.class)
    @ResponseBody
    public FrontResult exceptionHandler(HttpServletRequest req, SQLException e)  {
        logger.error("URL : " + req.getRequestURL().toString());
        logger.error("HTTP_METHOD : " + req.getMethod());
        logger.error("發生SQL異常!原因是:",e);
        return FrontResult.getExceptionResult(ResultCodeEnum.FAIL.getCode(), ResutlMsgEnum.EXECUTE_FAIL.getMsg());
    }
 
    /**
     * 處理IO異常
     * @param req
     * @param e
     * @return
     */
    @ExceptionHandler(value = IOException.class)
    @ResponseBody
    public FrontResult exceptionHandler(HttpServletRequest req, IOException e)  {
        logger.error("URL : " + req.getRequestURL().toString());
        logger.error("HTTP_METHOD : " + req.getMethod());
        logger.error("發生IO異常!原因是:",e);
        return FrontResult.getExceptionResult(ResultCodeEnum.FAIL.getCode(), ResutlMsgEnum.EXECUTE_FAIL.getMsg());
    }
 
 
    /**
     * 處理其他異常
     * @param req
     * @param e
     * @return
     */
    @ExceptionHandler(value =Exception.class)
    @ResponseBody
    public FrontResult exceptionHandler(HttpServletRequest req, Exception e){
        logger.error("URL : " + req.getRequestURL().toString());
        logger.error("HTTP_METHOD : " + req.getMethod());
        logger.error("未知異常!原因是:",e);
        return FrontResult.getExceptionResult(ResultCodeEnum.FAIL.getCode(), ResutlMsgEnum.EXECUTE_FAIL.getMsg());
    }
}

前端返回值類

 
 
import com.tfjy.arbackend.enumtool.ResultCodeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
 
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FrontResult {
    /**
     * 結果狀態碼
     */
    private String code;
    /**
     * 響應結果描述
     */
    private String message;
    /**
     * 返回數據
     */
    private Object data;
 
    /**
     * 靜態方法,返回前端實體結果
     *
     * @param code    狀態碼
     * @param message 消息
     * @param data    數據
     * @return 前端實體結果
     */
    public static FrontResult build(String code, String message, Object data) {
        return new FrontResult(code, message, data);
    }
 
    /**
     * 返回成功的結果實體
     *
     * @param message 消息
     * @param data    數據
     * @return 實體
     */
    public static FrontResult getSuccessResult(String message, Object data) {
        FrontResult result = new FrontResult();
        result.code = ResultCodeEnum.SUCCESS.getCode();
        result.message = message;
        result.data = data;
        return result;
    }
 
    /**
     * 返回無需data的成功結果實體
     *
     * @param message 消息內容
     * @return 返回結果
     */
    public static FrontResult getSuccessResultOnlyMessage(String message) {
        FrontResult result = new FrontResult();
        result.code = ResultCodeEnum.SUCCESS.getCode();
        result.message = message;
        result.data = null;
        return result;
    }
 
    /**
     * 獲取一個異常結果
     *
     * @param code 錯誤碼
     * @param message 自定義異常信息
     * @return FrontResult
     */
    public static FrontResult getExceptionResult(String code, String message) {
        FrontResult result = new FrontResult();
        result.code = code.isEmpty() ? ResultCodeEnum.CODE_EXCEPTION.getCode() : code;
        result.message = message.isEmpty() ? ResultCodeEnum.CODE_EXCEPTION.getMsg() : message;
        return result;
    }
}
import lombok.AllArgsConstructor;
 
@AllArgsConstructor
public enum ResultCodeEnum {
    // 請求成功
    SUCCESS("0000"),
    // 請求失敗
    FAIL("1111"),
    // EXCEL 導入失敗
    EXCEL_FAIL("1000"),
    // userID 為空
    ID_NULL("1001"),
    // 前端傳的實體為空
    MODEL_NULL("1002"),
    // 更新失敗
    UPDATE_FAIL("1011"),
    // 參數為空
    PARAM_ERROR("400"),
    // 代碼內部異常
    CODE_EXCEPTION("500", "代碼內部異常");
 
    /**
     * 狀態碼
     */
    private String code;
 
    public String getCode() {
        return code;
    }
 
    ResultCodeEnum(String code) {
        this.code = code;
    }
 
    private String msg;
 
    public String getMsg() {
        return msg;
    }
 
}
 
 
public enum ResutlMsgEnum {
 
    //查詢成功
    FIND_SUCCESS("查詢成功!"),
    //查詢失敗
    FIND_FAIL("查詢失敗!"),
 
    //更新成功
    UPDATE_SUCCESS("更新成功"),
    //更新失敗
    UPDATE_FAIL("更新成功"),
   
    SEND_SUCCESS("發送成功"),
 
    SEND_FAIL("發送失敗");
 
 
    private String msg;
 
    ResutlMsgEnum(String msg) {
        this.msg = msg;
    }
 
    public String getMsg() {
        return msg;
    }
}

測試用例

/**
 * 測試用例
 *
 * @author Promsing(張有博)
 * @version 1.0.0
 * @since 2021/11/29 - 9:05
 */
@Api(tags = {"測試controller"})
@RequestMapping(value = "/testController")
@RestController
public class TestController {
 
   
    @ApiOperation(value = "測試null")
    @GetMapping(value = "getNull")
    public FrontResult getNull() {
        int length = 0;
 
            String name=null;
            length = name.length();
 
 
        return FrontResult.build(ResultCodeEnum.SUCCESS.getCode(),
                ResutlMsgEnum.EXECUTE_SUCCESS.getMsg(), length);
    }
}

附:Spring Boot默認的異常處理機制

默認情況下,Spring Boot為兩種情況提供瞭不同的響應方式。

一種是瀏覽器客戶端請求一個不存在的頁面或服務端處理發生異常時,一般情況下瀏覽器默認發送的請求頭中Accept: text/html,所以Spring Boot默認會響應一個html文檔內容,稱作“Whitelabel Error Page”。

另一種是使用Postman等調試工具發送請求一個不存在的url或服務端處理發生異常時,Spring Boot會返回類似如下的Json格式字符串信息

{
    "timestamp": "2018-05-12T06:11:45.209+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/index.html"
} 

原理也很簡單,Spring Boot 默認提供瞭程序出錯的結果映射路徑/error。這個/error請求會在BasicErrorController中處理,其內部是通過判斷請求頭中的Accept的內容是否為text/html來區分請求是來自客戶端瀏覽器(瀏覽器通常默認自動發送請求頭內容Accept:text/html)還是客戶端接口的調用,以此來決定返回頁面視圖還是 JSON 消息內容。

相關BasicErrorController中代碼如下:

總結

其他異常同理,也可以捕獲。完美,沒問題。全局統一異常處理設置成功。

到此這篇關於Spring Boot統一處理全局異常的文章就介紹到這瞭,更多相關SpringBoot統一處理全局異常內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: