Laravel 源码核心解读:全局异常处理
Laravel 为用户提供了一个基础的全局异常拦截处理器App\Exceptions\Hander
。如果没有全局的异常错误拦截器,那我们在每个可能发生错误异常的业务逻辑分支中,都要使用 try ... catch,然后将执行结果返回 Controller层,再由其根据结果来构造相应的 Response,那代码冗余的会相当可以。
全局异常错误处理,是每个框架都应该具备的,这次我们就通过简析 Laravel 的源码和执行流程,来看一下此模式是如何被运用的。
源码解析
laravel/laravel
脚手架中有一个预定义好的异常处理器:
app/Exceptions/Handler.php
namespace App\Exceptions; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; class Handler extends ExceptionHandler { // 不被处理的异常错误 protected $dontReport = []; // 认证异常时不被flashed的数据 protected $dontFlash = [ 'password', 'password_confirmation', ]; // 上报异常至错误driver,如日志文件(storage/logs/laravel.log),第三方日志存储分析平台 public function report(Exception $exception) { parent::report($exception); } // 将异常信息响应给客户端 public function render($request, Exception $exception) { return parent::render($request, $exception); } }
当 Laravel 处理一次请求时,在启动文件中注册了以下服务:bootstrap/app.php
// 绑定 http 服务提供者 $app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class ); // 绑定 cli 服务提供者 $app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class ); // 这里将异常处理器的服务提供者绑定到了 `App\Exceptions\Handler::class` $app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class );
而后进入请求捕获,处理阶段:public/index.php
// 使用 http 服务处理请求 $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); // http 服务处理捕获的请求 $requeset $response = $kernel->handle( $request = Illuminate\Http\Request::capture() );
因Illuminate\Contracts\Http\Kernel::class
具体提供者是App\Http\Kernel::class
继承至Illuminate\Foundation\Http\Kernel::class
,我们去其中看http 服务
的 handle 方法是如何处理请求的。
请求处理阶段:Illuminate\Foundation\Http\Kernel::class
的 handle
方法对请求做一次处理,如果没有异常则分发路由,如果有异常则调用 reportException
和 renderException
方法记录
&渲染
异常。
具体处理者则是我们在 bootstrap/app.php
中注册绑定的异常处理服务 Illuminate\Contracts\Debug\ExceptionHandler::class
的 report
& render
,具体的服务即绑定的 App\Exceptions\Handler::class
。
public function handle($request) { try { // 没有异常 则进入路由分发 $request->enableHttpMethodParameterOverride(); $response = $this->sendRequestThroughRouter($request); } catch (Exception $e) { // 捕获异常 则 report & render $this->reportException($e); $response = $this->renderException($request, $e); } catch (Throwable $e) { $this->reportException($e = new FatalThrowableError($e)); $response = $this->renderException($request, $e); } $this->app['events']->dispatch( new Events\RequestHandled($request, $response) ); return $response; } //Report the exception to the exception handler. protected function reportException(Exception $e) { // 服务`Illuminate\Contracts\Debug\ExceptionHandler::class` 的 report 方法 $this->app[ExceptionHandler::class]->report($e); } //Render the exception to a response. protected function renderException($request, Exception $e) { // 服务`Illuminate\Contracts\Debug\ExceptionHandler::class` 的 render 方法 return $this->app[ExceptionHandler::class]->render($request, $e); }
handler
方法作为请求处理的入口,后续的路由分发,用户业务调用(controller, model)等执行的上下文依然在此方法中,故异常也能在这一层被捕获。
然后我们就可以在业务中通过 throw new CustomException($code, "错误异常描述");
的方式将控制流程转交给全局异常处理器,由其解析异常并构建响应实体给客户端,这一模式在 Api服务
的开发中是效率极高的。
laravel 的依赖中有 symfony 这个超级棒的组件库,symfony 为我们提供了详细的 Http 异常库,我们可以直接借用这些异常类(当然也可以自定义)
laravel 有提供 abort
助手函数来实现创建一个异常错误,但主要面向 web 网站(因为laravel主要就是用来开发后台的嘛)的,对 Api
不太友好,而且看源码发现只顾及了 404 这货。
/** * abort(401, "你需要登录") * abort(403, "你登录了也白搭") * abort(404, "页面找不到了") * Throw an HttpException with the given data. * * @param int $code * @param string $message * @param array $headers * @return void * * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ public function abort($code, $message = '', array $headers = []) { if ($code == 404) { throw new NotFoundHttpException($message); } throw new HttpException($code, $message, null, $headers); }
即只有 404 用了具体的异常类去抛出,其他的状态码都一股脑的归为 HttpException,这样就不太方便我们在全局异常处理器的 render
中根据 Exception
的具体类型来分而治之了,但 abort
也的确是为了方便你调用具体的错误页面的 resources/views/errors/{statusCode.blade.php}
的,需要对 Api 友好自己改写吧。
使用场景
// 业务代码 不满足直接抛出异常即可 if ("" = trim($username)) { throw new BadRequestHttpException("用户名必须"); }
// 全局处理器 public function render($request, Exception $exception) { if ($exception instanceof BadRequestHttpException) { return response()->json([ "err" => 400, "msg" => $exception->getMessage() ]); } if ($exception instanceof AccessDeniedHttpException) { return response()->json([ "err" => 403, "msg" => "unauthorized" ]); } if ($exception instanceof NotFoundHttpException) { return response()->json([ "err" => 403, "msg" => "forbidden" ]); } if ($exception instanceof NotFoundHttpException) { return response()->json([ "err" => 404, "msg" => "not found" ]); } if ($exception instanceof MethodNotAllowedHttpException) { return response()->json([ "err" => 405, "msg" => "method not allowed" ]); } if ($exception instanceof MethodNotAllowedHttpException) { return response()->json([ "err" => 406, "msg" => "你想要的数据类型我特么给不了啊" ]); } if ($exception instanceof TooManyRequestsHttpException) { return response()->json([ "err" => 429, "msg" => "to many request" ]); } return parent::render($request, $exception); }