3. 错误处理框架现状分析:传统 try-catch 模式在 SaaS 场景下的局限性

在单体应用时代,try-catch 是错误处理的标准范式。然而,当应用架构演进到多租户 SaaS 模式后,这种传统的错误处理方式在鉴权拦截链路中暴露出严重的结构性缺陷。本章将从多租户隔离、性能、可观测性三个维度,系统分析其局限性。

3.1 传统 try-catch 的典型实现

在传统的鉴权拦截器中,开发者通常使用类似以下的模式来处理异常:

@Component
public class LegacyAuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) {
        try {
            // 1. 提取 Token
            String token = extractToken(request);
            // 2. 解析租户上下文
            TenantContext tenant = parseTenant(token);
            // 3. 鉴权校验
            if (!authService.verify(tenant, request.getRequestURI())) {
                throw new AuthException("无权限访问");
            }
            // 4. 设置上下文
            TenantContextHolder.set(tenant);
            return true;
        } catch (TokenExpiredException e) {
            // 处理 Token 过期
            response.setStatus(401);
            writeError(response, "Token 已过期");
            return false;
        } catch (AuthException e) {
            // 处理权限不足
            response.setStatus(403);
            writeError(response, e.getMessage());
            return false;
        } catch (Exception e) {
            // 兜底处理
            response.setStatus(500);
            writeError(response, "系统内部错误");
            return false;
        }
    }
}

这段代码在单租户或低并发场景下看似合理,但在多租户 SaaS 环境中,它隐藏着多个致命问题。

3.2 四大核心局限性分析

局限性维度 具体表现 对 SaaS 的影响
租户隔离失效 catch 块中未清理 ThreadLocal,导致租户上下文泄漏 租户 A 的请求可能错误地使用租户 B 的上下文,造成数据越权
熔断降级缺失 鉴权服务不可用时,try-catch 只能返回 500,无法快速失败 鉴权服务雪崩,拖垮整个网关或微服务实例
可观测性不足 异常信息散落在 catch 块中,缺乏结构化日志和指标 运维人员无法快速定位是哪个租户、哪个接口触发了异常
代码腐化加速 每个拦截器重复编写 try-catch,业务逻辑与错误处理耦合 新增租户策略或鉴权规则时,容易遗漏异常处理分支

3.3 局限性一:租户上下文泄漏与隔离失效

在多租户架构中,TenantContextHolder 通常使用 ThreadLocal 实现。传统 try-catch 模式在异常发生时,往往忘记在 finally 块中清理上下文。更严重的是,当 catch 块中直接返回 false 时,ThreadLocal 中残留的租户信息会被线程池中的下一个请求复用。

典型事故场景: 租户 A 的请求因 Token 过期进入 catch 块,返回 401。但 ThreadLocal 中仍持有租户 A 的上下文。线程池复用该线程处理租户 B 的请求时,鉴权拦截器误认为租户 B 是租户 A,导致租户 B 看到租户 A 的数据。

下图展示了传统 try-catch 模式下租户上下文泄漏的完整链路:

传统 try-catch 模式:租户上下文泄漏链路 请求进入 try 块执行 异常抛出 catch 处理 ThreadLocal 未清理 (租户上下文残留) 线程池复用线程 处理其他租户请求 数据越权访问 租户隔离被破坏 核心问题:catch 块未执行 finally 清理 → ThreadLocal 残留 → 线程复用 → 租户数据越权

3.4 局限性二:缺乏熔断降级机制

传统 try-catch 模式在鉴权服务出现故障时,只能被动地捕获异常并返回错误响应。它无法识别故障的严重程度,也无法主动切断对下游依赖的调用。在 SaaS 场景下,一个租户的恶意请求或突发流量可能导致鉴权服务响应变慢,try-catch 模式会使所有租户的请求都陷入等待或频繁重试,最终引发级联故障。

关键指标对比:
  • 传统 try-catch: 鉴权服务超时 → 每个请求等待 30s → 线程池耗尽 → 服务不可用
  • 熔断降级: 鉴权服务超时 → 快速失败(100ms) → 返回降级响应 → 保护系统稳定

3.5 局限性三:可观测性缺失

在 try-catch 块中,错误处理通常是“捕获-返回”的简单逻辑,缺乏结构化的观测数据。运维人员无法回答以下关键问题:

  • 哪个租户的鉴权失败率最高?
  • 哪个 API 接口触发的鉴权异常最多?
  • 鉴权服务的平均响应时间是否在恶化?
  • 异常是集中在某个时间段还是持续发生?

下表对比了传统模式与可观测模式在错误处理上的差异:

观测维度 传统 try-catch 可观测模式(指标+日志+链路)
错误计数 无,或依赖日志文件 grep 按租户、接口、错误码聚合的 Prometheus 指标
错误详情 散落在 catch 块的日志字符串中 结构化 JSON 日志,包含 tenantId、userId、traceId
调用链路 无法关联上下游调用 通过 TraceId 串联网关→鉴权→业务服务的完整链路
告警能力 依赖人工巡检日志 错误率超过阈值自动告警,并通知对应租户的运维群

3.6 局限性四:业务逻辑与错误处理耦合

传统 try-catch 模式将正常的鉴权流程与异常处理流程交织在一起。随着 SaaS 业务的发展,鉴权规则会不断增加(如 IP 白名单、设备指纹、行为校验等),每个新规则都可能引入新的异常类型。开发人员需要在同一个拦截器中不断添加新的 catch 块,导致代码快速腐化。

代码坏味道信号:
  • 一个拦截器中有超过 5 个 catch 块
  • catch 块中包含 if-else 分支判断不同的错误码
  • catch 块中调用了外部服务(如发送告警邮件)
  • finally 块中同时处理资源清理和上下文清理

3.7 传统模式 vs 升级框架:全景对比

下图从架构层面直观对比传统 try-catch 模式与升级后的错误处理框架:

传统模式 vs 升级框架:错误处理架构对比 传统 try-catch 模式 请求进入拦截器 try { 鉴权逻辑 } catch (Exception e) { 处理 } 返回 401/403/500 ThreadLocal 残留风险 升级错误处理框架 请求进入鉴权过滤器链 熔断器状态检查 统一异常处理 + 降级策略 结构化响应 + 指标上报 finally 保证上下文清理 升级

3.8 小结:为什么必须升级?