Exception异常与异常处理(.Net)

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异常处理

abp.io/docs/latest...

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);
    }
}
相关推荐
千寻技术帮2 小时前
10340_基于Springboot的游戏网站
spring boot·后端·游戏·vue·商城
于顾而言2 小时前
【一文带你搞懂】漏扫核弹Nuclei
后端
WX-bisheyuange2 小时前
基于SpringBoot的诊疗预约平台
java·spring boot·后端·毕业设计
SimonKing2 小时前
基于Netty的WebSocket客户端
java·后端·程序员
麦兜*3 小时前
Spring Boot 整合 Spring Data JPA 入门:只需注解,告别 SQL
spring boot·后端·sql
Sally璐璐3 小时前
RESTful与RPC接口终极对比指南
后端·rpc·restful
J_liaty3 小时前
前后端跨域处理全指南:Java后端+Vue前端完整解决方案
java·前端·vue.js·spring boot·后端
颜淡慕潇3 小时前
深度解读 Spring Boot 3.5.9— 工程视角的稳健演进与价值释放
java·spring boot·后端·spring
玄〤3 小时前
黑马点评中的分布式锁设计与实现(Redis + Redisson)
java·数据库·redis·笔记·分布式·后端