天天看點

TP5_接口開發之全局異常控制

前言:

說到異常控制,也許很多會比較陌生,我身邊很少人會去寫抛異常的代碼。但是異常用好了是非常的友善大家開發。首先我們來回顧下哪裡可以看到異常,首先我們用架構開發的時候,我們的代碼出錯或者别的東西。如果開啟調試模式的話,浏覽器頁面會報出錯誤的位置,還有調用的順序,甚至還有記憶體的使用等等很多資訊。這就是架構在捕獲異常時候,将這些資料擷取然後渲染了一套html,才讓我們這麼直覺的看到錯誤。那麼既然開發架構的開發者為了友善我們使用,使用了抛出異常,捕獲異常。我們也可以照貓畫虎,來學習下。

抛出異常

在捕獲異常之前我們先來看看抛出異常。

雖然可能不少朋友不常用抛出異常,可是抛出異常的方法,大家一定不會陌生

throw new Exception('錯誤');
           

沒錯,使用throw指令 後面跟個new Exception 就抛出了。其實大家仔細觀察發現,其實這個new Exception其實就是 執行個體化了一個類的對象。那麼抛出異常的本質,實際上就是 throw 一個異常類的對象。

那麼怎麼樣才算是一個異常類呢?

我們平時抛出最多的異常類就是think\Exception 這是一個tp封裝的一個異常類。

image.png

我們發現這個異常類繼承一個基礎異常類,由此可知,隻有直接繼承或鍊式繼承這個最最最基礎的Exception類的類才算做一個異常類。

那麼我業務需要,我們需要來建構我們自己的異常類,來友善我們抛出。

在寫自己的異常類之前,我們需要了解,我們的異常類需要包含哪些資訊。我這裡寫出3個資訊在接口開發中我認為是足夠了。

首先我們建立一個基礎異常類,我認為在開發中,隻要是新的類型的類,都應該去建立一個Base類用來繼承,先不管用不用的上,當用上時确實會節省很多時間,這也是面向對象程式設計的優勢

class BaseException extends Exception
{
    //預設傳回碼為400參數錯誤
    public $code = 400;
    //預設傳回資訊為參數錯誤
    public $msg = 'parameter error';
    //預設傳回通用錯誤碼
    public $errorCode = 10000;
}
           

我們有了基類之後我們建立自定義異常類時繼承一下就好了。後來我發現有些同類錯誤,但是錯誤資訊又有點小差異這種去建立兩個異常類又有點傻逼。是以我在基類中加上一個構造方法

//基礎異常類,用于被各種不同的異常繼承
class BaseException extends Exception
{
    //預設傳回碼為400參數錯誤
    public $code = 400;
    //預設傳回資訊為參數錯誤
    public $msg = 'parameter error';
    //預設傳回通用錯誤碼
    public $errorCode = 10000;

    //設計構造函數,友善某些異常類需要傳入參數修改
    public function __construct($params = [])
    {
        if (!is_array($params) || empty($params)) {
            //如果不是數組或為空,則代表不修改目前的類成員變量,也就是用預設的值來傳回給用戶端
            return;
        }
        if (key_exists('code', $params)) {
            $this->code = $params['code'];
        }
        if (key_exists('msg', $params)) {
            $this->msg = $params['msg'];
        }
        if (key_exists('errorCode', $params)) {
            $this->errorCode = $params['errorCode'];
        }
    }
}
           
這樣的話,我們隻需要寫一些比較大體的異常類,然後在構造函數中傳入我想修改的資訊就可以。
什麼是全局異常控制

然後我們需要思考,我們為什麼要抛出異常,抛出異常和傳回false有什麼差別

下面我們設想下一個場景:

假如我們現在有個控制器層,控制器去調用一個服務層的方法,服務層代碼中又調用了模型層的方法,在這個方法中間,我們判斷有個什麼不太對的地方,我們需要傳回個用戶端一個報錯資訊,比如,參數錯誤或者别的東西。那麼如果我們要使用傳回false的話,則需要,從Model層的方法中傳回false,然後在service層中接收,再傳回false,然後控制器裡接收,再根據傳回的false的地方構造報錯資訊,轉換為json,在傳回給用戶端。

那麼抛出異常的優勢就提現出來了,首先我們抛出的異常對象可以包含一些報錯資訊,其次,抛出異常會直接中斷後面的所有代碼的執行,非常的幹脆。

現在來看看沒有錯誤的情況我們的操作流程

正常不出錯的情況

也許沒有這麼多層,可能就是一個模型就完了 我隻是打個比方。

那麼如果出錯的情況,流程應該怎麼走呢?

異常控制

我們知道架構有一個異常控制,會将抛出的異常處理成html頁面。我們希望有個類似的東西來幫我們捕獲我們抛出的異常,并且,将錯誤資訊直接傳回給用戶端。這樣我們就不用一層一層的往控制器傳。

那麼事實上,TP5的确給了我們這樣的東西,在手冊中名字叫異常處理接管。從名字不難看出,這個就是我們想要的功能,隻是tp的文檔中寫的比較生澀,不太容易懂。必須要結合案例來學習。

TP5異常接管的使用

我們要接管tp5的異常控制,我們需要知道tp5之前異常控制的地方在哪。tp5将這個路徑寫到了配置裡了

原本tp異常控制類

我們将我們自己的異常控制類建立好之後将完整的帶命名空間的路徑配置到這裡。

再看看自己的異常控制類如何寫

其實對于異常控制來說,捕獲異常,分析異常類。。。。還是非常複雜的,我們實際上隻需要把最後一步渲染成html這一步改成我們需要的傳回用戶端資料。是以我們将之前的tp的異常控制繼承,然後重寫他渲染html那個方法供我們使用就好了

tp異常控制的渲染方法

那麼繼承了tp的Handle類,重寫這個render方法,當然同樣的render方法傳入的異常對象$e 我們繼承之後也回收到

class ExceptionHandler extends Handle
{
    //同樣的這三個參數,建立起來,友善使用
    private $code;
    private $msg;
    private $errorCode;

    public function render(Exception $e)
    {
       
    }
}
           

在書寫我們的代碼之前,我們需要了解一個非常重要的概念:異常的分類

異常分類

我們将我們自己設計的異常,分為一類。

将我們不可控的異常,分為一類。

我在圖中有舉了一些例子。

那麼,我們如何區分這兩類異常呢?

細心的朋友肯定會發現,我們自己設計的異常我們都會繼承我們自己寫BaseException類,通過這一點就可以區分,我們捕獲的異常到底是哪一類的。如果不是我們控制範圍之内的異常,我們就應該異常他的異常資訊,報一個通用的異常信心,比如未知錯誤,錯誤碼500 那種,這樣也能保護我們自己的一些資訊。

除了報通用的錯誤資訊之外,我們還應該記錄日志,友善我們排查我們代碼的錯誤

那麼我們現在需要思考一個新的問題,這個功能是屬于錦上添花的功能

那就是,我們把異常接管了之後,遇到非我們設計的異常,就會報通用錯誤,這個設定,在生産模式下沒有問題。但是在開發階段,我們更希望的是看到架構給我們設計好的html報錯頁面,友善我們定位錯誤。

基于以上的考慮,我通過判斷debug是否開啟來判斷是否處于生産模式,如果是開發模式的話,就調用父類的方法render方法,這樣就可以渲染出友好的html報錯頁面。

說了這麼多也來看看代碼吧(涉及到記錄日志方法,大家可以根據自己的需求來,記錄資料庫也可以,我就不過多介紹,不是本文重點)

namespace app\lib\exception;

//用于繼承tp5的全局異常處理類,用來重寫其中的render方法來做最終的異常處理
use think\Config;
use think\exception\Handle;
use Exception;
use think\Log;

//總的異常處理類
class ExceptionHandler extends Handle
{
    private $code;
    private $msg;
    private $errorCode;

    public function render(Exception $e)
    {
        //如果這個傳入的異常類是我們自定義的異常類的話,就說明這個異常在我們的控制之中
        if ($e instanceof BaseException) {
            //将該異常設定好的屬性給指派到總的異常處理類
            $this->code = $e->code;
            $this->msg = $e->msg;
            $this->errorCode = $e->errorCode;
        } else {
            //判斷配置中的dbug是否開啟确定開發或生産模式
            if (Config::get('app_debug')) {
                //如果是開發模式
                return parent::render($e);

            } else {
                //如果是生産模式,則傳回與設定好的未知錯誤的json
                $this->code = 500;
                $this->msg = 'Unknown Error';
                $this->errorCode = 999;
            }
            //全局的記錄日志
            $this->recordErrorLog($e);
        }
        $request = request();
        $result = [
            'errorCode' => $this->errorCode,
            'msg' => $this->msg,
            'url' => $request->url()
        ];
        //傳回異常資訊到用戶端
        return json($result, $this->code);
    }

    /**
     * @param $e
     * 傳入異常對象
     */
    private function recordErrorLog(Exception $e)
    {
        //由于在config檔案中關閉了tp5自己的日志系統,我們需要重新初始化下
        Log::init([
            'type' => 'file',
            'path' => LOG_PATH,
            'level' => ['error']
        ]);
        //記錄日志,傳入異常的資訊
        Log::record($e->getMessage(), 'error');
    }
}
           
最後将方法寫好之後,不要忘了在config檔案中配置你的異常控制類

應用抛出異常

那麼說了這麼多,現在拿出一個執行個體來展示下。

這次測試的接口是一個非常簡單的請求資源接口。我們設計的異常有兩個,第一就是用戶端傳遞過來的id不是正整數。第二個異常就是請求的資源為空。同樣的我也故意寫一個代碼錯誤抛出一個非我們自己設計的異常。
  1. 我們先看控制器,很明顯能看出來,當我去調用模型方法查出來的資料為空時,我會抛出一個BannerMisssException異常。
/**
     * @url http://local.jxshop.com/api/v1/banner/1
     * @http GET
     * @param $id integer banner的id
     * @throws BannerMissException
     * @return mixed json格式的banner資料
     */
    public function getBanner($id)
    {
        //執行個體化id驗證器對象并調用上面的goCheck方法,來擷取并驗證資料
        IdMustBePositiveInt::instance()->goCheck();
        //使用模型上的擷取banner資料方法
        $banner=BannerModel::getBannerInfoById($id);
        if (!$banner) {
            throw new BannerMissException();
        }
        return $banner;
    }
           
  1. 我們來看看異常類是怎麼寫的

    BannerMiss異常類

  2. 拿postMan來測試一下,我們傳遞一個資料庫沒有的banner_id

    測試結果

大家可以看到我們的異常控制起作用了。我們控制器中拿到banner_id 10000 然後到資料庫中去尋找,資料庫沒有查到,傳回一個空值,控制器中對傳回值進行判斷,如果為空,抛出異常。這時,異常對象就會被我們設計好的異常控制捕獲,并将異常對象中包含的報錯資訊取出,轉換為json。傳回給用戶端。

如果傳入的banner_id 在資料庫中能查到,則不抛出異常,傳回應該查詢到的資料

不抛異常

那麼我們再試一試傳遞非正整數的值去呢?

傳遞負數

同樣的會抛出異常,這個異常有别于剛才的BannerMiss。這是一個參數錯誤異常。

也許有人會有疑問,這個異常是從哪裡跑出來的呢?

其實答案就在goCheck()方法中。這個方法是一個通用的驗證資料方法,我在之前的

TP5巧用驗證器

有過介紹,這裡就不介紹了。直接貼代碼

/**
     * 擷取傳遞參數,并驗證
     * @return array
     * @throws Exception
     * @throws ParameterException
     */
    public function goCheck()
    {
        //接收參數
        $request = Request::instance();
        //通過param方法擷取到所有的參數
        $params = $request->param();
        //由哪個對象來調用goCheck方法,就是由哪個對象來調用check方法,将接收的所有參數傳遞進去
        $result = $this->batch()->check($params);
        if (!$result) {
            //如果結果為false,調用getError方法擷取錯誤資訊
            $error = $this->getError();
            //抛出參數錯誤異常
            throw new ParameterException(['msg' => $error]);
        } else {
            //調用擷取過濾參數的方法,傳回給控制器
            return $this->getDataByRule($params);
        }
    }
           
這又展示了抛出異常的好處,異常是直接中斷程式程序,将異常對象直接抛到最頂端的全局異常控制裡,在model裡可以抛,在service裡也可以,控制器裡也可以,驗證器裡也行。有不正确的地方就抛出異常,給用戶端友好提示。

之前展示的都是我們設計好的異常,那麼如果是我們代碼寫的不對,或者别的什麼我們沒有考慮到的異常出現怎麼辦呢?本文之前也有提過,如果是開發模式,異常控制捕獲後會渲染架構自己的報錯html。如果是生産模式,會傳回給用戶端一個通用錯誤資訊。并記錄日志。

那麼我們現在示範一下。

非設計異常

我們在控制器中加入一個除數為0的代碼。我們都知道這樣寫肯定是要報錯的。

首先我們看開發模式下

開發模式下的非設計異常

伺服器傳回了我們熟悉的tp報錯頁面。準确的定位,還有代碼執行的堆棧資料

那麼現在我将代碼改為生産模式試試

關閉debug

生産模式下非設計異常

這時,傳回的就是一個通用的錯誤資訊。讓用戶端收到比較友好的json資訊,而不是一個HTML代碼。也保護了我們代碼和路徑不被暴露。

之前認真看了代碼的朋友一定記得,我們除了抛出通用錯誤資訊之外,我們還記錄日志,那麼我們去看看日志裡有沒有我們想要的内容。

日志

我們看到根目錄中log檔案,根據日期生成了日志檔案

日志内容

日志記錄錯誤時間,請求ip 請求位址。錯誤資訊。友善我們開發者回溯錯誤,修改bug

好了,兩種例子也展示完了,這個全局異常控制,其實我想寫了很久了,一直沒有寫的原因還是感覺自己的了解不夠深刻,希望在文章中更多的表達清除自己的意思。如果有疑問的地方,歡迎郵件

[email protected]

有沒有寫對的地方,也希望能得到大神的指點。感謝

以上