Exception(异常)
Exception(异常)是什么
1.1 基本概念
Exception是程序运行时发生的异常情况的表示。当程序执行中遇到错误或意外情况时,就会抛出异常。
php
程序正常流程 ──→ 发生错误 ──→ 抛出Exception ──→ 中断执行 ──→ 寻找异常处理器(catch) ──→ 恢复或记录
1.2 Exception类
System.Exception作为所有异常的基类,是错误处理的核心机制
-
Message用来描述异常原因的详细信息- 如果你捕捉到了异常,一般使用这段描述能知道发生的大致原因。
- 如果你准备抛出异常,在这个信息里面记录能帮助调试问题的详细文字信息。
-
StackTrace包含用来确定错误位置的堆栈跟踪(当有调试信息如 PDB 时,这里就会包含源代码文件名和源代码行号) -
InnerException包含内部异常信息 -
Source这个属性包含导致错误的应用程序或对象的名称 -
Data这是一个字典,可以存放基于键值的任意数据,帮助在异常信息中获得更多可以用于调试的数据 -
HelpLink这是一个 url,这个 url 里可以提供大量用于说明此异常原因的信息
1.3 引发异常
引发异常使用 throw 关键字。只是注意如果要重新抛出异常,请使用 throw; 语句或者将原有异常作为内部异常。
php
catch (Exception ex)
{
//throw ex; //会丢失调用链,找不到真正的异常所在
//throw; //调用链完整
//ExceptionDispatchInfo.Capture(ex).Throw();//调用链更完整,显示了重新抛出异常所在的位置。
}
异常处理的本质
异常处理 = 异常拦截器
目标:拦截异常 → 消除真正的抛错 → 返回可控的结果 → 程序继续运行
❌ 没有处理:异常不断向上传播 → 最终击溃系统
✅ 有处理:异常被拦截 → 转化为返回值 → 系统继续运行
核心思想:
- 异常并不可怕,可怕的是让异常不受控制地传播
- try-catch 的作用就是在异常传播的过程中"截住"它
- 截住以后,你可以:记录日志、转成 JSON、转成状态码、忽略它...
- 但无论如何处理,目的都是一样的:消除真正的抛错
注意:
erlang
控制台项目程序中抛错直接导致程序退出
.net Core Web API项目抛错,有最底层的抛错拦截兜底,因此不会直接程序退出.
1. 异常的层次结构
1.1 . NET 异常体系
php
System.Exception (所有异常的基类)
│
├── SystemException (系统异常)
│ ├── NullReferenceException # 空引用
│ ├── IndexOutOfRangeException # 数组越界
│ ├── InvalidOperationException # 无效操作
│ ├── ArgumentException # 参数异常
│ │ ├── ArgumentNullException # 参数为null
│ │ └── ArgumentOutOfRangeException # 参数超范围
│ └── DivideByZeroException # 除零
│
└── ApplicationException (应用程序异常 - 不推荐直接使用)
└── 自定义业务异常...
1.2 异常继承关系重要性
php
// ❌ 不好的做法:捕获基类异常
try { ... }
catch (Exception ex)
{
// 什么异常都捕获,无法区别对待
}
// ✅ 好的做法:按异常类型分别处理
try { ... }
catch (ArgumentNullException ex)
{
// 处理参数为 null 的情况
}
catch (ArgumentException ex)
{
// 处理其他参数异常
}
catch (NotFoundException ex)
{
// 处理资源不存在
}
catch (Exception ex)
{
// 最后捕获未预期的异常
}
2. 异常传播机制(重要!)
2.1 异常的传播过程
arduino
调用栈(从上往下执行):
┌─────────────────────────────┐
│ Main() │ ← 最顶层(应用入口)
│ ↓ │
│ MethodA() │
│ ↓ │
│ MethodB() │
│ ↓ │
│ MethodC() ← throw new ... │ ← 异常发生点 💥
└─────────────────────────────┘
异常传播(从下往上逃逸):
💥 MethodC 产生异常
↓
❓ MethodC 没有 catch → 异常继续向上传播
↓
❓ MethodB 没有 catch → 异常继续向上传播
↓
❓ MethodA 没有 catch → 异常继续向上传播
↓
❓ Main 没有 catch → 异常继续向上传播
↓
❓ 框架最后一层 catch(如果有)→ 可能被拦截
↓
❌ 都没有 catch → 程序崩溃!
2.2 异常被拦截的情况
scss
// 在某一层被 catch 了
MethodC()
{
throw new Exception(); // 💥 异常产生
}
MethodB()
{
try
{
MethodC(); // 调用可能抛异常的方法
}
catch (Exception ex) // 🛑 异常在这里被拦截!
{
Console.WriteLine("异常已处理");
// 异常不再向上传播
}
Console.WriteLine("程序继续运行"); // ✅ 这行会执行
}
MethodA()
{
MethodB(); // 没有异常,正常执行
}
3. Try-Catch 基础用法
3.1 基本结构
csharp
try
{
// 可能抛出异常的代码
DoSomethingDangerous();
}
catch (SpecificException ex)
{
// 处理特定异常
Console.WriteLine($"捕获特定异常: {ex.Message}");
}
catch (Exception ex)
{
// 处理所有其他异常(范围最大,必须放在最后)
Console. WriteLine($"捕获通用异常: {ex.Message}");
}
finally
{
// 无论是否发生异常都会执行
// 通常用于资源清理(释放数据库连接、文件句柄等)
Console.WriteLine("清理资源");
}
// 执行顺序:
// 1. 执行 try 块
// 2. 如果有异常:执行匹配的 catch 块
// 3. 最后执行 finally 块
// 4. 继续执行后续代码
3.2 异常信息
csharp
try { ... }
catch (Exception ex)
{
// 常用属性
ex.Message // 异常描述信息
ex. StackTrace // 堆栈跟踪(调用链)
ex.InnerException // 内部异常(异常链)
ex.GetType().Name // 异常类型名称
// 实际使用
Console.WriteLine($"异常类型: {ex.GetType().Name}");
Console.WriteLine($"异常信息: {ex.Message}");
Console.WriteLine($"堆栈跟踪: {ex. StackTrace}");
}
3.3 Catch 的顺序很重要
php
// ❌ 错误的做法:通用异常在前面
try { ... }
catch (Exception ex) // ❌ 捕获所有异常
{
// 这会拦截下面所有的 catch,它们永远不会执行
}
catch (NotFoundException ex) // ❌ 永远到不了这里
{
// 死代码!
}
// ✅ 正确的做法:具体异常在前面,通用异常在后面
try { ... }
catch (NotFoundException ex) // ✅ 先处理具体异常
{
}
catch (ArgumentException ex) // ✅ 再处理更通用的异常
{
}
catch (Exception ex) // ✅ 最后处理最通用的异常
{
}
自定义异常与全局异常处理
自定义异常
csharp
/// <summary>
/// 业务异常基类
/// 所有自定义异常都继承这个类
/// </summary>
public abstract class BusinessException : Exception
{
/// <summary>
/// HTTP 状态码
/// 🔑 这是关键:异常类定义 HTTP 状态码是多少
/// 由具体子类覆盖这个属性来指定各自的状态码
///
/// 例如:
/// - NotFoundException 返回 404
/// - ValidationException 返回 400
/// - 这样中间件就知道应该返回什么状态码了
/// </summary>
public abstract int HttpStatusCode { get; }
/// <summary>
/// 错误代码(给前端的)
/// 例如:"NOT_FOUND", "VALIDATION_ERROR", "UNAUTHORIZED"
/// 前端可以根据这个代码做不同的处理
/// </summary>
public string ErrorCode { get; set; }
public BusinessException(string message, string errorCode)
: base(message)
{
ErrorCode = errorCode;
}
public BusinessException(string message, string errorCode, Exception innerException)
: base(message, innerException)
{
ErrorCode = errorCode;
}
}
csharp
/// <summary>
/// 资源未找到异常
///
/// 什么时候抛?
/// 当查询数据库,用户/产品/订单等资源不存在时
///
/// HTTP 状态码是多少?404
///
/// 错误代码是什么?NOT_FOUND
/// </summary>
public class NotFoundException : BusinessException
{
public override int HttpStatusCode => 404; // 🔑 关键:定义状态码是 404
public NotFoundException(string message)
: base(message, "NOT_FOUND") // 🔑 关键:定义错误代码
{
}
}
csharp
/// <summary>
/// 请求过于频繁异常(限流)
///
/// 什么时候抛?
/// 当用户请求太频繁,触发限流规则时
///
/// HTTP 状态码是多少?429 Too Many Requests
///
/// 错误代码是什么?TOO_MANY_REQUESTS
/// </summary>
public class TooManyRequestsException : BusinessException
{
public override int HttpStatusCode => 429; // 🔑 关键:定义状态码是 429
public TooManyRequestsException(string message)
: base(message, "TOO_MANY_REQUESTS") // 🔑 关键:定义错误代码
{
}
}
错误响应模型
csharp
/// <summary>
/// 错误响应模型
///
/// 当异常被处理后,中间件会生成这样的 JSON 响应返回给客户端
///
/// 示例:
/// {
/// "statusCode": 404,
/// "code": "NOT_FOUND",
/// "message": "用户 123 不存在",
/// "timestamp": "2026-01-14T10:30:45Z"
/// }
/// </summary>
public class ErrorResponse
{
/// <summary>
/// HTTP 状态码
/// 告诉客户端这是什么类型的错误
/// 例如:400(参数错误), 404(未找到), 429(太多请求), 500(服务器错误)
/// </summary>
public int StatusCode { get; set; }
/// <summary>
/// 错误代码
/// 前端可以根据这个代码做针对性的处理
/// 例如:
/// - code = "NOT_FOUND" → 显示"资源不存在"
/// - code = "VALIDATION_ERROR" → 显示"参数错误,请检查"
/// - code = "TOO_MANY_REQUESTS" → 显示"请求过于频繁,请稍后再试"
/// </summary>
public string Code { get; set; }
/// <summary>
/// 错误信息
/// 给用户看的,说明具体发生了什么
/// 例如:"用户 123 不存在"
/// </summary>
public string Message { get; set; }
/// <summary>
/// 时间戳
/// 记录异常发生的时间
/// </summary>
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}
全局异常处理中间件
csharp
/// <summary>
/// 异常处理中间件
///
/// 作用:
/// 1. 拦截整个应用中发生的异常
/// 2. 根据异常类型判断应该返回什么 HTTP 状态码
/// 3. 生成友好的 JSON 错误响应
/// 4. 返回给客户端
///
/// 为什么需要这个中间件?
/// - 如果不处理异常,程序会崩溃
/// - 如果每个 Controller 都自己处理异常,代码重复
/// - 有了中间件,所有异常都在这里统一处理
///
/// 执行位置?
/// 必须在 Program.cs 中最前面注册:
/// app.UseMiddleware<ExceptionHandlingMiddleware>();
/// 这样才能捕获所有下层代码的异常
/// </summary>
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(
RequestDelegate next,
ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
/// <summary>
/// 中间件的执行方法
///
/// 执行流程:
/// 1. try 块:执行下面所有的中间件和代码
/// 2. 如果没有异常:正常返回响应
/// 3. 如果有异常:catch 块捕获,进行处理
/// </summary>
public async Task InvokeAsync(HttpContext context)
{
try
{
// 🔑 执行下面所有的中间件和 Controller 代码
// 例如:路由、认证、授权、Controller...
// 如果这些地方抛异常,会直接跳到 catch 块
await _next(context);
}
catch (Exception ex)
{
// 🛑 所有异常都会在这里被捕获
_logger.LogError(ex, "发生未处理的异常");
// 调用异常处理方法
await HandleExceptionAsync(context, ex);
}
}
/// <summary>
/// 处理异常的方法
///
/// 逻辑:
/// 1. 根据异常的类型判断应该返回什么状态码和错误代码
/// 2. 生成 ErrorResponse 对象
/// 3. 设置 HTTP 响应状态码
/// 4. 返回 JSON 格式的错误信息
/// </summary>
private static async Task HandleExceptionAsync(
HttpContext context,
Exception exception)
{
// 设置响应的内容类型为 JSON
context.Response.ContentType = "application/json; charset=utf-8";
// 🔑 根据异常类型判断,生成不同的响应
ErrorResponse errorResponse;
int statusCode;
if (exception is BusinessException businessEx)
{
// 📌 业务异常的处理
// businessEx.HttpStatusCode 会根据异常类型返回不同的值
// 例如:
// - NotFoundException.HttpStatusCode = 404
// - TooManyRequestsException.HttpStatusCode = 429
statusCode = businessEx.HttpStatusCode;
errorResponse = new ErrorResponse
{
StatusCode = statusCode,
Code = businessEx.ErrorCode,
Message = businessEx.Message
};
}
else if (exception is ArgumentNullException argNullEx)
{
// 📌 参数为 null 的异常 → 400 Bad Request
statusCode = 400;
errorResponse = new ErrorResponse
{
StatusCode = 400,
Code = "BAD_REQUEST",
Message = $"参数不能为空: {argNullEx.ParamName}"
};
}
else if (exception is ArgumentException argEx)
{
// 📌 参数不合法的异常 → 400 Bad Request
statusCode = 400;
errorResponse = new ErrorResponse
{
StatusCode = 400,
Code = "BAD_REQUEST",
Message = argEx.Message
};
}
else
{
// 📌 未预期的异常 → 500 Internal Server Error
// 这个状态码表示服务器内部出错了
statusCode = 500;
errorResponse = new ErrorResponse
{
StatusCode = 500,
Code = "INTERNAL_SERVER_ERROR",
Message = "服务器内部错误,请稍后重试"
// 🔒 注意:不要暴露异常的原始信息给前端
// 这样可以保护系统的安全
};
}
// 设置 HTTP 响应的状态码
// 这个状态码非常重要,客户端会根据这个来判断请求是否成功
// 例如:
// - 200: 成功
// - 400: 客户端错误(参数问题)
// - 404: 资源未找到
// - 429: 请求过于频繁
// - 500: 服务器错误
context.Response.StatusCode = statusCode;
// 将错误响应序列化为 JSON 并返回给客户端
await context. Response.WriteAsJsonAsync(errorResponse);
}
}
注册中间件
scss
var app = builder.Build();
// 🔑 【关键】异常处理中间件必须在最前面注册
// 这样才能捕获下面所有中间件和代码的异常
app.UseMiddleware<ExceptionHandlingMiddleware>();
// 其他中间件...
app.UseRouting();
app.MapControllers();
app.Run();
管道处理流程
csharp
═══════════════════════════════════════════════════════════════════
【请求进来 - 向下流动】
═══════════════════════════════════════════════════════════════════
客户端请求
↓
GET /api/user/123
↓
┌────────────────────────────────────────────────┐
│ Pipeline:中间件管道 (按顺序) │
├────────────────────────────────────────────────┤
│ │
│ 【第1层】异常处理中间件 │
│ ┌─────────────────────────────────────────┐ │
│ │ try { │ │
│ │ await _next(context) ← 进入 try 块 │ │
│ │ │ │
│ │ } catch (Exception ex) { │ │
│ │ // 将在这里捕获下面所有的异常 │ │
│ │ } │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ 【第2层】日志中间件 │
│ ├─ 记录请求信息 │
│ └─ await _next(context) │
│ ↓ │
│ 【第3层】认证中间件 │
│ ├─ 检查 Token │
│ ├─ 验证用户身份 │
│ └─ await _next(context) │
│ ↓ │
│ 【第4层】授权中间件 │
│ ├─ 检查权限 │
│ └─ await _next(context) │
│ ↓ │
│ 【第5层】路由中间件 │
│ ├─ 匹配路由规则 │
│ └─ 确定使用哪个 Controller │
│ │
└────────────────────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ 【Controller】控制器 │
├─────────────────────────────────┤
│ │
│ [HttpGet("{id}")] │
│ public IActionResult Get(int id)
│ { │
│ // 从 DI 容器获取服务 │
│ var user = _userService │ ← DI 容器注入
│ . GetUser(id); │
│ │
│ return Ok(user); │
│ } │
│ │
└─────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ 【Service 层】业务逻辑 │
├──────────────────────────────────────┤
│ │
│ public User GetUser(int id) │
│ { │
│ // 参数验证 │
│ if (id <= 0) │
│ throw new ValidationException();│
│ │
│ // 从 DI 容器获取 Repository │
│ var user = _userRepository │ ← DI 容器注入
│ .GetById(id); │
│ │
│ if (user == null) │
│ throw new NotFoundException(); │
│ │
│ return user; │
│ } │
│ │
└──────────────────────────────────────┘
↓
┌───────────────────────────────────────────┐
│ 【Repository 层】数据访问 │
├───────────────────────────────────────────┤
│ │
│ public User GetById(int id) │
│ { │
│ return _dbContext.Users.Find(id); │
│ } │
│ │
└───────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────┐
│ 【数据库 / 外部系统】 │
├──────────────────────────────────────────────┤
│ │
│ SQL Server / MySQL / MongoDB / API │
│ │
│ ✅ 成功:返回数据 │
│ ❌ 异常:抛出异常 (连接超时、查询失败等) │
│ │
└──────────────────────────────────────────────┘
↓
═══════════════════════════════════════════════════════════════════
【异常向上传播 - 上升流动】
═══════════════════════════════════════════════════════════════════
💥 异常产生(数据库)
↑
【Repository】抛异常向上
(没有 catch)
↑
【Service】捕获?通常再抛异常
┌─ try { _repo.GetById() }
└─ catch { throw new BusinessException(...) }
↑
【Controller】捕获?通常不处理
(假设 Service 抛了异常)
↑
┌────────────────────────────────────────────────┐
│ Pipeline:中间件管道 (向上返回) │
├────────────────────────────────────────────────┤
│ │
│ 【第5层】路由中间件 │
│ ├─ 异常向上传播 │
│ └─ catch?没有 │
│ ↑ │
│ 【第4层】授权中间件 │
│ ├─ 异常向上传播 │
│ └─ catch?没有 │
│ ↑ │
│ 【第3层】认证中间件 │
│ ├─ 异常向上传播 │
│ └─ catch?没有 │
│ ↑ │
│ 【第2层】日志中间件 │
│ ├─ 可能记录异常 │
│ └─ catch?可能有,但通常只是记录后继续抛 │
│ ↑ │
│ 【第1层】异常处理中间件 │
│ ┌─────────────────────────────────────────┐ │
│ │ try { │ │
│ │ await _next(context) │ │
│ │ │ │
│ │ } catch (Exception ex) { │ │
│ │ 🛑 异常在这里被终止! │ │
│ │ │ │
│ │ // 判断异常类型 │ │
│ │ if (ex is ValidationException) │ │
│ │ return 422; │ │
│ │ else if (ex is NotFoundException) │ │
│ │ return 404; │ │
│ │ else │ │
│ │ return 500; │ │
│ │ │ │
│ │ // 生成错误响应 │ │
│ │ await context.Response │ │
│ │ .WriteAsJsonAsync(errorResponse); │ │
│ │ } │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ 异常处理完毕,不再向上 │
│ 返回错误响应给客户端 │
│ │
└────────────────────────────────────────────────┘
↓
HTTP 响应
(状态码 + JSON 体)
↓
【客户端接收】
═══════════════════════════════════════════════════════════════════
关键要点
═══════════════════════════════════════════════════════════════════
✅ 【DI 容器】
- Service 通过 DI 容器从 Controller 获取
- Repository 通过 DI 容器从 Service 获取
- 所有依赖都由容器管理,解耦合
✅ 【异常传播链】
数据库异常
↑
Repository 抛异常
↑
Service 捕获 → 转换为 BusinessException → 继续抛
↑
Controller(通常不处理)
↑
中间件们(通常不处理)
↑
异常处理中间件 🛑 最终捕获!
✅ 【为什么异常处理中间件在最顶层】
- 因为异常是向上传播的
- 越往顶层越能捕获更多的异常
- 第1层的 catch 是整个应用的最后防线
- 确保没有异常"逃脱"
✅ 【处理流程小结】
请求向下 → 经过管道 → 到达 Controller
→ 调用 Service(通过 DI)
→ Service 调用 Repository(通过 DI)
→ Repository 调用数据库
异常向上 → 从数据库/业务逻辑产生
→ 向上传播...
→ 被异常处理中间件 catch 🛑 终止
→ 返回错误响应
═══════════════════════════════════════════════════════════════════
为什么抛错能被读懂和接收
context.Response.StatusCode = statusCode;
markdown
【解释】
1. context(上下文)
- 代表当前的 HTTP 请求和响应
- 包含了请求的所有信息(URL、参数、Body 等)
- 也包含了即将返回给客户端的响应信息
2. Response(响应)
- HTTP 响应对象
- 用来设置返回给客户端的内容
3. StatusCode(状态码)
- 这是响应的状态行中的数字
- 它告诉客户端:"这个请求的结果如何?"
4. = statusCode(赋值)
- 将变量 statusCode 的值赋给 Response. StatusCode
- 例如:statusCode = 404,就设置为 HTTP 404
await context. Response.WriteAsJsonAsync(errorResponse);
javascript
1. context. Response(响应对象)
- 这是即将返回给客户端的 HTTP 响应
- 我们要把内容写入这个对象
2. WriteAsJsonAsync(异步写入 JSON)
- 将对象序列化(转换)为 JSON 格式
- "异步"意味着这是一个异步操作,需要 await
- 需要等待数据写入完成
3. errorResponse(错误响应对象)
- 一个 ErrorResponse 对象,包含:
{
"statusCode": 404,
"code": "NOT_FOUND",
"message": "用户 123 不存在",
"timestamp": "2026-01-14..."
}
4. 序列化过程
ErrorResponse 对象
↓
转换为 JSON 字符串
↓
写入 HTTP 响应体
↓
客户端接收
【完整过程】
var errorResponse = new ErrorResponse
{
StatusCode = 404,
Code = "NOT_FOUND",
Message = "用户 123 不存在",
Timestamp = DateTime. UtcNow
};
如上:
客户端http接收到后可以根据状态码和得到的返回json,知道当前请求的状态和数据
ABP异常处理
kotlin
public class AbpExceptionHandlingMiddleware : AbpMiddlewareBase, ITransientDependency
ABP封装了异常处理中间件
自定义异常
csharp
public class AbpRateLimitException : BusinessException,IHasHttpStatusCode
{
public int RetryAfterSeconds { get; set; } = 60;
public AbpRateLimitException(int retryAfterSeconds = 60)
: base("请求过于频繁,请稍后再试。","RateLimitException")
{
RetryAfterSeconds = retryAfterSeconds;
}
public int HttpStatusCode => 429;
}
Error: response status is 429
Response body
{
"error": {
"code": "请求过于频繁,请稍后再试。",
"message": "对不起,在处理您的请求期间产生了一个服务器内部错误!!",
"details": null,
"data": {},
"validationErrors": null
}
}
csharp
public class AbpRateLimitException : UserFriendlyException,IHasHttpStatusCode
{
public int RetryAfterSeconds { get; set; } = 60;
public AbpRateLimitException(int retryAfterSeconds = 60)
: base($"请求过于频繁,请{retryAfterSeconds}秒后再试。","RateLimitException")
{
RetryAfterSeconds = retryAfterSeconds;
// ✅ 直接设置 Data 字典,扩展字段数据,来至于Exception
//this.Data["retryAfterSeconds"] = retryAfterSeconds;
//使用内置的WithData 方法
WithData("retryAfterSeconds",retryAfterSeconds);
}
public int HttpStatusCode => 429;
}
Error: response status is 429
Response body
{
"error": {
"code": "RateLimitException",
"message": "请求过于频繁,请稍后再试。",
"details": null,
"data": {},
"validationErrors": null
}
}
//继承的ABP封装的异常类不同,Code/message也不同,同时非UserFriendlyException,会被覆盖message数据,只有在log中才能看到.
拓展
Http状态码
┌──────────┬──────────────────────┬─────────────────────┐ │ 状态码 │ 含义 │ 何时使用 │ ├──────────┼──────────────────────┼─────────────────────┤ │ 200 │ OK(成功) │ GET, PUT, PATCH 成功 │ │ 201 │ Created(已创建) │ POST 创建成功 │ │ 204 │ No Content(无内容) │ DELETE 成功 │ ├──────────┼──────────────────────┼─────────────────────┤ │ 400 │ Bad Request(坏请求)│ 参数验证失败 │ │ 401 │ Unauthorized(未授权)│ 未登录或 Token 过期 │ │ 403 │ Forbidden(禁止) │ 已登录但权限不足 │ │ 404 │ Not Found(未找到) │ 资源不存在 │ │ 409 │ Conflict(冲突) │ 数据冲突 │ │ 429 │ Too Many(太多) │ 请求过于频繁(限流)│ ├──────────┼──────────────────────┼─────────────────────┤ │ 500 │ Server Error(服务器错误) │ 未处理的异常 │ │ 502 │ Bad Gateway(网关错误) │ 上游服务故障 │ │ 503 │ Unavailable(不可用)│ 服务维护或过载 │ │ 504 │ Timeout(超时) │ 请求处理超时 │ └──────────┴──────────────────────┴─────────────────────┘
为什么ASP.NET Core框架不会因为异常而崩溃?
架构对比
| 框架 | 运行模式 | 异常处理 | 结果 |
|---|---|---|---|
| ASP.NET Core | 服务器进程(Web API 后端) | ✅ 框架内置异常捕获 | 异常不导致进程崩溃 |
| WinForms | 桌面应用(单个进程) | ❌ 没有内置异常捕获 | 主线程异常 → UI 冻结/崩溃 |
| WPF | 桌面应用(单个进程) | ❌ 没有内置异常捕获 | 主线程异常 → UI 冻结/崩溃 |
| Console | 控制台应用 | ❌ 没有内置异常捕获 | 异常 → 应用直接退出 |
关键原因
1️⃣ 服务器的必要性(为什么 ASP.NET Core 必须处理异常)
ASP.NET Core:
进程启动 → 持续监听 → 接收多个 HTTP 请求 → 需要 24/7 运行
必要条件:
✓ 一个请求出错,不能影响其他请求
✓ 一个请求异常,不能导致进程崩溃
✓ 必须有异常隔离机制
→ 所以框架内置了异常捕获机制
2️⃣ 请求隔离(ASP.NET Core 的核心优势)
vbscript
Request 1 → 异常 → 捕获 → 返回 500 ✓
Request 2 → 正常 → 返回 200 ✓
Request 3 → 异常 → 捕获 → 返回 500 ✓
每个请求独立处理,互不影响!
3️⃣ WinForms/WPF/Console 为什么会崩溃
css
WinForms 单线程模型:
Main() 线程 → UI 事件处理 → 异常 → 线程停止 → 应用崩溃
原因:
❌ 没有请求隔离(没有多个请求概念)
❌ UI 线程就是唯一的处理线程
❌ 框架不需要保证服务可用性(桌面应用没有这个要求)
Kestrel 服务器层的异常处理
css
HTTP 请求到达
↓
Kestrel(Web 服务器)← 最底层异常捕获点
↓
ASP.NET Core Middleware Pipeline
↓
Your Code(Controller、Service)
↓
异常发生 → 逐层往上抛 → Kestrel 捕获 → 返回 500 → 进程继续运行
框架默认做的:
- 任何未处理的异常都会被 Kestrel 捕获
- 返回 HTTP 500 错误响应
- 进程不会退出
框架内置异常处理的限制与自定义全局异常中间件
vbscript
框架能自动处理的异常:
✅ 数据验证失败 → 400 Bad Request
✅ 模型绑定失败 → 400 Bad Request
✅ 路由不匹配 → 404 Not Found
框架无法细致处理的异常:
❌ 业务逻辑异常 → 都返回 500(不管什么原因)
❌ 数据库连接失败 → 都返回 500
❌ 权限不足异常 → 都返回 500(应该返回 401)
❌ 资源不存在 → 都返回 500(应该返回 404)
所以需要自定义异常处理中间件来区分处理
总结
| 点 | 说明 |
|---|---|
| 为什么 ASP.NET Core 稳定 | 服务器架构 + 请求隔离 + 框架内置异常捕获 |
| 为什么 WinForms 容易崩溃 | 桌面架构 + 单线程 + 框架不处理异常 |
| 框架默认处理 | 只是"保命",防止进程崩溃,但返回值都是 500 |
| 需要自定义中间件 | 为了区分异常、自定义返回格式、记录日志、提升用户体验 |
WinForms/WPF 的全局异常处理
WinForms/WPF没有中间件或者全局异常处理兜底,因此需要使用其它的方式处理全局异常保证程序稳定
1 WinForms
csharp
// Program.cs
static class Program
{
[STAThread]
static void Main()
{
// ✅ UI 线程异常处理
Application.ThreadException += Application_ThreadException;
// ✅ 后台线程异常处理
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
ApplicationConfiguration.Initialize();
Application.Run(new Form1());
}
private static void Application_ThreadException(
object sender,
System.Threading.ThreadExceptionEventArgs e)
{
Exception ex = e.Exception;
// ✅ 拦截异常
MessageBox.Show(
$"发生错误: {ex.Message}",
"异常",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
_logger.LogError(ex, "应用异常");
// e. Handled = true 表示异常被处理,程序继续运行
e. Handled = true;
}
private static void CurrentDomain_UnhandledException(
object sender,
UnhandledExceptionEventArgs e)
{
Exception ex = (Exception)e.ExceptionObject;
MessageBox.Show(
$"严重错误: {ex.Message}",
"严重错误",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
// 这里无法通过 e. Handled 来保留程序继续运行
// 通常会导致程序退出
}
}
// Form1.cs
public partial class Form1 : Form
{
private void button_Click(object sender, EventArgs e)
{
try
{
int result = 10 / 0;
MessageBox.Show(result.ToString());
}
catch (Exception ex)
{
MessageBox.Show($"错误: {ex.Message}");
}
}
}
2 WPF
csharp
// App.xaml. cs
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// ✅ UI 线程异常处理
this. DispatcherUnhandledException += App_DispatcherUnhandledException;
// ✅ 后台线程异常处理
AppDomain.CurrentDomain.UnhandledException +=
CurrentDomain_UnhandledException;
}
private void App_DispatcherUnhandledException(
object sender,
System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
Exception ex = e.Exception;
MessageBox.Show(
$"应用程序错误: {ex.Message}",
"错误",
MessageBoxButton.OK,
MessageBoxImage.Error);
// e.Handled = true 表示异常被处理,程序继续运行
e. Handled = true;
}
private void CurrentDomain_UnhandledException(
object sender,
UnhandledExceptionEventArgs e)
{
Exception ex = (Exception)e.ExceptionObject;
MessageBox. Show(
$"严重错误: {ex.Message}",
"严重错误",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
}