背景
在软件开发的日常工作里,大家都知道,处理各种各样的异常情况是躲不开的必修课。就我个人的切身体会而言,我仔细回想了一下,好家伙,我投入到处理异常当中的精力,保守估计得占了开发总时长的一半还多。
这直接后果就是,我手头代码里频繁冒出大量的 try {...} catch {...} finally {...}
代码块,一眼望去,它们就跟杂乱无章的补丁似的。
一方面,这里面存在着大量重复、冗余的代码,仿佛在无声地消耗着代码库的 “整洁度”,另一方面,这些代码块还严重影响了代码整体的可读性,每次我想要深入理解或者修改某段代码逻辑时,都得在这堆乱糟糟的异常处理代码里 “跋涉” 半天,别提多费劲了。
现在呢,咱们来看看下面两个代码模块,对照一下,瞧瞧我目前编写的代码更贴近哪种风格。说实在的,我心里也犯嘀咕呢,到底哪种编码风格才是更优解?要是大家有什么高见,欢迎畅所欲言,帮我指点一二。
丑陋的代码模块
declare(strict_types=1);
namespace app\controller;
use app\common\service\ArticleService;
use support\Request;
use support\Response;
use Tinywan\ExceptionHandler\Exception\BadRequestHttpException;
use Tinywan\ExceptionHandler\Exception\ForbiddenHttpException;
use Webman\Exception\BusinessException;
class ArticleController
{
/**
* @desc index
* @param Request $request
* @return Response
*/
public function index(Request $request): Response
{
try {
$res = ArticleService::getArticleList();
} catch (ForbiddenHttpException $e) {
// do something
} catch (BadRequestHttpException $e) {
// do something
} catch (BusinessException $e) {
// do something
}
return json($res);
}
/**
* @desc add
* @param Request $request
* @return Response
*/
public function add(Request $request): Response
{
try {
$res = ArticleService::getArticleList();
} catch (BadRequestHttpException $e) {
// do something
} catch (BusinessException $e) {
// do something
}
return json($res);
}
/**
* @desc detail
* @param Request $request
* @return Response
*/
public function detail(Request $request): Response
{
try {
$res = ArticleService::getArticleList();
} catch (ForbiddenHttpException $e) {
// do something
} catch (BusinessException $e) {
// do something
}
return json($res);
}
}
优雅的代码模块
<?php
declare(strict_types=1);
namespace app\controller;
use app\common\service\ArticleService;
use support\Request;
use support\Response;
class ArticleController
{
/**
* @desc index
* @param Request $request
* @return Response
*/
public function index(Request $request): Response
{
return json(ArticleService::getArticleList());
}
/**
* @desc add
* @param Request $request
* @return Response
*/
public function add(Request $request): Response
{
return json(ArticleService::getArticleList());
}
/**
* @desc detail
* @param Request $request
* @return Response
*/
public function detail(Request $request): Response
{
return json(ArticleService::getArticleList());
}
}
大家瞧瞧,就刚才咱们看到的那些例子,其实还都局限在 Controller
层呢。要是再往深一层,到了 Service
层,那情况可就更 “热闹” 了,try catch
代码块大概率会跟雨后春笋似的,一个接一个往外冒。这可太要命了,代码的可读性被折腾得够呛,原本清晰流畅的逻辑线,全被这些 “补丁” 式的代码给搅得乱成一锅粥,从审美的角度看,那也是 “惨不忍睹”,完全没了 “美感”。
所以,我这里肯定会毫不犹豫地选第二种处理方式。为啥呢?这么一来,我就能把大把的精力一门心思地投入到业务代码的精雕细琢上了,与此同时,代码整体也会变得清爽利落得多。不过,这里面有个关键问题得拎清楚,虽说业务代码不再大张旗鼓地显式捕获、处理异常了,但异常这玩意儿可不能就这么放任不管啊,真要是撒手不管,系统还不得跟个纸糊的一样,稍微来点 “风吹草动” 就立马崩溃歇菜了。
所以,必然得有个合适的 “兜底” 之处,把这些四处乱窜的异常稳稳接住并妥善处置咯。那么,问题就来了,究竟该怎么个优雅法,才能把各种各样的异常处理得妥妥当当呢?
异常
异常是程序在运行中出现不符合预期的情况及与正常流程不同的状况。一种不正常的情况,按照正常逻辑本不该出的错误,但仍然会出现的错误,这是属于逻辑和业务流程的错误,而不是编译或者语法上的错误。
PHP有一个和其他语言相似的异常模型。在 PHP 里可以 throw 并捕获(catch)异常。为了捕获潜在的异常,代码会包含在 try 块里。每个 try 都必须至少有一个相应的 catch 或 finally 块。
如果抛出异常的函数作用域内没有 catch 块,异常会沿调用栈“向上冒泡”,直到找到匹配的 catch 块。沿途会执行所有遇到的 finally 块。在没有设置全局异常处理程序时,如果调用栈向上都没有遇到匹配的 catch,程序会抛出 fatal 错误并终止。
统一异常处理
现代的 PHP 框架都提供了异常处理机制,比如 Webman Laravel、ThinkPHP、Yii2.0 等。这些框枕都提供了异常处理的机制,可以让我们在应用中统一处理异常,而不是在每个地方都写一遍异常处理代码。
例如:在 Webman 中,可以通过 Webman\Exception\ExceptionHandler
类来处理异常。Webman\Exception\ExceptionHandler
类是 Webman 的异常处理类。
Webman 的 ExceptionHandler
类核心代码如下:
/**
* Class Handler
* @package support\exception
*/
class ExceptionHandler implements ExceptionHandlerInterface
{
/**
* @var LoggerInterface
*/
protected $logger = null;
/**
* @var bool
*/
protected $debug = false;
/**
* @var array
*/
public $dontReport = [];
/**
* ExceptionHandler constructor.
* @param $logger
* @param $debug
*/
public function __construct($logger, $debug)
{
$this->logger = $logger;
$this->debug = $debug;
}
/**
* @param Throwable $exception
* @return void
*/
public function report(Throwable $exception)
{
if ($this->shouldntReport($exception)) {
return;
}
$logs = '';
if ($request = \request()) {
$logs = $request->getRealIp() . ' ' . $request->method() . ' ' . trim($request->fullUrl(), '/');
}
$this->logger->error($logs . PHP_EOL . $exception);
}
/**
* @param Request $request
* @param Throwable $exception
* @return Response
*/
public function render(Request $request, Throwable $exception): Response
{
if (method_exists($exception, 'render') && ($response = $exception->render($request))) {
return $response;
}
$code = $exception->getCode();
if ($request->expectsJson()) {
$json = ['code' => $code ?: 500, 'msg' => $this->debug ? $exception->getMessage() : 'Server internal error'];
$this->debug && $json['traces'] = (string)$exception;
return new Response(200, ['Content-Type' => 'application/json'],
json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
$error = $this->debug ? nl2br((string)$exception) : 'Server internal error';
return new Response(500, [], $error);
}
/**
* @param Throwable $e
* @return bool
*/
protected function shouldntReport(Throwable $e): bool
{
foreach ($this->dontReport as $type) {
if ($e instanceof $type) {
return true;
}
}
return false;
}
/**
* Compatible $this->_debug
*
* @param string $name
* @return bool|null
*/
public function __get(string $name)
{
if ($name === '_debug') {
return $this->debug;
}
return null;
}
}
我们可以通过继承 Webman\Exception\ExceptionHandler
类来自定义异常处理。
自定义优雅异常处理
自定义优雅异常处理 TinywanHandler
类核心代码如下:
<?php
/**
* @desc TinywanHandler
* @author Tinywan(ShaoBo Wan)
*/
declare(strict_types=1);
namespace Tinywan\ExceptionHandler;
use FastRoute\BadRouteException;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
use Throwable;
use Tinywan\ExceptionHandler\Event\DingTalkRobotEvent;
use Tinywan\ExceptionHandler\Exception\BaseException;
use Tinywan\ExceptionHandler\Exception\ServerErrorHttpException;
use Tinywan\Jwt\Exception\JwtRefreshTokenExpiredException;
use Tinywan\Jwt\Exception\JwtTokenException;
use Tinywan\Jwt\Exception\JwtTokenExpiredException;
use Tinywan\Validate\Exception\ValidateException;
use Webman\Exception\ExceptionHandler;
use Webman\Http\Request;
use Webman\Http\Response;
class TinywanHandler extends ExceptionHandler
{
/**
* 不需要记录错误日志.
*
* @var string[]
*/
public $dontReport = [];
/**
* HTTP Response Status Code.
*
* @var array
*/
public int $statusCode = 200;
/**
* HTTP Response Header.
*
* @var array
*/
public array $header = [];
/**
* Business Error code.
*
* @var int
*/
public int $errorCode = 0;
/**
* Business Error message.
*
* @var string
*/
public string $errorMessage = 'no error';
/**
* 响应结果数据.
*
* @var array
*/
protected array $responseData = [];
/**
* config下的配置.
*
* @var array
*/
protected array $config = [];
/**
* Log Error message.
*
* @var string
*/
public string $error = 'no error';
/**
* @param Throwable $exception
*/
public function report(Throwable $exception)
{
$this->dontReport = config('plugin.tinywan.exception-handler.app.exception_handler.dont_report', []);
parent::report($exception);
}
/**
* @param Request $request
* @param Throwable $exception
* @return Response
*/
public function render(Request $request, Throwable $exception): Response
{
$this->config = array_merge($this->config, config('plugin.tinywan.exception-handler.app.exception_handler', []) ?? []);
$this->addRequestInfoToResponse($request);
$this->solveAllException($exception);
$this->addDebugInfoToResponse($exception);
$this->triggerNotifyEvent($exception);
$this->triggerTraceEvent($exception);
return $this->buildResponse();
}
/**
* 请求的相关信息.
*
* @param Request $request
* @return void
*/
protected function addRequestInfoToResponse(Request $request): void
{
$this->responseData = array_merge($this->responseData, [
'domain' => $request->host(),
'method' => $request->method(),
'request_url' => $request->method() . ' ' . $request->uri(),
'timestamp' => date('Y-m-d H:i:s'),
'client_ip' => $request->getRealIp(),
'request_param' => $request->all(),
]);
}
/**
* 处理异常数据.
*
* @param Throwable $e
*/
protected function solveAllException(Throwable $e)
{
if ($e instanceof BaseException) {
$this->statusCode = $e->statusCode;
$this->header = $e->header;
$this->errorCode = $e->errorCode;
$this->errorMessage = $e->errorMessage;
$this->error = $e->error;
if (isset($e->data)) {
$this->responseData = array_merge($this->responseData, $e->data);
}
if (!$e instanceof ServerErrorHttpException) {
return;
}
}
$this->solveExtraException($e);
}
/**
* @desc: 处理扩展的异常
* @param Throwable $e
* @author Tinywan(ShaoBo Wan)
*/
protected function solveExtraException(Throwable $e): void
{
$status = $this->config['status'];
$this->errorMessage = $e->getMessage();
if ($e instanceof BadRouteException) {
$this->statusCode = $status['route'] ?? 404;
} elseif ($e instanceof \TypeError) {
$this->statusCode = $status['type_error'] ?? 400;
$this->errorMessage = isset($status['type_error_is_response']) && $status['type_error_is_response'] ? $e->getMessage() : '网络连接似乎有点不稳定。请检查您的网络!';
$this->error = $e->getMessage();
} elseif ($e instanceof ValidateException) {
$this->statusCode = $status['validate'];
} elseif ($e instanceof JwtTokenException) {
$this->statusCode = $status['jwt_token'];
} elseif ($e instanceof JwtTokenExpiredException) {
$this->statusCode = $status['jwt_token_expired'];
} elseif ($e instanceof JwtRefreshTokenExpiredException) {
$this->statusCode = $status['jwt_refresh_token_expired'];
} elseif ($e instanceof \InvalidArgumentException) {
$this->statusCode = $status['invalid_argument'] ?? 415;
$this->errorMessage = '预期参数配置异常:' . $e->getMessage();
} elseif ($e instanceof DbException) {
$this->statusCode = 500;
$this->errorMessage = 'Db:' . $e->getMessage();
$this->error = $e->getMessage();
} elseif ($e instanceof ServerErrorHttpException) {
$this->errorMessage = $e->errorMessage;
$this->statusCode = 500;
} else {
$this->statusCode = $status['server_error'] ?? 500;
$this->errorMessage = isset($status['server_error_is_response']) && $status['server_error_is_response'] ? $e->getMessage() : 'Internal Server Error';
$this->error = $e->getMessage();
Logger::error($this->errorMessage, array_merge($this->responseData, [
'error' => $this->error,
'file' => $e->getFile(),
'line' => $e->getLine(),
]));
}
}
/**
* 调试模式:错误处理器会显示异常以及详细的函数调用栈和源代码行数来帮助调试,将返回详细的异常信息。
* @param Throwable $e
* @return void
*/
protected function addDebugInfoToResponse(Throwable $e): void
{
if (config('app.debug', false)) {
$this->responseData['error_message'] = $this->errorMessage;
$this->responseData['error_trace'] = explode("\n", $e->getTraceAsString());
$this->responseData['file'] = $e->getFile();
$this->responseData['line'] = $e->getLine();
}
}
/**
* 触发通知事件.
*
* @param Throwable $e
* @return void
*/
protected function triggerNotifyEvent(Throwable $e): void
{
if (!$this->shouldntReport($e) && $this->config['event_trigger']['enable'] ?? false) {
$responseData = $this->responseData;
$responseData['message'] = $this->errorMessage;
$responseData['error'] = $this->error;
$responseData['file'] = $e->getFile();
$responseData['line'] = $e->getLine();
DingTalkRobotEvent::dingTalkRobot($responseData, $this->config);
}
}
/**
* 触发 trace 事件.
*
* @param Throwable $e
* @return void
*/
protected function triggerTraceEvent(Throwable $e): void
{
if (isset(request()->tracer) && isset(request()->rootSpan)) {
$samplingFlags = request()->rootSpan->getContext();
$this->header['Trace-Id'] = $samplingFlags->getTraceId();
$exceptionSpan = request()->tracer->newChild($samplingFlags);
$exceptionSpan->setName('exception');
$exceptionSpan->start();
$exceptionSpan->tag('error.code', (string)$this->errorCode);
$value = [
'event' => 'error',
'message' => $this->errorMessage,
'stack' => 'Exception:' . $e->getFile() . '|' . $e->getLine(),
];
$exceptionSpan->annotate(json_encode($value));
$exceptionSpan->finish();
}
}
/**
* 构造 Response.
*
* @return Response
*/
protected function buildResponse(): Response
{
$bodyKey = array_keys($this->config['body']);
$bodyValue = array_values($this->config['body']);
$responseBody = [
$bodyKey[0] ?? 'code' => $this->setCode($bodyValue, $this->errorCode), // 自定义异常code码
$bodyKey[1] ?? 'msg' => $this->errorMessage,
$bodyKey[2] ?? 'data' => $this->responseData,
];
$header = array_merge(['Content-Type' => 'application/json;charset=utf-8'], $this->header);
return new Response($this->statusCode, $header, json_encode($responseBody));
}
private function setCode($bodyValue, $errorCode)
{
if($errorCode > 0){
return $errorCode;
}
return $bodyValue[0] ?? 0;
}
}
统一异常处理实战
接管 Webman 的异常处理,只需要在 config/exception.php
配置文件中配置 handler
选项即可,如下所示:
return [
// 这里配置异常处理类
'' => support\exception\TinywanHandler::class,
];
多应用模式时,你可以为每个应用单独配置异常处理类,参见多应用配置。
案例1
class ArticleController
{
/**
* @desc index
* @param Request $request
* @return Response
* @throws BadRequestHttpException
*/
public function index(Request $request): Response
{
$res = ArticleService::getArticleList();
if (empty($res)) {
throw new BadRequestHttpException('文章列表为空');
}
return json($res);
}
}
以上响应输出信息,如下格式:
HTTP/1.1 400 Bad Request
Content-Type: application/json;charset=utf-8
{
"code": 0,
"msg": "文章列表为空",
"data": {},
}
案例2
public function index(Request $request): Response
{
$page = 100/0;
return json(['page' => $page]);
}
以上响应输出信息,如下格式:
HTTP/1.1 500 Bad Request
Content-Type: application/json;charset=utf-8
{
"code": 0,
"msg": "Division by zero",
"data": {},
}
更多案例请参考:https://www.workerman.net/plugin/16
本文分享自微信公众号 - 开源技术小栈(shaobowan)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。