Spring項目中優雅的異常處理

前言

現在的Java Web項目可能是以 MVC 模式構建的,一般咱們都是將 Service 層的異常統一的拋出,包括自定義異常和一些意外出現的異常,以便進行事務回滾,而 Service 的調用者 Controller 則承擔着異常處理的責任,由於他是與 Web 前端交互的最後一道防線,若是此時還不進行處理則用戶會在網頁上看到一臉懵逼的前端

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4
    at cn.keats.TestAdd.main(TestAdd.java:20)

這樣作有如下幾點壞處:java

  1. 用戶體驗很不友好,可能用戶會吐槽一句:這是什麼XX網站。而後再也不訪問了
  2. 若是這個用戶是同行,他不只看到了項目代碼的結構,並且看到拋出的是這麼低級的索引越界異常,會被人家看不起
  3. 用戶看到網站有問題,打電話給客服,客服找到產品,產品叫醒正在熟睡/打遊戲的你。你不只睡很差遊戲打不了還得挨批評完事改代碼

哎,真慘。所以通常咱們採用的方法會是像這樣:程序員

異常處理

通常的Controller處理

Service代碼以下:web

@Service
public class DemoService {
    public String respException(String param){
        if(StringUtils.isEmpty(param)){
            throw new MyException(ExceptionEnum.PARAM_EXCEPTION);
        }
        int i = 1/0;
        return "你看不見我!";
    }
}

Controller代碼以下:spring

@RestController
public class DemoController {
    @Autowired
    private DemoService demoService;
    
    @PostMapping("respException")
    public Result respException(){
        try {
            return Result.OK(demoService.respException(null)); 
        } catch (MyException e){
            return Result.Exception(e, null);
        }
        catch (Exception e) {
            return Result.Error();
        }
    } 
}

若是此時發送以下的請求:編程

http://localhost/respException

服務器捕捉到自定義的異常 MyException,而返回參數異常的Json串:json

{
    "code": 1,
    "msg": "參數異常",
    "data": null
}

而當咱們補上參數:服務器

http://localhost/respException?param=zhangsan

則服務器捕捉到 by zero 異常,會返回未知錯誤到前端頁面app

{
    "code": -1,
    "msg": "未知錯誤",
    "data": null
}

這樣就會在必定程度上規避一些問題,例如參數錯誤就可讓用戶去修改其參數,固然這通常須要前端同窗配合作頁面的參數校驗,必傳參數都有的時候再向服務器發送請求,一方面減輕服務器壓力,一方面將問題前置節省雙方的時間。可是這樣寫有一個壞處就是全部的Controller方法中關於異常的部分都是同樣的,代碼很是冗餘。且不利於維護,並且一些不太熟悉異常機制的同窗可能會像踢皮球同樣將異常抓了拋,拋完又抓回來,鬧着玩呢。。。(筆者就曾經接手過一個跑路同窗的代碼這樣處理異常,那簡直是跟異常捉迷藏呢!可恨)咱們在Service有全局事務處理,在系統中能夠有全局的日誌處理,這些都是基於Spring 的一大殺器:AOP(面向切面編程) 實現的,AOP是什麼呢?框架

AOP

AOP是Spring框架面向切面的編程思想,AOP採用一種稱爲「橫切」的技術,將涉及多業務流程的通用功能抽取並單獨封裝,造成獨立的切面,在合適的時機將這些切面橫向切入到業務流程指定的位置中。若是說咱們經常使用的OOP思想是從上到下執行業務流程的話,AOP就至關於在咱們執行業務的時候橫切一刀,以下圖所示:

image-20191201205830708

而Advice(通知)是AOP思想中重要的一個術語,分爲前置通知(Before)、後置通知(AfterReturning)、異常通知(AfterThrowing)、最終通知(After)和環繞通知(Around)五種。具體通知所表示的意義我這裏很少贅述,網上關於Spring核心原理的講解都會說起。而咱們熟知的 Service 事務處理其實就是基於AOP AfterThrowing 通知實現的事務回滾。咱們自定義的日誌處理也能夠根據不一樣的需求定製不一樣的通知入口。那既然如此,咱們爲什麼不自定義一個全局異常處理的切面去簡化咱們的代碼呢?別急,且繼續向下看。

優雅的處理異常

Spring 在 3.2 版本已經爲咱們提供了該功能: @ControllerAdvice 註解。此註解會捕捉Controller層拋出的異常,並根據 @ExceptionHandler 註解配置的方法進行異常處理。下面是一個示例工程,主要代碼以下:

Result類:

此 Result 採用泛型的方式,便於在 Swagger 中配置方法的出參。使用靜態工廠方法是的對象的初始化更加見名只意。對於不存在共享變量問題的 Error 對象,採用雙重校驗鎖懶漢單例模式來節省服務器資源(固然最好仍是整個項目運行中一直沒有初始化它讓人更加舒服。)

package cn.keats.util;

import cn.keats.exception.MyException;
import lombok.Data;

/**
 * 功能:統一返回結果,直接調用對應的工廠方法
 *
 * @author Keats
 * @date 2019/11/29 18:20
 */
@Data
public class Result<T>  {
    private Integer code;
    private String msg;
    private T data;

    /**
     * 功能:響應成功
     *
     * @param data 響應的數據
     * @return woke.cloud.property.transformat.Result
     * @author Keats
     * @date 2019/11/30 8:54
     */
    public static <T> Result<T> OK(T data){
        return new Result<>(0, "響應成功", data);
    }

    private static Result errorResult;
    /**
     * 功能:返回錯誤,此錯誤不可定製,全局惟一。通常是代碼出了問題,須要修改代碼
     *
     * @param
     * @return Result
     * @author Keats
     * @date 2019/11/30 8:55
     */
    public static Result Error(){
        if(errorResult == null){
            synchronized (Result.class){
                if(errorResult == null){
                    synchronized (Result.class){
                        errorResult = new Result<>(-1, "未知錯誤", null);
                    }
                }
            }
        }
        return errorResult;
    }

    /**
     * 功能:返回異常,直接甩自定義異常類進來
     *
     * @param e 自定義異常類
     * @param data 數據,若是沒有填入 null 便可
     * @return woke.cloud.property.transformat.Result<T>
     * @author Keats
     * @date 2019/11/30 8:55
     */
    public static <T> Result<T> Exception(MyException e, T data){
        return new Result<>(e.getCode(), e.getMsg(), data);
    }

    /**
     * 功能:爲了方便使用,使用靜態工廠方法建立對象。如需新的構造方式,請添加對應的靜態工廠方法
     *
     * @author Keats
     * @date 2019/11/30 8:56
     */
    private Result(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}

自定義異常類:

package cn.keats.exception;

import lombok.Getter;

/**
 * 功能:系統自定義異常類。繼承自RuntimeException,方便Spring進行事務回滾
 *
 * @author Keats
 * @date 2019/11/29 18:50
 */
@Getter
public class MyException extends RuntimeException{
    private Integer code;
    private String msg;

    public MyException(ExceptionEnum eEnum) {
        this.code = eEnum.getCode();
        this.msg = eEnum.getMsg();
    }
}

異常代碼枚舉類:

package cn.keats.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 功能:異常枚舉
 *
 * @author Keats
 * @date 2019/11/29 18:49
 */
@Getter
@AllArgsConstructor
public enum ExceptionEnum {
    PARAM_EXCEPTION(1,"參數異常"),
    USER_NOT_LOGIN(2,"用戶未登陸"),
    FILE_NOT_FOUND(3,"文件不存在,請從新選擇");


    private Integer code;
    private String msg;
}

異常切面:

其中 @RestControllerAdvice 是spring 4.3 添加的新註解,是 @ControllerAdvice 和 @ResponseBody 的簡寫方式,相似與 @RestController 與 @Controller 的關係

package cn.keats.advice;

import cn.keats.exception.MyException;
import cn.keats.util.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 功能:全局異常處理器,Controller異常直接拋出
 *
 * @return
 * @author Keats
 * @date 2019/11/30 10:28
 */
@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {
    /**
     * 功能:其他非預先規避的異常返回錯誤
     *
     * @param e
     * @return woke.cloud.property.transformat.Result
     * @author Keats
     * @date 2019/11/30 10:08
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Result ResponseException(Exception e) {
        log.error("未知錯誤,錯誤信息:", e);
        return Result.Error();
    }

    /**
     * 功能:捕捉到 MyException 返回對應的消息
     *
     * @param e
     * @return woke.cloud.property.transformat.Result
     * @author Keats
     * @date 2019/11/30 10:07
     */
    @ExceptionHandler(value = MyException.class)
    @ResponseBody
    public Result myException(MyException e) {
        log.info("返回自定義異常:異常代碼:" + e.getCode() + "異常信息:" + e.getMsg());
        return Result.Exception(e, null);
    }
}

此時的 Controller 方法能夠這樣寫:

package cn.keats.controller;

import cn.keats.service.DemoService;
import cn.keats.util.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {
    @Autowired
    private DemoService demoService;

    @PostMapping("respException")
    public Result respException(String param) throws Exception {
        return Result.OK(demoService.respException(param));
    }
    
    @PostMapping("respError")
    public Result respError() throws Exception {
        return Result.OK(demoService.respException(null));
    }
}

省略的大部分的異常處理代碼,使得咱們只須要關注業務,一方面提升了代碼質量,可閱讀性,另外一方面也提升了咱們的開發速度。美哉!

啓動項目,進行測試沒有問題。

image-20191201213847490

image-20191201213912251

我是 Keats,一個熱愛技術的程序員,鑑於技術有限,若是本文有什麼紕漏或者兄臺還有其餘更好的建議/實現方式,歡迎留言評論,謝謝您!

相關文章
相關標籤/搜索