23 KiB
23 KiB
全局异常处理开发文档
概述
本文档详细介绍了基于Laravel 12实现的全局异常处理系统,确保所有API请求的错误响应都采用统一的JSON格式,提供完善的错误信息和调试支持。
系统架构
1. 整体结构
全局异常处理系统
├── 异常处理器 (Handler)
│ ├── JSON异常渲染
│ ├── 异常分类处理
│ └── 统一响应格式
├── 业务异常 (BusinessException)
│ ├── 自定义错误码
│ ├── 错误消息
│ └── 业务逻辑异常
├── 日志异常 (LogException)
│ ├── 安全日志记录
│ ├── 多通道尝试
│ └── 权限检查修复
├── 响应枚举 (ResponseEnum)
│ ├── 标准错误码
│ ├── HTTP状态码映射
│ └── 错误消息定义
└── 全局配置 (bootstrap/app.php)
├── 异常处理注册
├── 报告策略
└── 渲染策略
2. 文件结构
app/
├── Exceptions/
│ ├── Handler.php # 主异常处理器
│ ├── BusinessException.php # 业务异常类
│ └── LogException.php # 日志异常处理类
├── Helpers/
│ └── ResponseEnum.php # 响应码枚举
└── Http/Controllers/
└── BaseController.php # 控制器基类
bootstrap/
└── app.php # 应用启动配置
docs/
└── 全局异常处理开发文档.md # 本文档
核心组件详细说明
1. Handler 异常处理器
位置: app/Exceptions/Handler.php
职责:
- 统一处理所有应用异常
- 区分API和Web请求
- 提供统一的JSON响应格式
- 支持开发和生产环境的不同处理策略
主要方法:
render() - 异常渲染入口
- 功能: 判断请求类型,选择处理方式
- 逻辑: admin/* 路径和期望JSON的请求返回JSON格式
- 输出: JsonResponse 或 标准Response
renderJsonException() - JSON异常处理
- 功能: 处理API请求的异常
- 支持的异常类型:
AuthenticationException- 认证失败AccessDeniedHttpException- 权限不足ValidationException- 参数验证失败MethodNotAllowedHttpException- 请求方法不允许ModelNotFoundException- 模型未找到NotFoundHttpException- 路由未找到TooManyRequestsHttpException- 请求频率超限BusinessException- 业务逻辑异常
renderSystemException() - 系统异常处理
- 功能: 处理未预期的系统异常
- 生产环境: 隐藏敏感信息,返回通用错误
- 开发环境: 显示详细调试信息
jsonResponse() - 统一JSON响应
- 功能: 生成标准格式的JSON响应
- 格式:
{
"success": false,
"message": "错误描述",
"code": 错误代码,
"data": null,
"errors": {} // 仅验证错误时存在
}
辅助方法:
throwError()- 快速抛出自定义异常throwParamError()- 抛出参数错误throwNotFound()- 抛出数据不存在异常throwDataExist()- 抛出数据已存在异常
2. BusinessException 业务异常
位置: app/Exceptions/BusinessException.php
职责:
- 处理业务逻辑相关的异常
- 支持自定义错误码和消息
- 继承标准Exception类
构造函数:
public function __construct(array $codeResponse, $info = '')
使用示例:
// 使用预定义错误码
throw new BusinessException(ResponseEnum::DATA_NOT_FOUND_ERROR);
// 自定义错误信息
throw new BusinessException(ResponseEnum::CLIENT_PARAMETER_ERROR, '用户名不能为空');
// 完全自定义
throw new BusinessException([400001, '自定义错误消息']);
3. LogException 日志异常处理
位置: app/Exceptions/LogException.php
职责:
- 安全记录异常到日志系统
- 处理日志系统本身的异常
- 提供多种日志记录策略
主要方法:
safeLog() - 安全日志记录
- 功能: 防止日志记录失败影响主业务
- 策略: 多通道尝试记录
- 兜底: 系统错误日志
tryLogToChannels() - 多通道尝试
- 支持通道: single, daily, errorlog
- 逻辑: 依次尝试,成功即返回
checkAndFixLogPermissions() - 权限检查修复
- 功能: 检查并修复日志文件权限问题
- 自动修复: 创建目录、设置权限
4. ResponseEnum 响应码枚举
位置: app/Helpers/ResponseEnum.php
职责:
- 定义标准错误码和消息
- 提供HTTP状态码映射
- 统一错误信息管理
错误码分类:
100-199: 信息提示200xxx: 操作成功/失败400xxx: 客户端错误500xxx: 服务器错误
常用错误码:
const HTTP_OK = [200001, '操作成功'];
const CLIENT_PARAMETER_ERROR = [400200, '参数错误'];
const DATA_NOT_FOUND_ERROR = [400202, '数据不存在'];
const CLIENT_HTTP_UNAUTHORIZED = [401, '授权失败,请先登录'];
const SYSTEM_ERROR = [500001, '服务器错误'];
异常处理流程
1. 请求异常处理流程
graph TD
A[应用异常发生] --> B{请求类型判断}
B -->|admin/* 或 expectsJson| C[JSON异常处理]
B -->|普通Web请求| D[默认处理]
C --> E{异常类型判断}
E -->|认证异常| F[返回401 JSON]
E -->|验证异常| G[返回422 JSON + errors]
E -->|业务异常| H[返回200 JSON + code]
E -->|系统异常| I{环境判断}
E -->|其他HTTP异常| J[返回对应状态码JSON]
I -->|生产环境| K[隐藏错误详情]
I -->|开发环境| L[显示调试信息]
F --> M[统一JSON格式]
G --> M
H --> M
J --> M
K --> M
L --> M
2. 业务异常抛出流程
graph TD
A[业务逻辑检查] --> B{条件判断}
B -->|正常| C[继续执行]
B -->|异常| D[选择异常类型]
D --> E[Handler::throwParamError()]
D --> F[Handler::throwNotFound()]
D --> G[Handler::throwDataExist()]
D --> H[new BusinessException()]
E --> I[抛出BusinessException]
F --> I
G --> I
H --> I
I --> J[Handler捕获处理]
J --> K[返回JSON响应]
3. 日志记录流程
graph TD
A[异常发生] --> B[Handler.register()]
B --> C[LogException::safeLog()]
C --> D[尝试single通道]
D --> E{记录成功?}
E -->|是| F[记录完成]
E -->|否| G[尝试daily通道]
G --> H{记录成功?}
H -->|是| F
H -->|否| I[尝试errorlog通道]
I --> J{记录成功?}
J -->|是| F
J -->|否| K[系统error_log兜底]
使用方法
1. 控制器中抛出业务异常
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\BaseController;
use App\Exceptions\Handler;
use App\Helpers\ResponseEnum;
use Illuminate\Http\Request;
class UserController extends BaseController
{
public function show(Request $request, $id)
{
// 方法1: 使用辅助方法
if (empty($id)) {
Handler::throwParamError('用户ID不能为空');
}
$user = User::find($id);
if (!$user) {
Handler::throwNotFound('用户不存在');
}
// 方法2: 直接抛出BusinessException
if ($user->status === 0) {
throw new \App\Exceptions\BusinessException(
ResponseEnum::CLIENT_HTTP_UNAUTHORIZED,
'用户已被禁用'
);
}
return $this->SuccessObject($user);
}
public function store(Request $request)
{
$username = $request->input('username');
// 检查用户名是否已存在
if (User::where('username', $username)->exists()) {
Handler::throwDataExist('用户名已存在');
}
// 创建用户逻辑...
$user = User::create($request->all());
return $this->SuccessObject($user);
}
}
2. 服务类中使用异常
<?php
namespace App\Services;
use App\Exceptions\Handler;
use App\Models\User;
class UserService
{
public function updateUser($id, array $data)
{
$user = User::find($id);
if (!$user) {
Handler::throwNotFound('用户不存在');
}
// 检查权限
if (!$this->canUpdate($user)) {
Handler::throwError('无权修改此用户', 403001);
}
// 更新逻辑
$user->update($data);
return $user;
}
private function canUpdate(User $user): bool
{
// 权限检查逻辑
return true;
}
}
3. 中间件中使用异常
<?php
namespace App\Http\Middleware;
use App\Exceptions\Handler;
use Closure;
use Illuminate\Http\Request;
class CheckApiQuota
{
public function handle(Request $request, Closure $next)
{
$user = $request->user();
if (!$user) {
Handler::throwError('用户未认证', 401);
}
// 检查API调用额度
if ($this->exceedsQuota($user)) {
Handler::throwError('API调用次数已达上限', 429001);
}
return $next($request);
}
private function exceedsQuota($user): bool
{
// 额度检查逻辑
return false;
}
}
4. 模型中使用异常
<?php
namespace App\Models;
use App\Exceptions\Handler;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function changeStatus($status)
{
// 状态验证
if (!in_array($status, [0, 1])) {
Handler::throwParamError('无效的用户状态');
}
// 检查是否可以修改
if ($this->is_admin && $status === 0) {
Handler::throwError('不能禁用管理员账户', 403002);
}
$this->status = $status;
$this->save();
}
}
响应格式规范
1. 成功响应格式
{
"success": true,
"message": "success",
"code": 200,
"data": {
// 具体数据
}
}
2. 错误响应格式
基本错误响应
{
"success": false,
"message": "错误描述",
"code": 400200,
"data": null
}
参数验证错误
{
"success": false,
"message": "参数错误",
"code": 400200,
"data": null,
"errors": {
"username": ["用户名不能为空"],
"email": ["邮箱格式不正确"]
}
}
开发环境系统错误
{
"success": false,
"message": "具体错误信息",
"code": 500001,
"data": {
"exception": "Illuminate\\Database\\QueryException",
"file": "/path/to/file.php",
"line": 123,
"trace": "异常堆栈信息..."
}
}
生产环境系统错误
{
"success": false,
"message": "服务器错误",
"code": 500001,
"data": null
}
错误码规范
1. 错误码结构
错误码格式:XYYZZZ
- X: 错误类别 (4=客户端错误, 5=服务器错误)
- YY: 业务模块 (00=通用, 01=用户, 02=订单等)
- ZZZ: 具体错误 (001, 002, 003...)
2. 常用错误码
| 错误码 | HTTP状态码 | 说明 | 使用场景 |
|---|---|---|---|
| 200001 | 200 | 操作成功 | 正常业务响应 |
| 400200 | 422 | 参数错误 | 请求参数验证失败 |
| 400201 | 409 | 数据已存在 | 创建重复数据 |
| 400202 | 404 | 数据不存在 | 查询不到数据 |
| 401 | 401 | 授权失败 | Token无效或过期 |
| 403 | 403 | 权限不足 | 没有操作权限 |
| 404 | 404 | 页面不存在 | 路由不存在 |
| 405 | 405 | 请求方法错误 | HTTP方法不允许 |
| 429 | 429 | 请求频率超限 | 触发限流 |
| 500001 | 500 | 服务器错误 | 系统异常 |
3. 自定义错误码示例
// 在ResponseEnum中添加新的错误码
const USER_NOT_VERIFIED = [400101, '用户未验证'];
const USER_LOCKED = [400102, '用户账户已锁定'];
const ORDER_EXPIRED = [400201, '订单已过期'];
const PAYMENT_FAILED = [400301, '支付失败'];
const API_QUOTA_EXCEEDED = [429001, 'API调用次数超限'];
配置说明
1. 异常处理配置
文件: bootstrap/app.php
->withExceptions(function (Exceptions $exceptions): void {
// 配置不需要报告的异常类型
$exceptions->dontReport([
\App\Exceptions\BusinessException::class,
]);
// 全局API异常处理
$exceptions->render(function (Throwable $e, $request) {
if ($request->expectsJson() || $request->is('admin/*')) {
$handler = app(\App\Exceptions\Handler::class);
return $handler->render($request, $e);
}
});
})
2. 日志配置
文件: config/logging.php
'channels' => [
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => 14,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
],
],
3. 环境变量
.env 文件:
# 应用调试模式
APP_DEBUG=true # 开发环境
APP_DEBUG=false # 生产环境
# 日志级别
LOG_LEVEL=debug # 开发环境
LOG_LEVEL=error # 生产环境
# 错误报告
LOG_CHANNEL=daily
测试方法
1. 单元测试
<?php
namespace Tests\Unit;
use App\Exceptions\BusinessException;
use App\Exceptions\Handler;
use App\Helpers\ResponseEnum;
use Tests\TestCase;
class ExceptionHandlerTest extends TestCase
{
public function test_business_exception_response()
{
$this->expectException(BusinessException::class);
Handler::throwParamError('测试参数错误');
}
public function test_api_error_response()
{
$response = $this->postJson('/admin/auth/login', []);
$response->assertStatus(422)
->assertJson([
'success' => false,
'code' => 400200,
])
->assertJsonStructure([
'success',
'message',
'code',
'data',
'errors'
]);
}
}
2. 集成测试
public function test_authentication_error()
{
$response = $this->getJson('/admin/auth/me');
$response->assertStatus(401)
->assertJson([
'success' => false,
'code' => 401,
'message' => '授权失败,请先登录'
]);
}
public function test_not_found_error()
{
$response = $this->getJson('/admin/nonexistent');
$response->assertStatus(404)
->assertJson([
'success' => false,
'code' => 404
]);
}
3. 手动测试
测试参数验证错误
curl -X POST http://localhost:8000/admin/auth/login \
-H "Content-Type: application/json" \
-d '{}'
预期响应:
{
"success": false,
"message": "参数错误",
"code": 400200,
"data": null,
"errors": {
"username": ["The username field is required."],
"password": ["The password field is required."]
}
}
测试认证错误
curl -X GET http://localhost:8000/admin/auth/me \
-H "Authorization: Bearer invalid_token"
预期响应:
{
"success": false,
"message": "授权失败,请先登录",
"code": 401,
"data": null
}
性能优化
1. 异常处理性能
避免过度异常处理:
// ❌ 不好的做法 - 用异常控制正常流程
try {
$user = User::findOrFail($id);
} catch (ModelNotFoundException $e) {
return $this->Field('用户不存在');
}
// ✅ 好的做法 - 正常判断
$user = User::find($id);
if (!$user) {
Handler::throwNotFound('用户不存在');
}
缓存异常信息:
// 对于频繁抛出的业务异常,可以考虑缓存错误信息
class CachedBusinessException extends BusinessException
{
private static array $messageCache = [];
public function __construct(array $codeResponse, $info = '')
{
$key = md5(serialize($codeResponse) . $info);
if (!isset(self::$messageCache[$key])) {
self::$messageCache[$key] = $info ?: $codeResponse[1];
}
parent::__construct($codeResponse, self::$messageCache[$key]);
}
}
2. 日志性能优化
异步日志记录:
// 在config/logging.php中配置
'async' => [
'driver' => 'custom',
'via' => App\Logging\AsyncLoggerFactory::class,
'channel' => 'daily',
],
日志分级:
// 只在必要时记录详细信息
if (config('app.debug')) {
LogException::safeLog('详细调试信息', $e, $context);
} else {
LogException::safeLog('简化错误信息', $e);
}
监控和报警
1. 错误监控
统计异常频率:
// 可以添加到Handler中
protected function reportException(Throwable $e): void
{
// 统计异常类型和频率
$type = get_class($e);
Cache::increment("exception_count:{$type}");
// 记录异常趋势
$hour = now()->format('Y-m-d-H');
Cache::increment("exception_hourly:{$hour}");
parent::report($e);
}
关键异常报警:
protected function shouldAlert(Throwable $e): bool
{
// 系统异常立即报警
if (!($e instanceof BusinessException)) {
return true;
}
// 高频业务异常报警
$count = Cache::get("exception_count:" . get_class($e), 0);
return $count > 100; // 超过100次/小时报警
}
2. 性能监控
响应时间监控:
class ExceptionMiddleware
{
public function handle($request, Closure $next)
{
$start = microtime(true);
try {
return $next($request);
} catch (Throwable $e) {
$duration = microtime(true) - $start;
// 记录异常处理时间
Log::info('Exception handling time', [
'duration' => $duration,
'exception' => get_class($e)
]);
throw $e;
}
}
}
安全考虑
1. 信息泄露防护
生产环境信息过滤:
protected function filterSensitiveInfo(Throwable $e): array
{
if (!config('app.debug')) {
return [
'message' => '服务器内部错误',
'code' => 500001
];
}
// 过滤敏感路径
$file = str_replace(base_path(), '', $e->getFile());
return [
'message' => $e->getMessage(),
'file' => $file,
'line' => $e->getLine()
];
}
2. 日志安全
敏感信息脱敏:
protected function sanitizeLogData(array $context): array
{
$sensitive = ['password', 'token', 'secret', 'key'];
foreach ($context as $key => $value) {
if (in_array(strtolower($key), $sensitive)) {
$context[$key] = '***';
}
}
return $context;
}
注意事项
1. 开发注意事项
异常分类原则:
- 业务异常: 可预期的业务逻辑错误,使用BusinessException
- 系统异常: 不可预期的技术错误,让系统自动处理
- 验证异常: 参数格式错误,使用Laravel验证器
性能考虑:
- 避免在循环中频繁抛出异常
- 合理使用异常,不要用异常控制正常业务流程
- 异常信息要清晰明确,便于调试
2. 部署注意事项
环境配置:
- 生产环境必须关闭DEBUG模式
- 配置适当的日志轮转策略
- 设置日志文件权限
监控设置:
- 配置异常报警阈值
- 监控错误率和响应时间
- 定期检查日志文件大小
3. 维护注意事项
错误码管理:
- 统一错误码命名规范
- 定期清理无用的错误码
- 保持错误信息的一致性
日志管理:
- 定期清理旧日志文件
- 监控磁盘空间使用
- 备份重要错误日志
扩展开发
1. 自定义异常类型
<?php
namespace App\Exceptions;
class ValidationException extends BusinessException
{
public function __construct(array $errors, string $message = '参数验证失败')
{
$this->errors = $errors;
parent::__construct([422, $message]);
}
public function getErrors(): array
{
return $this->errors;
}
}
2. 异常处理中间件
<?php
namespace App\Http\Middleware;
class ExceptionHandlingMiddleware
{
public function handle($request, Closure $next)
{
try {
return $next($request);
} catch (BusinessException $e) {
// 业务异常特殊处理
return response()->json([
'success' => false,
'message' => $e->getMessage(),
'code' => $e->getCode(),
'timestamp' => now()->toISOString()
]);
}
}
}
3. 异常通知系统
<?php
namespace App\Services;
class ExceptionNotificationService
{
public function notify(Throwable $e): void
{
// 邮件通知
if ($this->shouldEmailNotify($e)) {
Mail::to(config('app.admin_email'))
->send(new ExceptionNotification($e));
}
// 钉钉/企微通知
if ($this->shouldInstantNotify($e)) {
$this->sendInstantMessage($e);
}
}
private function shouldEmailNotify(Throwable $e): bool
{
return !($e instanceof BusinessException) && config('app.env') === 'production';
}
}
技术支持
1. 常见问题
Q: 如何添加新的异常类型?
A: 在Handler的renderJsonException方法中添加新的异常处理逻辑,或者继承BusinessException创建自定义异常类。
Q: 如何修改异常响应格式?
A: 修改Handler的jsonResponse方法,调整返回的JSON结构。
Q: 生产环境异常信息太少,如何调试? A: 检查日志文件,或者临时开启DEBUG模式进行调试。
2. 调试技巧
查看详细异常信息:
# 查看最新的异常日志
tail -f storage/logs/laravel.log
# 搜索特定异常
grep "Exception" storage/logs/laravel-*.log
临时调试模式:
// 在特定方法中临时启用详细错误
if ($request->input('debug') === 'true' && auth()->user()->is_admin) {
config(['app.debug' => true]);
}
文档版本: v1.0
最后更新: 2024年12月
作者: 开发团队
审核: 技术负责人