果断抛弃try catch!业界大佬力荐的异常优雅处理方案
背景
在软件开发的日常工作里,大家都知道,处理各种各样的异常情况是躲不开的必修课。就我个人的切身体会而言,我仔细回想了一下,好家伙,我投入到处理异常当中的精力,保守估计得占了开发总时长的一半还多。
这直接后果就是,我手头代码里频繁冒出大量的 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源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
IvorySQL 4.0 之 Invisible Column 功能解析
前言 随着数据库应用场景的多样化,用户对数据管理的灵活性和隐私性提出了更高要求。IvorySQL 作为一款基于 PostgreSQL 并兼容 Oracle 的开源数据库,始终致力于在功能上保持领先和创新。在最新发布的 4.0 版本中,IvorySQL 新增了 Oracle 兼容特性 Invisible Column(不可见列),这一功能由社区贡献者 Imran Zaheer 提供,体现了开源社区协作的力量。 Invisible Column 的引入,为开发者提供了在不影响现有应用的情况下动态调整数据库结构的新选择,进一步提升了 IvorySQL 在数据灵活性管理上的能力,为用户在数据库升级、兼容性优化等方面提供了更大的便利性。 本文将详细介绍这一特性的功能、使用场景以及使用方式。 什么是 Invisible Column? 在现代数据库开发中,列的可见性管理在一定程度上影响了应用程序的灵活性与迁移效率。Oracle 12c 提供了一项强大的功能:Invisible Column(不可见列)。这是一种隐藏数据列的特性,用于增强数据安全性和实现业务逻辑。这一功能为开发者提供了灵活性和控制能...
- 下一篇
为什么在 Python 中 hash(-1) == hash(-2)?
英文:https://omairmajid.com/posts/2021-07-16-why-is-hash-in-python 作者:Omair Majid 译者:豌豆花下猫&Claude-3.5-Sonnet 时间:原文发布于 2021.07.16,翻译于 2025.01.11 收录于:Python为什么系列 https://github.com/chinesehuazhou/python-whydo 当我在等待代码编译的时候,我在 Reddit 的 r/Python 上看到了这个问题: > hash(-1) == hash(-2) 是个彩蛋吗? 等等,这是真的吗? $ python Python 3.9.6 (default, Jun 29 2021, 00:00:00) [GCC 11.1.1 20210531 (Red Hat 11.1.1-3)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> has...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS8编译安装MySQL8.0.19
- CentOS7,CentOS8安装Elasticsearch6.8.6