--- url: /sre/devops/exception/exception-08.md description: 在 06篇 中规划了异常文件结构,07篇 中完成了异常类和错误码的设计实现。 --- 在 [06篇](exception-06.md) 中规划了异常文件结构,[07篇](exception-07.md) 中完成了异常类和错误码的设计实现。本篇是异常编程实践系列的最后一篇,解决**异常模块怎么用**的问题——通过全局异常处理器将所有组件串联起来,一行代码完成项目集成。 ## 全局异常处理器 在 [04篇](exception-04.md) 中,我们确立了声明式异常处理的理念:业务代码只关注"正确路径",异常由框架统一兜底。现在将其落地到 `handler.py` 中。 ### 处理器实现 ```python # exception/handler.py import logging from flask import jsonify from .base import AppBaseException from .client import ClientException from .business import BizException from .server import ServerException, UnknownException logger = logging.getLogger(__name__) def register_error_handlers(app): """ 统一注册全局异常处理器 在 Flask app 初始化时调用:register_error_handlers(app) """ @app.errorhandler(ClientException) def handle_client_error(e): """客户端异常:记录 warning 级别日志""" logger.warning(f"客户端异常: {e}") return jsonify(e.to_dict()), e.http_status @app.errorhandler(BizException) def handle_biz_error(e): """业务异常:记录 info 级别日志""" logger.info(f"业务异常: {e}") return jsonify(e.to_dict()), e.http_status @app.errorhandler(ServerException) def handle_server_error(e): """服务端异常:记录 error 级别日志,包含完整堆栈""" logger.error(f"服务端异常: {e}", exc_info=True) return jsonify(e.to_dict()), e.http_status @app.errorhandler(Exception) def handle_unknown_error(e): """未知异常兜底:转换为 UnknownException,记录 critical 日志""" logger.critical(f"未知异常: {type(e).__name__}: {e}", exc_info=True) unknown = UnknownException(message="系统繁忙,请稍后重试") return jsonify(unknown.to_dict()), 500 ``` ### 日志级别策略 不同类型的异常,日志级别应当有所区分: | 异常类型 | 日志级别 | 理由 | | --- | --- | --- | | 客户端异常 | `WARNING` | 调用方的问题,需要关注但非紧急 | | 业务异常 | `INFO` | 正常的业务逻辑分支,属于预期行为 | | 服务端异常 | `ERROR` | 服务自身故障,需要及时排查 | | 未知异常 | `CRITICAL` | 未预见的风险,需要最高优先级关注 | 这与 [00篇](exception-00.md) 中的**可观测性**原则一脉相承:不同级别的异常触发不同的监控告警策略。`WARNING` 和 `INFO` 可做趋势分析,`ERROR` 应触发工单,`CRITICAL` 应立即通知值班人员。 ### 处理器的匹配优先级 回顾 [01篇](exception-01.md) 中介绍的异常处理优先级:具体异常类 → 父异常类 → `Exception`。这意味着: ```text raise ValidationException(...) → 匹配 ClientException 处理器(ValidationException 是 ClientException 的子类) raise BizException(...) → 匹配 BizException 处理器 raise RuntimeError(...) → 匹配 Exception 兜底处理器(转换为 UnknownException) ``` 因此,我们只需注册**三个层级处理器 + 一个兜底处理器**,就能覆盖所有异常场景。无需为每个具体的异常子类(如 `ValidationException`、`AuthenticationException`)单独注册处理器,Python 的异常继承机制会自动向上匹配。 ### 处理器的设计粒度 在 [06篇](exception-06.md) 中我们留下了一个问题:`handler.py` 只有一个文件,但异常类有十几个,handler 是为每个异常类写一个处理器,还是按类别统一处理? 这里有三种设计思路: **❌ 过度通用:一个处理器兜一切** ```python @app.errorhandler(Exception) def handle_all(e): if isinstance(e, ClientException): logger.warning(f"客户端异常: {e}") return jsonify(e.to_dict()), e.http_status elif isinstance(e, BizException): logger.info(f"业务异常: {e}") return jsonify(e.to_dict()), e.http_status elif isinstance(e, ServerException): logger.error(f"服务端异常: {e}", exc_info=True) return jsonify(e.to_dict()), e.http_status else: logger.critical(f"未知异常: {e}", exc_info=True) return jsonify({"message": "系统繁忙"}), 500 ``` 大量 `isinstance` 判断,本质上是在手写分发逻辑,没有利用框架的异常匹配机制,违背了声明式处理的初衷。 **❌ 过度具体:每个异常类一个处理器** ```python @app.errorhandler(ValidationException) def handle_validation(e): ... @app.errorhandler(AuthenticationException) def handle_auth(e): ... @app.errorhandler(AuthorizationException) def handle_authz(e): ... @app.errorhandler(ResourceException) def handle_resource(e): ... # ... 十几个处理器,逻辑几乎一模一样 ``` 处理器膨胀,而且这些处理器的逻辑几乎完全相同(都是日志 + `jsonify(e.to_dict())` + `e.http_status`),唯一的差异是日志级别——而日志级别的差异恰恰取决于异常的**类别**(客户端/业务/服务端),而非具体的异常类型。 **✅ 推荐:按处理策略分组** ```python @app.errorhandler(ClientException) # 覆盖所有 4xx 子类 @app.errorhandler(BizException) # 覆盖所有 299 子类 @app.errorhandler(ServerException) # 覆盖所有 5xx 子类 @app.errorhandler(Exception) # 兜底 ``` 这正是本篇开头的设计。其核心原则是:**handler 的注册粒度应与处理策略的差异对齐,而非与异常类的数量对齐。** 同一类别下的异常,处理策略确实相同: | 处理策略 | ClientException 及其子类 | BizException 及其子类 | ServerException 及其子类 | | --- | --- | --- | --- | | 日志级别 | `WARNING` | `INFO` | `ERROR` | | 是否记录堆栈 | 否 | 否 | 是(`exc_info=True`) | | 是否触发告警 | 趋势监控 | 不告警 | 即时告警 | | 返回 message | 直接使用原始 message | 直接使用原始 message | 脱敏后返回通用提示 | `ValidationException` 和 `AuthenticationException` 虽然是不同的异常类,但它们的处理策略完全一致(都是 WARNING + 返回 4xx),所以共享同一个 `ClientException` 处理器即可。 **扩展场景**:如果未来某个具体子类需要差异化处理(例如 `AuthenticationException` 需要额外清除 Session),只需为它单独注册一个处理器。Flask 的异常匹配机制会**优先匹配更具体的处理器**(参见 [01篇](exception-01.md) 的异常处理优先级),已有的层级处理器完全不受影响。 ```python # 新增:AuthenticationException 的专属处理器 @app.errorhandler(AuthenticationException) def handle_auth_error(e): """认证异常需要额外清除 Session""" session.clear() logger.warning(f"认证异常(已清除Session): {e}") return jsonify(e.to_dict()), e.http_status # 其他 ClientException 子类仍然走通用的 ClientException 处理器 ``` ## 模块导出 通过 `__init__.py` 统一导出,让外部使用者不需要关心内部文件结构: ```python # exception/__init__.py from .base import AppBaseException from .client import ClientException, ValidationException, AuthenticationException, AuthorizationException from .business import BizException, ResourceException, StateException from .server import ServerException, InfrastructureException, ExternalServiceException, UnknownException from .error_codes import ErrorCode from .handler import register_error_handlers __all__ = [ # 基类 'AppBaseException', # 客户端异常 'ClientException', 'ValidationException', 'AuthenticationException', 'AuthorizationException', # 业务异常 'BizException', 'ResourceException', 'StateException', # 服务端异常 'ServerException', 'InfrastructureException', 'ExternalServiceException', 'UnknownException', # 错误码 'ErrorCode', # 处理器注册 'register_error_handlers', ] ``` 外部使用效果: ```python # 简洁的导入方式 from exception import ValidationException, BizException, ErrorCode, register_error_handlers # 而非繁琐的 # from exception.client import ValidationException # from exception.business import BizException # from exception.error_codes import ErrorCode # from exception.handler import register_error_handlers ``` `__init__.py` 的价值在于**解耦内部结构与外部使用**。当项目从基础结构升级为 [06篇](exception-06.md) 中介绍的二级目录结构时,外部的 `from exception import XxxException` 完全不需要修改。 ## 项目集成 在 Flask 项目入口完成异常模块的集成,仅需一行: ```python # app.py from flask import Flask from exception import register_error_handlers app = Flask(__name__) # 注册全局异常处理器(一行集成) register_error_handlers(app) ``` 至此,整个 `exception/` 模块与项目的集成就完成了。此后在 Service 层和 Controller 层中,开发者只需要关注"什么时候抛出什么异常",而无需关心异常的捕获、格式化和响应——这些都由全局处理器统一收敛。 ### 集成后的开发体验 集成完成后,业务开发的编码模式变得非常简洁: ```python # Service 层:只负责抛出,完全不写 try-except def get_order(order_id): order = order_repository.find_by_id(order_id) if not order: raise ResourceException(f"订单 {order_id} 不存在") if order.status != "PENDING": raise StateException(f"订单状态为 {order.status},不支持当前操作") return order # Controller 层:透明传递,完全不写 try-except @app.route('/api/orders/') def handle_get_order(order_id): order = get_order(order_id) return jsonify(order.to_dict()) ``` 无论 Service 层抛出什么异常,全局处理器都会自动: 1. 匹配对应的处理器(按异常类型层级) 2. 记录适当级别的日志(`INFO` / `WARNING` / `ERROR` / `CRITICAL`) 3. 调用 `to_dict()` 序列化为统一的 JSON 格式 4. 返回正确的 HTTP 状态码 开发者只需专注于**业务正确路径**和**在合适的时机抛出合适的异常**。 ## 总结:异常编程实践全系列回顾 至此,异常编程实践三篇已全部完成。三篇文章解决了三个递进的问题: | 篇章 | 核心问题 | 核心产出 | | --- | --- | --- | | [06篇](exception-06.md) 异常文件结构 | 异常代码**往哪放** | `exception/` 模块目录结构 | | [07篇](exception-07.md) 异常类与错误码 | 异常代码**怎么写** | 异常类层次 + 错误码枚举 | | 本篇 全局处理器与集成 | 异常模块**怎么用** | 全局处理器 + 一行集成 | 完整的 `exception/` 模块文件清单: | 文件 | 职责 | 对应架构设计篇 | | --- | --- | --- | | `base.py` | 异常基类,封装通用属性和序列化方法 | [02-异常分类](exception-02.md) | | `client.py` | 客户端异常,参数校验/认证/授权 | [02-异常分类](exception-02.md) | | `business.py` | 业务异常,业务规则/资源/状态 | [02-异常分类](exception-02.md) | | `server.py` | 服务端异常,基础设施/外部服务/未知 | [02-异常分类](exception-02.md) | | `error_codes.py` | 错误码枚举,前后端统一标识 | [00-核心原则](exception-00.md)中的用户友好 | | `handler.py` | 全局异常处理器,统一注册和收敛 | [04-异常处理](exception-04.md) | | `__init__.py` | 模块统一导出 | — | 回顾整个异常系列(00-08),从架构设计到编程实践,形成了完整的闭环: ```text 架构设计(法) 编程实践(术) 00 核心原则 ──────────────────────────→ 贯穿全系列 01 异常基础 ──────────────────────────→ 07 基类设计(super().__init__、__str__) 02 异常分类 ──────────────────────────→ 06 文件结构 + 07 类层次设计 03 异常抛出 ──────────────────────────→ 07 使用示例(if-raise / try-except-raise) 04 异常处理 ──────────────────────────→ 08 全局处理器(声明式异常处理落地) 05 系统性总结 ────────────────────────→ 08 全系列回顾 ``` 好的异常处理,不是为了让程序"不报错",而是为了让错误发生得**清晰、体面且可控**。异常不应该是事后补救的补丁,而应该是项目伊始就被精心设计的基础设施。