Exception异常架构设计:异常处理(04)

3 阅读6分钟

在前三篇中,已经讨论了:异常设计原则、异常分类模型、异常抛出策略。本篇回答两个个更重要的问题:

  • 异常在系统中如何流动?
  • 异常应该在哪里被处理?

异常处理,本质是架构边界设计。

异常处理的基本原则

异常处理核心架构准则是:决策中心化处理,流经过程保持透明。

战略性晚处理 (Strategic Catching)

异常应尽可能传递到具备“决策能力”的层级再处理。

  • 中间层级(如 Repository/转发层) 通常不具备业务决策权,只需让异常透明穿透。
  • 最终处理层(如 Service/Global Handler) 拥有全局视野,能决定是重试、执行降级逻辑,还是反馈给用户。
  • 核心价值: 避免业务处理逻辑碎片化,让异常治理逻辑收敛。

禁止隐式消化 (No Swallowing)

除非明确知道该异常可以被安全忽略,否则绝不允许捕获后不采取任何行动。

  • 处理必须产生副作用:要么业务恢复(Retry/Fallback),要么语义转换(Wrap),要么证据留存(Log)。
  • 任何空的 try-except 都是系统的隐患,会导致系统进入“不可知的故障状态”。

带上下文的语义转换 (Context Enrichment)

在涉及跨架构边界捕获异常时,应赋予其更清晰的业务内涵。

  • 规则: 将技术驱动的异常(如 TimeoutError)转换为业务驱动的异常(如 ExternalServiceUnavailable)。
  • 要求: 转换时必须保留原始异常链(Root Cause),并补充关键业务上下文(如 order_id),以便高层决策和排查。

收敛出口 (Single Exit)

系统最终应对外呈现单一的异常出口(Global Handler)。

  • 统一处理所有未被消化的异常,负责响应格式化、错误日志审计以及内部堆栈脱敏。
  • 确保系统不会因为细碎的本地处理而产生不一致的错误反馈模型。

异常的流动路径

外部请求如 REST API、RPC、消息队列等视为平台层,类比 Repository 层

在典型三层架构(Controller-Service-Repository)中,用户请求链路:

Client -> Controller -> Service -> Repository -> Database

异常流链路:

Database → Repository → Service → Controller → ErrorHandler

当然,最深处的异常在 Database 层,任何一层都可以发生异常,快速失败。

异常的流动模型如下,从调用堆栈的角度理解:

Client
  ↓
Controller
  ↓
Service
  ↓
Repository
  ↑
Exception
  ↑
Global Error Handler
  ↑
Client

异常自下而上冒泡,最终在统一出口收敛。这是异常的“漏斗模型”:

  • 底层负责报告问题;
  • 中层负责理解语义;
  • 高层负责统一出口。

分层职责边界

异常处理是否清晰,取决于分层是否清晰。

Repository —— 技术问题的报告者

Repository 层只负责技术访问:

  • 数据库
  • 缓存
  • 文件系统
  • 第三方 SDK

它不具备业务语义,因此它的职责只有一个:如实上抛技术异常。它不应:转换为业务异常、屏蔽底层异常、做业务推断。

底层只负责“报告”,不负责“解释”。

Service —— 语义的转换层

Service 是唯一同时理解:技术语义、业务语义的层级。

因此,异常的语义转换必须发生在 Service 层。它负责:

  • 异常上抛:将技术异常转换为业务语义异常
  • 异常消化:进行补救或降级

Service 是异常语义的边界。

Controller —— 透明传递层

Controller 的职责应保持最小化:接收请求、调用 Service、返回结果。Controller 不应承担异常处理逻辑。如果 Controller 开始处理异常,说明分层已经被破坏。

Controller 应保持透明。

全局异常处理层 —— 唯一出口

开发框架中如SpringBoot中全局异常处理层属于拦截器 Interceptor

所有未被 Service 消化的异常,最终汇聚到统一的异常处理层。它的职责包括:分类异常、统一输出结构、屏蔽内部实现细节、保证对外语义一致。

一个系统只能有一个异常出口。如果异常在多个地方被分散处理,错误模型会迅速失控。

职责汇总

层级角色核心职责
Repository技术报告者上抛技术异常
Service语义转换者防御消化异常,或语义转换并上抛业务异常
Controller透明传递者不处理异常,仅透传
Global Handler统一出口统一分流与输出

因此,异常处理仅发生在2个部分:

  • Service 层:重试、降级,但建议也同步向上抛出异常;
  • Global Handler:统一对异常进行分流与输出。

声明式异常处理

异常处理有2种方式:

  • 编程式异常处理:try-except 异常碎片化
  • 声明式异常处理:errorhandler 异常中心化

❌ 不推荐:try-except 异常碎片化

这种方式会导致业务逻辑被模板代码淹没,且每个接口返回的错误格式可能不一致。

# Service 层
def get_user(user_id):
    try:
        return db.find_user(user_id)
    except DBError as e:
        # 在这里记录日志
        logger.error(e)
        # 还要决定返回什么
        return None

# Controller 层
@app.get("/users/{id}")
def handle_get_user(id):
    user = service.get_user(id)
    if user is None:
        return {"code": 404, "msg": "用户没找到"} # 业务逻辑和错误处理混在一起
    return user

✅ 推荐:errorhandler 声明式异常处理

业务代码只关注“正确路径”,异常由框架统一兜底。

# --- 1. 业务代码保持“干净” ---

# Service 层:只负责抛出,不负责捕获
def get_user(user_id):
    user = db.find_user(user_id)
    if not user:
        # 直接抛出自定义业务异常,不写 try-except
        raise ResourceException(f"User {user_id} not found")
    return user

# Controller 层:透明转发,完全不写 try-except
@app.get("/users/{id}")
def handle_get_user(id):
    return service.get_user(id)

# --- 2. 全局异常处理器:统一收敛逻辑 ---

@app.exception_handler(AppBaseException)
async def global_exception_handler(request, exc: AppBaseException):
    # 1. 统一记录日志(证据留存)
    logger.error(f"Path: {request.url} | Error: {exc.message} | Code: {exc.error_code}")
    
    # 2. 统一返回格式(收敛出口)
    return JSONResponse(
        status_code=exc.http_status,
        content={
            "success": False,
            "error_code": exc.error_code,
            "message": exc.message,
            "request_id": request.state.request_id # 链路追踪
        }
    )

这种方式是非常合理的,它的核心价值在于:

  • 逻辑解耦:Service 层只需表达“我想做什么”和“什么条件下我不做了”,而不需要关心“报错后前端要看什么样式的 JSON”。
  • 保证一致性:整个系统数千个接口,通过一个 Global Handler 就能保证输出的 JSON 结构、状态码规范、脱敏规则完全统一。
  • 减少代码腐烂:避免了 try-except 块带来的代码缩进嵌套,使业务主流程(Happy Path)清晰可见。
  • 强制 Fail-Fast:一旦出现问题立即中断,异常携带完整的堆栈冒泡,不会出现“虽然报错了但代码还在往后跑”导致的脏数据。

这种异常处理方式为可观测性异常治理奠定了基础。

总结

异常处理不是语法技巧,而是架构设计。成熟系统的标志,不是“没有异常”,而是:

  • 异常可以被清晰地记录;
  • 异常可以被统一地收敛;
  • 异常不会破坏分层边界。

当异常的流动路径清晰,系统复杂度才是可控的。至此,异常架构设计的五部分已经全部设计完毕。下一篇,将继续讨论异常架构设计如何编程落地到软件项目中。

关注微信公众号,获取运维资讯

如果此篇文章对你有所帮助,感谢你的点赞收藏,也欢迎在评论区友好交流。

微信搜索关注公众号:持续运维