前言:
說(shuō)到異常控制吕喘,也許很多會(huì)比較陌生赘那,我身邊很少人會(huì)去寫拋異常的代碼。但是異常用好了是非常的方便大家開(kāi)發(fā)氯质。首先我們來(lái)回顧下哪里可以看到異常募舟,首先我們用框架開(kāi)發(fā)的時(shí)候,我們的代碼出錯(cuò)或者別的東西闻察。如果開(kāi)啟調(diào)試模式的話拱礁,瀏覽器頁(yè)面會(huì)報(bào)出錯(cuò)誤的位置,還有調(diào)用的順序辕漂,甚至還有內(nèi)存的使用等等很多信息呢灶。這就是框架在捕獲異常時(shí)候,將這些數(shù)據(jù)獲取然后渲染了一套html钉嘹,才讓我們這么直觀的看到錯(cuò)誤鸯乃。那么既然開(kāi)發(fā)框架的開(kāi)發(fā)者為了方便我們使用,使用了拋出異常跋涣,捕獲異常飒责。我們也可以照貓畫虎,來(lái)學(xué)習(xí)下仆潮。
拋出異常
在捕獲異常之前我們先來(lái)看看拋出異常。
雖然可能不少朋友不常用拋出異常遣臼,可是拋出異常的方法性置,大家一定不會(huì)陌生
throw new Exception('錯(cuò)誤');
沒(méi)錯(cuò),使用throw命令 后面跟個(gè)new Exception 就拋出了揍堰。其實(shí)大家仔細(xì)觀察發(fā)現(xiàn)鹏浅,其實(shí)這個(gè)new Exception其實(shí)就是 實(shí)例化了一個(gè)類的對(duì)象。那么拋出異常的本質(zhì)屏歹,實(shí)際上就是 throw 一個(gè)異常類的對(duì)象隐砸。
那么怎么樣才算是一個(gè)異常類呢?
我們平時(shí)拋出最多的異常類就是think\Exception 這是一個(gè)tp封裝的一個(gè)異常類蝙眶。
我們發(fā)現(xiàn)這個(gè)異常類繼承一個(gè)基礎(chǔ)異常類季希,由此可知褪那,只有直接繼承或鏈?zhǔn)嚼^承這個(gè)最最最基礎(chǔ)的Exception類的類才算做一個(gè)異常類。
那么我業(yè)務(wù)需要式塌,我們需要來(lái)構(gòu)建我們自己的異常類博敬,來(lái)方便我們拋出。
在寫自己的異常類之前峰尝,我們需要了解偏窝,我們的異常類需要包含哪些信息。我這里寫出3個(gè)信息在接口開(kāi)發(fā)中我認(rèn)為是足夠了武学。
首先我們建立一個(gè)基礎(chǔ)異常類祭往,我認(rèn)為在開(kāi)發(fā)中,只要是新的類型的類火窒,都應(yīng)該去建立一個(gè)Base類用來(lái)繼承硼补,先不管用不用的上,當(dāng)用上時(shí)確實(shí)會(huì)節(jié)省很多時(shí)間沛鸵,這也是面向?qū)ο缶幊痰膬?yōu)勢(shì)
class BaseException extends Exception
{
//默認(rèn)返回碼為400參數(shù)錯(cuò)誤
public $code = 400;
//默認(rèn)返回信息為參數(shù)錯(cuò)誤
public $msg = 'parameter error';
//默認(rèn)返回通用錯(cuò)誤碼
public $errorCode = 10000;
}
我們有了基類之后我們新建自定義異常類時(shí)繼承一下就好了括勺。后來(lái)我發(fā)現(xiàn)有些同類錯(cuò)誤,但是錯(cuò)誤信息又有點(diǎn)小差異這種去建立兩個(gè)異常類又有點(diǎn)傻逼曲掰。所以我在基類中加上一個(gè)構(gòu)造方法
//基礎(chǔ)異常類,用于被各種不同的異常繼承
class BaseException extends Exception
{
//默認(rèn)返回碼為400參數(shù)錯(cuò)誤
public $code = 400;
//默認(rèn)返回信息為參數(shù)錯(cuò)誤
public $msg = 'parameter error';
//默認(rèn)返回通用錯(cuò)誤碼
public $errorCode = 10000;
//設(shè)計(jì)構(gòu)造函數(shù),方便某些異常類需要傳入?yún)?shù)修改
public function __construct($params = [])
{
if (!is_array($params) || empty($params)) {
//如果不是數(shù)組或?yàn)榭?則代表不修改當(dāng)前的類成員變量,也就是用預(yù)設(shè)的值來(lái)返回給客戶端
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'];
}
}
}
這樣的話疾捍,我們只需要寫一些比較大體的異常類,然后在構(gòu)造函數(shù)中傳入我想修改的信息就可以栏妖。
什么是全局異陈叶梗控制
然后我們需要思考,我們為什么要拋出異常吊趾,拋出異常和返回false有什么區(qū)別
下面我們?cè)O(shè)想下一個(gè)場(chǎng)景:
假如我們現(xiàn)在有個(gè)控制器層宛裕,控制器去調(diào)用一個(gè)服務(wù)層的方法,服務(wù)層代碼中又調(diào)用了模型層的方法论泛,在這個(gè)方法中間揩尸,我們判斷有個(gè)什么不太對(duì)的地方,我們需要返回個(gè)客戶端一個(gè)報(bào)錯(cuò)信息屁奏,比如岩榆,參數(shù)錯(cuò)誤或者別的東西。那么如果我們要使用返回false的話坟瓢,則需要勇边,從Model層的方法中返回false,然后在service層中接收折联,再返回false粒褒,然后控制器里接收,再根據(jù)返回的false的地方構(gòu)造報(bào)錯(cuò)信息诚镰,轉(zhuǎn)換為json奕坟,在返回給客戶端祥款。
那么拋出異常的優(yōu)勢(shì)就提現(xiàn)出來(lái)了,首先我們拋出的異常對(duì)象可以包含一些報(bào)錯(cuò)信息执赡,其次镰踏,拋出異常會(huì)直接中斷后面的所有代碼的執(zhí)行,非常的干脆沙合。
現(xiàn)在來(lái)看看沒(méi)有錯(cuò)誤的情況我們的操作流程
也許沒(méi)有這么多層奠伪,可能就是一個(gè)模型就完了 我只是打個(gè)比方。
那么如果出錯(cuò)的情況首懈,流程應(yīng)該怎么走呢绊率?
我們知道框架有一個(gè)異尘柯模控制滤否,會(huì)將拋出的異常處理成html頁(yè)面。我們希望有個(gè)類似的東西來(lái)幫我們捕獲我們拋出的異常最仑,并且藐俺,將錯(cuò)誤信息直接返回給客戶端。這樣我們就不用一層一層的往控制器傳泥彤。
那么事實(shí)上欲芹,TP5的確給了我們這樣的東西,在手冊(cè)中名字叫異常處理接管吟吝。從名字不難看出菱父,這個(gè)就是我們想要的功能,只是tp的文檔中寫的比較生澀剑逃,不太容易懂浙宜。必須要結(jié)合案例來(lái)學(xué)習(xí)。
TP5異常接管的使用
我們要接管tp5的異秤蓟牵控制粟瞬,我們需要知道tp5之前異常控制的地方在哪萤捆。tp5將這個(gè)路徑寫到了配置里了
我們將我們自己的異常控制類建立好之后將完整的帶命名空間的路徑配置到這里鳖轰。
再看看自己的異常控制類如何寫
其實(shí)對(duì)于異撤龆疲控制來(lái)說(shuō)蕴侣,捕獲異常,分析異常類臭觉。昆雀。辱志。。還是非常復(fù)雜的狞膘,我們實(shí)際上只需要把最后一步渲染成html這一步改成我們需要的返回客戶端數(shù)據(jù)揩懒。所以我們將之前的tp的異常控制繼承挽封,然后重寫他渲染html那個(gè)方法供我們使用就好了
那么繼承了tp的Handle類,重寫這個(gè)render方法辅愿,當(dāng)然同樣的render方法傳入的異常對(duì)象$e 我們繼承之后也回收到
class ExceptionHandler extends Handle
{
//同樣的這三個(gè)參數(shù)智亮,建立起來(lái),方便使用
private $code;
private $msg;
private $errorCode;
public function render(Exception $e)
{
}
}
在書寫我們的代碼之前点待,我們需要理解一個(gè)非常重要的概念:異常的分類
我們將我們自己設(shè)計(jì)的異常阔蛉,分為一類。
將我們不可控的異常癞埠,分為一類状原。
我在圖中有舉了一些例子。
那么苗踪,我們?nèi)绾?strong>區(qū)分這兩類異常呢颠区?
細(xì)心的朋友肯定會(huì)發(fā)現(xiàn),我們自己設(shè)計(jì)的異常我們都會(huì)繼承我們自己寫B(tài)aseException類徒探,通過(guò)這一點(diǎn)就可以區(qū)分瓦呼,我們捕獲的異常到底是哪一類的。如果不是我們控制范圍之內(nèi)的異常测暗,我們就應(yīng)該異常他的異常信息央串,報(bào)一個(gè)通用的異常信心,比如未知錯(cuò)誤碗啄,錯(cuò)誤碼500 那種质和,這樣也能保護(hù)我們自己的一些信息。
除了報(bào)通用的錯(cuò)誤信息之外稚字,我們還應(yīng)該記錄日志饲宿,方便我們排查我們代碼的錯(cuò)誤
那么我們現(xiàn)在需要思考一個(gè)新的問(wèn)題,這個(gè)功能是屬于錦上添花的功能
那就是胆描,我們把異常接管了之后瘫想,遇到非我們?cè)O(shè)計(jì)的異常,就會(huì)報(bào)通用錯(cuò)誤昌讲,這個(gè)設(shè)定国夜,在生產(chǎn)模式下沒(méi)有問(wèn)題。但是在開(kāi)發(fā)階段短绸,我們更希望的是看到框架給我們?cè)O(shè)計(jì)好的html報(bào)錯(cuò)頁(yè)面车吹,方便我們定位錯(cuò)誤筹裕。
基于以上的考慮,我通過(guò)判斷debug是否開(kāi)啟來(lái)判斷是否處于生產(chǎn)模式窄驹,如果是開(kāi)發(fā)模式的話朝卒,就調(diào)用父類的方法render方法,這樣就可以渲染出友好的html報(bào)錯(cuò)頁(yè)面乐埠。
說(shuō)了這么多也來(lái)看看代碼吧(涉及到記錄日志方法抗斤,大家可以根據(jù)自己的需求來(lái),記錄數(shù)據(jù)庫(kù)也可以饮戳,我就不過(guò)多介紹豪治,不是本文重點(diǎn))
namespace app\lib\exception;
//用于繼承tp5的全局異常處理類,用來(lái)重寫其中的render方法來(lái)做最終的異常處理
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)
{
//如果這個(gè)傳入的異常類是我們自定義的異常類的話,就說(shuō)明這個(gè)異常在我們的控制之中
if ($e instanceof BaseException) {
//將該異常設(shè)定好的屬性給賦值到總的異常處理類
$this->code = $e->code;
$this->msg = $e->msg;
$this->errorCode = $e->errorCode;
} else {
//判斷配置中的dbug是否開(kāi)啟確定開(kāi)發(fā)或生產(chǎn)模式
if (Config::get('app_debug')) {
//如果是開(kāi)發(fā)模式
return parent::render($e);
} else {
//如果是生產(chǎn)模式,則返回與設(shè)定好的未知錯(cuò)誤的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
* 傳入異常對(duì)象
*/
private function recordErrorLog(Exception $e)
{
//由于在config文件中關(guān)閉了tp5自己的日志系統(tǒng),我們需要重新初始化下
Log::init([
'type' => 'file',
'path' => LOG_PATH,
'level' => ['error']
]);
//記錄日志,傳入異常的信息
Log::record($e->getMessage(), 'error');
}
}
最后將方法寫好之后,不要忘了在config文件中配置你的異吵豆蓿控制類
應(yīng)用拋出異常
那么說(shuō)了這么多,現(xiàn)在拿出一個(gè)實(shí)例來(lái)展示下负拟。
這次測(cè)試的接口是一個(gè)非常簡(jiǎn)單的請(qǐng)求資源接口。我們?cè)O(shè)計(jì)的異常有兩個(gè)歹河,第一就是客戶端傳遞過(guò)來(lái)的id不是正整數(shù)掩浙。第二個(gè)異常就是請(qǐng)求的資源為空。同樣的我也故意寫一個(gè)代碼錯(cuò)誤拋出一個(gè)非我們自己設(shè)計(jì)的異常秸歧。
- 我們先看控制器厨姚,很明顯能看出來(lái),當(dāng)我去調(diào)用模型方法查出來(lái)的數(shù)據(jù)為空時(shí)键菱,我會(huì)拋出一個(gè)BannerMisssException異常谬墙。
/**
* @url http://local.jxshop.com/api/v1/banner/1
* @http GET
* @param $id integer banner的id
* @throws BannerMissException
* @return mixed json格式的banner數(shù)據(jù)
*/
public function getBanner($id)
{
//實(shí)例化id驗(yàn)證器對(duì)象并調(diào)用上面的goCheck方法,來(lái)獲取并驗(yàn)證數(shù)據(jù)
IdMustBePositiveInt::instance()->goCheck();
//使用模型上的獲取banner數(shù)據(jù)方法
$banner=BannerModel::getBannerInfoById($id);
if (!$banner) {
throw new BannerMissException();
}
return $banner;
}
-
我們來(lái)看看異常類是怎么寫的
-
拿postMan來(lái)測(cè)試一下,我們傳遞一個(gè)數(shù)據(jù)庫(kù)沒(méi)有的banner_id
大家可以看到我們的異尘福控制起作用了拭抬。我們控制器中拿到banner_id 10000 然后到數(shù)據(jù)庫(kù)中去尋找,數(shù)據(jù)庫(kù)沒(méi)有查到侵蒙,返回一個(gè)空值造虎,控制器中對(duì)返回值進(jìn)行判斷,如果為空纷闺,拋出異常算凿。這時(shí),異常對(duì)象就會(huì)被我們?cè)O(shè)計(jì)好的異忱绻Γ控制捕獲氓轰,并將異常對(duì)象中包含的報(bào)錯(cuò)信息取出,轉(zhuǎn)換為json浸卦。返回給客戶端戒努。
如果傳入的banner_id 在數(shù)據(jù)庫(kù)中能查到,則不拋出異常,返回應(yīng)該查詢到的數(shù)據(jù)
那么我們?cè)僭囈辉噦鬟f非正整數(shù)的值去呢储玫?
同樣的會(huì)拋出異常,這個(gè)異常有別于剛才的BannerMiss萤皂。這是一個(gè)參數(shù)錯(cuò)誤異常撒穷。
也許有人會(huì)有疑問(wèn),這個(gè)異常是從哪里跑出來(lái)的呢裆熙?
其實(shí)答案就在goCheck()方法中端礼。這個(gè)方法是一個(gè)通用的驗(yàn)證數(shù)據(jù)方法,我在之前的TP5巧用驗(yàn)證器有過(guò)介紹入录,這里就不介紹了蛤奥。直接貼代碼
/**
* 獲取傳遞參數(shù),并驗(yàn)證
* @return array
* @throws Exception
* @throws ParameterException
*/
public function goCheck()
{
//接收參數(shù)
$request = Request::instance();
//通過(guò)param方法獲取到所有的參數(shù)
$params = $request->param();
//由哪個(gè)對(duì)象來(lái)調(diào)用goCheck方法,就是由哪個(gè)對(duì)象來(lái)調(diào)用check方法,將接收的所有參數(shù)傳遞進(jìn)去
$result = $this->batch()->check($params);
if (!$result) {
//如果結(jié)果為false,調(diào)用getError方法獲取錯(cuò)誤信息
$error = $this->getError();
//拋出參數(shù)錯(cuò)誤異常
throw new ParameterException(['msg' => $error]);
} else {
//調(diào)用獲取過(guò)濾參數(shù)的方法僚稿,返回給控制器
return $this->getDataByRule($params);
}
}
這又展示了拋出異常的好處凡桥,異常是直接中斷程序進(jìn)程,將異常對(duì)象直接拋到最頂端的全局異呈赐控制里缅刽,在model里可以拋,在service里也可以蠢络,控制器里也可以衰猛,驗(yàn)證器里也行。有不正確的地方就拋出異常刹孔,給客戶端友好提示啡省。
之前展示的都是我們?cè)O(shè)計(jì)好的異常,那么如果是我們代碼寫的不對(duì)髓霞,或者別的什么我們沒(méi)有考慮到的異常出現(xiàn)怎么辦呢卦睹?本文之前也有提過(guò),如果是開(kāi)發(fā)模式酸茴,異撤衷ぃ控制捕獲后會(huì)渲染框架自己的報(bào)錯(cuò)html。如果是生產(chǎn)模式薪捍,會(huì)返回給客戶端一個(gè)通用錯(cuò)誤信息笼痹。并記錄日志。
那么我們現(xiàn)在演示一下酪穿。
我們?cè)诳刂破髦屑尤胍粋€(gè)除數(shù)為0的代碼凳干。我們都知道這樣寫肯定是要報(bào)錯(cuò)的。
首先我們看開(kāi)發(fā)模式下
服務(wù)器返回了我們熟悉的tp報(bào)錯(cuò)頁(yè)面被济。準(zhǔn)確的定位救赐,還有代碼執(zhí)行的堆棧數(shù)據(jù)
那么現(xiàn)在我將代碼改為生產(chǎn)模式試試
這時(shí),返回的就是一個(gè)通用的錯(cuò)誤信息。讓客戶端收到比較友好的json信息经磅,而不是一個(gè)HTML代碼泌绣。也保護(hù)了我們代碼和路徑不被暴露。
之前認(rèn)真看了代碼的朋友一定記得预厌,我們除了拋出通用錯(cuò)誤信息之外阿迈,我們還記錄日志,那么我們?nèi)タ纯慈罩纠镉袥](méi)有我們想要的內(nèi)容轧叽。
我們看到根目錄中l(wèi)og文件苗沧,根據(jù)日期生成了日志文件
日志記錄錯(cuò)誤時(shí)間,請(qǐng)求ip 請(qǐng)求地址炭晒。錯(cuò)誤信息待逞。方便我們開(kāi)發(fā)者回溯錯(cuò)誤,修改bug
好了网严,兩種例子也展示完了识樱,這個(gè)全局異常控制屿笼,其實(shí)我想寫了很久了牺荠,一直沒(méi)有寫的原因還是感覺(jué)自己的理解不夠深刻,希望在文章中更多的表達(dá)清除自己的意思驴一。如果有疑問(wèn)的地方休雌,歡迎郵件xx9090950@gmail.com 有沒(méi)有寫對(duì)的地方,也希望能得到大神的指點(diǎn)肝断。感謝
以上