前言:
说到异常控制,也许很多会比较陌生,我身边很少人会去写抛异常的代码。但是异常用好了是非常的方便大家开发。首先我们来回顾下哪里可以看到异常,首先我们用框架开发的时候,我们的代码出错或者别的东西。如果开启调试模式的话,浏览器页面会报出错误的位置,还有调用的顺序,甚至还有内存的使用等等很多信息。这就是框架在捕获异常时候,将这些数据获取然后渲染了一套html,才让我们这么直观的看到错误。那么既然开发框架的开发者为了方便我们使用,使用了抛出异常,捕获异常。我们也可以照猫画虎,来学习下。
抛出异常
在捕获异常之前我们先来看看抛出异常。
虽然可能不少朋友不常用抛出异常,可是抛出异常的方法,大家一定不会陌生
throw new Exception('错误');
没错,使用throw命令 后面跟个new Exception 就抛出了。其实大家仔细观察发现,其实这个new Exception其实就是 实例化了一个类的对象。那么抛出异常的本质,实际上就是 throw 一个异常类的对象。
那么怎么样才算是一个异常类呢?
我们平时抛出最多的异常类就是think\Exception 这是一个tp封装的一个异常类。
我们发现这个异常类继承一个基础异常类,由此可知,只有直接继承或链式继承这个最最最基础的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将这个路径写到了配置里了
我们将我们自己的异常控制类建立好之后将完整的带命名空间的路径配置到这里。
再看看自己的异常控制类如何写
其实对于异常控制来说,捕获异常,分析异常类。。。。还是非常复杂的,我们实际上只需要把最后一步渲染成html这一步改成我们需要的返回客户端数据。所以我们将之前的tp的异常控制继承,然后重写他渲染html那个方法供我们使用就好了
那么继承了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不是正整数。第二个异常就是请求的资源为空。同样的我也故意写一个代码错误抛出一个非我们自己设计的异常。
- 我们先看控制器,很明显能看出来,当我去调用模型方法查出来的数据为空时,我会抛出一个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;
}
-
我们来看看异常类是怎么写的
-
拿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报错页面。准确的定位,还有代码执行的堆栈数据
那么现在我将代码改为生产模式试试
这时,返回的就是一个通用的错误信息。让客户端收到比较友好的json信息,而不是一个HTML代码。也保护了我们代码和路径不被暴露。
之前认真看了代码的朋友一定记得,我们除了抛出通用错误信息之外,我们还记录日志,那么我们去看看日志里有没有我们想要的内容。
我们看到根目录中log文件,根据日期生成了日志文件
日志记录错误时间,请求ip 请求地址。错误信息。方便我们开发者回溯错误,修改bug
好了,两种例子也展示完了,这个全局异常控制,其实我想写了很久了,一直没有写的原因还是感觉自己的理解不够深刻,希望在文章中更多的表达清除自己的意思。如果有疑问的地方,欢迎邮件xx9090950@gmail.com 有没有写对的地方,也希望能得到大神的指点。感谢
以上