异常处理与错误码规范

共 19 题
📑 题目列表 19 题
#
★★★

1. 异常处理的分层原则中哪一层捕获、哪一层转译、哪一层兜底?"吞异常"和"异常当流程控制"两类反模式的危害?

异常处理的分层原则:哪一层捕获、哪一层转译、哪一层兜底?"吞异常"和"异常当流程控制"两类反模式的危害?

  • 异常处理的分层(捕获/转译/兜底)
  • "吞异常"反模式
  • "异常当流程控制"反模式

(1)分层原则:a) 业务层"捕获"——处理可恢复的业务异常,转成业务结果;b) 边界层"转译"——把底层异常(DB、IO)转成领域/API 异常,不泄露实现细节;c) 框架层"兜底"——全局异常处理(@ControllerAdvice)捕获未处理的异常,统一转错误响应与日志。核心是"面向异常类型的处理在各层各自责任"。 (2)"吞异常"危害:catch (Exception e) {} 静默吞掉异常,丢失错误信息,导致"看起来成功实则失败",难排查、难监控。应记录或传播,而非吞掉。 (3)"异常当流程控制"危害:用异常处理正常业务分支(如用 NumberFormatException 判断是否是数字),异常创建昂贵、语义混乱、控制流难读。应改用返回值/条件判断。 (4)分层落地:每层只捕获"该层能处理的"异常,其余上抛或转译;兜底层保证不泄漏;吞异常与异常当流程控制是反模式,分别用"记录/传播"与"条件/Result"替代。

异常分层是"责任边界":业务层捕获可恢复、边界层转译、兜底层兜底。反模式是吞异常(静默)与异常当流程控制(昂贵+语义混乱)。正确的分层让异常可处理、可转译、可兜底。

// 吞异常(反模式)
catch (Exception e) { /* 空 */ }
// 异常当流程控制(反模式)
try { Integer.parseInt(s); return true; } catch (NumberFormatException e) { return false; }
// 正确:记录/转译/兜底
catch (DataAccessException e) {
    log.error("db error", e);
    throw new BusinessException("DB_ERR", e);   // 转译
}
#
★★★

2. 统一错误码体系如何设计(业务码/系统码/HTTP 状态码的映射与分段规划),错误码字典如何集中定义、版本化与文档化?

统一错误码体系如何设计?业务码/系统码/HTTP 状态码如何映射与分段?错误码字典如何集中定义、版本化与文档化?

  • 错误码分段规划(业务码/系统码/HTTP 状态码)
  • 错误码字典集中定义
  • 版本化与文档化

(1)分段规划:错误码结构分"系统码"与"业务码"两大段,如 SYSTEM_TIMEOUTBIZ_ORDER_NOT_FOUND;错误码与 HTTP 状态码映射(4xx/5xx),但业务码保留更细粒度。 (2)映射:错误码(业务/系统)→ HTTP 状态码(4xx 客户端错误、5xx 服务端错误)→ 错误响应体 { code, message }。前端依据 code 而非 HTTP 状态码做业务分支。 (3)集中定义:错误码字典集中在一个文件/枚举(如 ErrorCode enum、errors.yaml),集中定义"code + message + httpStatus + 是否可重试",避免散落字符串。 (4)版本化与文档化:错误码字典纳入版本管理(随 API 版本演进);用 OpenAPI 的 responses 文档化错误码;生成错误码文档(集成到 API 文档),供前端与调用方查阅。 (5)实践:错误码结构稳定(如 {B}0001 业务码、{S}0001 系统码),新增时先查字典避免重复,删除时走 deprecation。

统一错误码体系 = 分段(业务/系统)+ 映射(HTTP 状态码)+ 集中定义(字典)+ 版本化文档化。前端用 code 分支,HTTP 状态码只做粗粒度分类;字典集中管理避免散落与冲突。

public enum ErrorCode {
    ORDER_NOT_FOUND("B1001", "订单不存在", HttpStatus.NOT_FOUND, false),
    DB_TIMEOUT("S0001", "数据库超时", HttpStatus.INTERNAL_SERVER_ERROR, true);
    private final String code; private final String message;
    private final HttpStatus status; private final boolean retryable;
    // ...
}
#
★★★

3. "异常即流程控制"的反模式如何识别,如何用 Result/Option 类型重构?

"异常即流程控制"的反模式如何识别?如何用 Result/Option 类型重构?

  • 异常即流程控制的识别特征
  • Result/Option 类型
  • 重构策略

(1)识别特征:a) 用异常分支处理"正常业务状态"(如用异常判断是否找到、是否合法);b) 异常在正常流程中频繁抛出(异常创建昂贵);c) 控制流依赖异常的 catch 分支而非正常条件。 (2)Result/Option 类型:Result<T, E>(成功/失败)或 Option<T>(有值/无值)显式表达"可能失败/可能无值",调用方用返回值匹配而非 try/catch。 (3)重构:a) "可能无值"用 Optional/Option(如 findById 返回 Optional);b) "可能失败且有错误信息"用 Result(如 Result<User, Error>);c) 调用方用 match/if 处理,而非 try/catch。 (4)适用边界:Result/Option 适合"可预期、调用方需处理"的错误;异常仍适合"不可预期、类型化、跨层传播"的错误。二者结合而非全盘替代。

"异常即流程控制"的特征是"用异常处理正常状态、频繁抛异常"。重构用 Result/Option 显式表达可预期失败,让调用方用返回值分支。但异常仍保留给不可预期/类型化错误,二者结合。

// 反例:异常即流程控制
try { return findUser(id); } catch (NotFound e) { return null; }

// 正例:用 Result/Option
sealed interface Result {
    record Ok(User u) implements Result {}
    record Err(String msg) implements Result {}
}
Result r = findUser(id);
if (r instanceof Result.Ok ok) { return ok.u(); }
#
★★★

4. 异常吞噬(Swallowed Exception)与"记录后继续"的边界在哪里,如何设计合理的降级?

异常吞噬(Swallowed Exception)与"记录后继续"的边界在哪里?如何设计合理的降级?

  • 异常吞噬 vs 记录后继续
  • 边界判断
  • 合理降级设计

(1)异常吞噬:catch (Exception e) {} 静默吞掉,不记录、不传播、不处理,错误信息丢失。永远不该。 (2)记录后继续:catch (Exception e) { log.error(...); } 记录后继续执行,用于"非关键路径失败不影响主流程"的场景。这是有意的降级,但必须记录。 (3)边界判断:a) 若失败不影响主流程结果(如辅助埋点、缓存更新失败)→ 记录后继续(降级);b) 若失败导致结果错误或状态不一致 → 必须传播/重试/回滚;c) 无论哪种都要记录,不静默。 (4)合理降级设计:a) 明确"哪些失败可降级、降到什么"(如缓存失败回源 DB、推荐失败返回空);b) 降级点必须记录(日志 + 监控指标);c) 降级策略可配置(如 feature flag、熔断阈值);d) 降级与主流程隔离,避免降级失败影响主流程。

边界是"失败是否影响主流程结果"。吞异常(不记录不处理)与降级(记录后继续)的本质区别是"是否有意且记录"。合理降级要明确降级策略、记录日志、可配置、隔离隔离。关键是不静默。

// 吞异常(反模式)
catch (Exception e) { }
// 记录后继续(合理降级)
catch (Exception e) {
    log.warn("cache update failed, degrade to DB", e);
    metrics.increment("cache.fail");
    repo.save(order);   // 降级路径
}
#
★★★

5. 异步代码中的异常传播中 Future/Promise、回调与事件循环下异常如何捕获与传递,未处理拒绝如何治理?

异步代码中的异常传播:Future/Promise、回调与事件循环下异常如何捕获与传递?未处理拒绝如何治理?

  • 异步异常传播(Future/Promise、回调、事件循环)
  • 未处理拒绝(unhandled rejection)
  • 治理手段

(1)异步异常特点:异步代码的异常不会同步到达调用方,需通过 Future/Promise 的失败通道、回调的 error 参数、事件循环的 unhandled 机制传播。 (2)Future/Promise:future.get() 抛 ExecutionException、Promise 的 .catch() 处理;异常通过链式传递(.then().catch()),不处理则沉淀为 unhandled rejection。 (3)回调与事件循环:回调风格用 (err, result) 约定;Node 事件循环中未处理的 promise reject 触发 unhandledRejection 事件,可能崩溃进程(新版 Node 默认崩溃)。 (4)治理:a) 所有异步链必须有错误处理(.catch/error 回调);b) 全局监听 unhandledRejection/uncaughtException 并记录/上报;c) 用 async/await + try/catch 统一;d) 排查未处理拒绝,避免"静默失败"。 (5)工程:异步错误纳入监控,未处理拒绝视为 bug 治理;日志记录 trace/上下文。

异步异常不会自动传播,需通过 Future/Promise 的失败通道、回调 error 参数、事件循环 unhandled 事件传递。治理核心是"每个异步链都有错误处理 + 全局监听 unhandled/上报 + async/await 统一",避免静默失败。

// async/await + try/catch
async function run() {
  try {
    const user = await fetchUser(id);
    return user;
  } catch (e) {
    log.error("fetch user failed", e);
    throw e;
  }
}
// 全局监听未处理拒绝
process.on('unhandledRejection', (reason) => {
  log.error("unhandled rejection", reason);
  process.exit(1);
});
#
★★

6. 全局错误处理在框架层(Spring @ControllerAdvice、React ErrorBoundary)的统一契约?

全局错误处理在框架层(Spring @ControllerAdvice、React ErrorBoundary)如何建立统一契约?

  • @ControllerAdvice 统一后端正处理
  • React ErrorBoundary 统一前端错误边界
  • 统一错误契约

(1)Spring @ControllerAdvice:全局异常处理器,把异常转成统一错误响应(HTTP 状态码 + 错误体 { code, message }),替代每个 Controller 各自 try/catch。它提供"边界层兜底"。 (2)统一契约:@ControllerAdvice 按异常类型映射到状态码与错误码,日志记录,返回统一结构的 ErrorBody。前端拿到一致错误格式。 (3)React ErrorBoundary:类组件捕获子树渲染/生命周期错误,渲染友好错误 UI,避免白屏,并上报错误。它是前端"错误边界"。 (4)统一契约价值:后端统一错误响应格式,前端统一错误展示与上报,前后端有稳定错误契约,减少各业务各自处理。 (5)落地:后端定义 ErrorBody 结构 + 全局异常映射;前端 ErrorBoundary 包裹页面 + 统一错误展示组件 + 上报。

全局错误处理是"框架层统一兜底"。Spring @ControllerAdvice 把后端异常统一转成错误响应,React ErrorBoundary 捕获前端渲染错误并统一展示,二者形成前后端一致的错误契约。

// 后端统一错误体
public record ErrorBody(String code, String message, String path) {}
@RestControllerAdvice
public class GHandler {
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorBody> handle(BusinessException e) { ... }
}
// React ErrorBoundary
class ErrorBoundary extends React.Component {
  componentDidCatch(err, info) { logError(err, info); }
  render() { return this.state.error ? <Fallback/> : this.props.children; }
}
#
★★

7. 业务错误码与 HTTP 状态码的边界(4xx/5xx 何时用业务错误码)?

业务错误码与 HTTP 状态码的边界:4xx/5xx 何时用业务错误码?

  • HTTP 状态码的粗粒度语义(4xx/5xx)
  • 业务错误码的细粒度
  • 边界与分工

(1)HTTP 状态码:粗粒度分类——4xx 客户端错误(400 参数、401 未认证、403 无权限、404 不存在、409 冲突)、5xx 服务端错误(500、503)。它表达"传输层语义"。 (2)业务错误码:细粒度业务语义(如"库存不足"、"余额不足"),表达"业务层具体失败原因"。 (3)边界:HTTP 状态码用于"粗分类"(客户端/服务端、资源类),业务错误码用于"细粒度业务原因"。前端用业务错误码分支,HTTP 状态码只做粗分类与缓存/重试依据。 (4)分工:a) 4xx → 客户端错误,业务码说明具体原因;b) 5xx → 服务端错误,业务码可能不返回敏感细节;c) 业务码与 HTTP 状态码映射(一次 HTTP 状态码可对应多个业务码)。 (5)原则:HTTP 状态码表达"请求亲和/资源",业务错误码表达"业务失败原因",二者互补,前端主要依赖业务码。

边界是"传输语义 vs 业务语义"。HTTP 状态码(4xx/5xx)做粗分类(资源、权限、服务端),业务错误码做细粒度业务原因。前端用业务码分支,HTTP 状态码做粗分类与缓存/重试。

// HTTP 400 + 业务码细粒度
HTTP/1.1 400 Bad Request
{ "code": "B1002", "message": "库存不足", "retryable": false }
#
★★

8. 受检异常与非受检异常的取舍争论中现代工程(Java/Kotlin/Go error)实践如何收敛?

受检异常 vs 非受检异常的取舍争论:现代工程(Java/Kotlin/Go error)实践如何收敛?

  • 受检异常(checked)与非受检(unchecked)的争论
  • Java/Kotlin/Go 的实践
  • 收敛趋势

(1)受检异常(checked):强迫调用方处理(throws/编译期检查),Java 特有;优点是"显式处理契约",缺点是"容易导致吞异常或到处什么都不 catch"。 (2)非受检异常(unchecked):不强制处理,可传播到兜底层;优点是灵活,缺点是"契约不显式"。 (3)现代实践:Kotlin 取消受检异常(只有 unchecked),用 Result/runCatching;Go 用 error 返回值显式处理(不抛异常);Java 社区趋势是"减少受检异常,用 Result/Optional + 非受检异常"。 (4)收敛趋势:a) 从"受检异常强制"转向"显式错误值(Result/error)+ 非受检异常兜底";b) 可预期、可恢复的错误用返回值/Result,不可预期、类型化错误用异常;c) 避免受检异常导致的"吞异常"。 (5)实践建议:用错误值(Result/error)表达可预期错误,用非受检异常表达编程错误与不可预期错误,辅以全局兜底。

受检异常争论的收敛是"错误值(Result/error)+ 非受检异常"。Kotlin 取消受检异常,Go 用 error 值,Java 趋势用 Result/Optional。可预期错误用返回值,不可预期用异常 + 全局兜底。

// Go:error 值显式处理
func FindUser(id int) (*User, error) {
    if id < 0 { return nil, errors.New("invalid id") }
    return &User{ID: id}, nil
}
u, err := FindUser(1)
if err != nil { return err }
#
★★

9. 防御式编程与契约式设计(前置/后置条件、不变式)的边界中过度防御的代码坏味道?

防御式编程与契约式设计(前置/后置条件、不变式)的边界?过度防御的代码坏味道?

  • 防御式编程 vs 契约式设计
  • 前置/后置条件、不变式
  • 过度防御的坏味道

(1)防御式编程:对"外部输入/调用方错误"做防御(参数校验、空值检查),避免崩溃。合理防御在"边界"(API 入口、外部输入)。 (2)契约式设计(DbC):用前置条件(调用方必须满足)、后置条件(被调方保证)、不变式(类内恒成立)定义契约,违反契约表示"调用方或实现错误"。 (3)边界:防御式编程用于"可预期的外部输入异常"(防御边界);契约式设计用于"内部可信方之间的契约"(违反即 bug,用 assert/throw)。二者不是"处处防御"。 (4)过度防御坏味道:a) 内部方法过度校验(重复校验、防御不可能发生的输入);b) 防御性分支掩盖真实 bug;c) 代码大量 if (x == null) return 掩盖设计缺陷。过度防御让代码臃肿、掩盖错误。 (5)原则:边界防御(外部输入),内部契约(DbC),降低重复防御。用 null-safe 类型与前置校验替代过度防御。

防御式编程面向"外部输入边界",契约式设计面向"内部可信契约"。过度防御(内部重复校验、防御不可能输入)是坏味道,掩盖 bug、臃肿代码。边界防御 + 内部契约 + 降重复。

// 合理:边界防御(API 入口)
public void createOrder(@NotNull Order o) {
    Objects.requireNonNull(o, "order must not be null");
    ...
}
// 过度防御(内部方法重复校验)
public Money compute(Money a, Money b) {
    if (a == null) return null;   // 内部不可信,过度防御
    ...
}
#
★★

10. 错误码与异常体系如何共存,对外 API 返回码、对内抛异常的分层约定,跨层传播时何时转换、何时透传?

错误码与异常体系如何共存?对外 API 返回码、对内抛异常的分层约定,跨层传播时何时转换、何时透传?

  • 对外返回码 vs 对内异常的分层
  • 跨层传播的转换 vs 透传
  • 边界约定

(1)分层约定:对外 API 用"返回码"(错误码响应),对内服务层用"异常"。外层(Controller/API 边界)把内部异常转成对外错误码,内层(业务/服务)用异常表达错误。 (2)转换 vs 透传:a) 转换——当错误语义需要"对外呈现"或"跨语言/跨服务"时,把内部异常转成错误码/可传输的错误;b) 透传——内部同级异常直接传播,不转换(保持类型)。 (3)边界:a) 业务异常(BusinessException)在业务层内部传播,在边界层转成错误码;b) 系统异常(InfrastructureException)在边界层转成 5xx 错误码;c) 编程错误(NPE、IllegalState)透传,由兜底层处理。 (4)规则:跨层传播时,若错误"需要被调用方理解/处理"则转换(成错误码),若"原样让上层兜底"则透传。避免在每一层都转换(层层包装)。

错误码与异常共存是"对外返回码、对内异常"的分层。业务异常在边界转成错误码,系统异常转 5xx,编程错误透传给兜底层。转换时机是"需要对外呈现或跨边界",透传是"内部保持类型"。

// 业务层:抛业务异常
throw new BusinessException(ErrorCode.ORDER_NOT_FOUND);
// 边界层:转成对外错误码
@RestControllerAdvice
public ResponseEntity<ErrorBody> handle(BusinessException e) {
    return new ResponseEntity<>(new ErrorBody(e.getCode(), e.getMessage()), e.getStatus());
}
#
★★

11. 跨服务调用时如何传递错误上下文(错误码、TraceID、业务单据号)?

跨服务调用时如何传递错误上下文(错误码、TraceID、业务单据号)?

  • 错误上下文传递(错误码、TraceID、业务单据号)
  • 分布式链路追踪
  • 错误传播与关联

(1)错误上下文三要素:a) 错误码(业务错误细分);b) TraceID(一次请求链路唯一标识,贯穿所有服务);c) 业务单据号(业务对象标识,如 orderId)。 (2)传递方式:错误码在响应体中传递;TraceID 通过 HTTP 头(如 X-Trace-Id)在调用链中传递,由网关生成、各服务透传;业务单据号在请求/响应体中传递。 (3)链路追踪:用 OpenTelemetry/zipkin 生成并传播 TraceID/SpanID,把各服务的错误日志关联到同一 trace,便于排查。 (4)错误传播:下游服务返回错误时,上游应保留错误码与 TraceID,包装上游错误码并透传链路 ID,形成"错误链"。 (5)价值:错误上下文让"一个跨服务错误"可被定位到"哪个服务、哪个 trace、哪个单据",快速排查。

跨服务错误上下文 = 错误码(业务语义)+ TraceID(链路标识)+ 业务单据号(业务对象)。TraceID 用 HTTP 头传播,链路追踪关联各服务日志,错误包装保留链路 ID,形成可追溯的错误链。

// 网关生成 TraceID 并透传
String traceId = UUID.randomUUID().toString();
httpHeaders.set("X-Trace-Id", traceId);
// 错误响应携带 traceId + 业务单号
{ "code": "B1001", "message": "订单不存在", "traceId": "abc123", "orderId": "8888" }
#
★★

12. 如何设计"可恢复错误"与"不可恢复错误"的分类,并据此决定降级、熔断重试与失败传播的边界?

如何设计"可恢复错误"与"不可恢复错误"的分类?据此决定降级、熔断重试与失败传播的边界?

  • 可恢复 vs 不可恢复错误分类
  • 降级、熔断重试、失败传播的边界
  • 分类依据

(1)分类依据:a) 可恢复错误——重试/降级可能成功(网络超时、瞬时故障、依赖暂不可用);b) 不可恢复错误——重试无意义(参数错误、业务规则冲突、数据不一致)。 (2)决定策略:a) 可恢复错误 → 重试(指数退避)、熔断(对连续失败快速失败)、降级(返回缓存/默认值);b) 不可恢复错误 → 失败传播(向上抛错,不重试),或返回明确错误。 (3)边界:a) 可恢复错误才重试/熔断,不可恢复错误直接失败;b) 熔断用于"保护下游",避免打爆失败的服务;c) 降级用于"非关键路径",关键路径失败要传播。 (4)落地:错误码标记 retryable;重试策略(次数、退避、幂等)配套;熔断阈值可配置;区分"可重试/不可重试"避免无限重试。

可恢复错误(瞬时/超时)→ 重试 + 熔断 + 降级;不可恢复错误(参数/规则)→ 失败传播。分类依据是"重试/降级是否可能成功"。错误码标 retryable,重试要幂等,熔断保护下游。

// 错误码标记可重试
enum ErrorCode {
    NETWORK_TIMEOUT("S0002", "网络超时", 503, true),   // retryable
    INVALID_PARAM("B2001", "参数错误", 400, false);    // 不可重试
}
// 重试策略:仅可重试错误
if (error.retryable() && attempts < MAX) { retryWithBackoff(); }
#
★★

13. 空值语义的编码规范中 null、空集合、空字符串的语义区分,以及空指针安全(Optional、空对象模式)如何作为错误处理规范的一部分?

空值语义的编码规范:null、空集合、空字符串的语义区分,以及空指针安全(Optional、空对象模式)如何纳入错误处理规范?

  • null/空集合/空字符串的语义区分
  • 空指针安全(Optional、空对象模式)
  • 作为错误处理规范

(1)语义区分:a) null 表示"无值/未设置";b) 空集合 [] 表示"查询结果为空";c) 空字符串 "" 表示"值存在但为空"。三者语义不同,混用会误导(如把空集合当 null 表示无数据)。 (2)规范约定:a) 集合若无数据返回空集合而非 null;b) 字符串"未设置"用 null、"空值"用 ""(或统一 Optional);c) 明确"无值"语义,避免 null 与空集合歧义。 (3)空指针安全:a) 用 Optional 表达"可能无值";b) 用空对象模式(Null Object)返回无害空对象而非 null;c) 用 @Nullable/@NonNull 注解 + 静态检查。 (4)作为错误处理规范:空值语义明确后,避免 NPE(空指针错误)成为隐性错误;用 Optional/空对象/注解把"可能无值"显式化,纳入错误处理与契约。

空值编码规范要区分 null(无值)、空集合(无结果)、空字符串(有空值)。用 Optional 表达可能无值、空对象模式替代 null、@Nullable 注解,把空指针风险显式化,纳入错误处理契约。

// 区分语义
List<User> users = repo.findByStatus(active); // 空集合表示无结果
String note = user.getNote(); // null 表示未设置,"" 表示空值
// 空指针安全
Optional<User> opt = repo.findByEmail(email); // 可能无值
User user = opt.orElseGet(User::empty); // 空对象模式
#
★★

14. 资源清理阶段的异常安全中 finally/RAII 中清理操作抛异常时如何不掩盖原始异常,如何记录与上报?

资源清理阶段的异常安全:finally/RAII 中清理操作抛异常时如何不掩盖原始异常,如何记录与上报?

  • finally/RAII 清理异常
  • 不掩盖原始异常(suppressed exception)
  • 记录与上报

(1)问题:try { ... } finally { close(); } 中若 close() 抛异常,会覆盖 try 块抛出的原始异常(Java 的 suppressed exception 机制;C++ RAII 析构抛异常导致 terminate)。 (2)避免掩盖:a) Java 用 try-with-resources(自动 suppressed 合并,getSuppressed() 可取);b) finally 中清理抛异常时,若已有原始异常,把它作为 suppressed 附加并保留原始异常;c) 记录清理异常但不覆盖原始异常。 (3)记录与上报:a) 清理异常必须记录(日志),因为清理失败可能泄漏资源;b) 通过 getSuppressed() 或日志上下文把原始 + 清理异常都上报;c) 不让清理异常"吞掉"原始异常。 (4)原则:清理异常不能掩盖主异常;两者都要记录/上报;优先用 try-with-resources/RAII 自动管理资源。

资源清理异常安全的核心是"清理异常不掩盖主异常"。Java 用 try-with-resources(suppressed 合并)、C++ 析构不抛异常,清理失败记录日志。关键:主异常保留、清理异常记录、两者都上报。

// try-with-resources:自动 suppressed
try (Connection c = open(); ResultSet rs = c.query(sql)) {
    return process(rs);
} // 若 process 抛异常且 close 也抛,close 异常成为 suppressed
// 手动 finally 中清理
try {
    ...
} finally {
    try { resource.close(); }
    catch (Exception e) { log.error("close failed", e); } // 记录,不掩盖
}
#
★★

15. 异常消息的上下文规范中 message 应包含哪些操作与实体信息,如何与错误码和参数化模板配合,避免敏感信息?

异常消息的上下文规范:message 应包含哪些操作与实体信息?如何与错误码和参数化模板配合,避免敏感信息?

  • 异常消息的上下文(操作、实体、参数)
  • 与错误码、参数化模板配合
  • 避免敏感信息

(1)异常消息应包含:a) 操作(什么操作失败);b) 实体(哪个实体/资源);c) 关键参数(ID、业务单号);d) 失败原因。如 "订单 8888 支付失败:余额不足"。 (2)配合错误码与模板:a) 错误码提供结构化分类;b) message 用参数化模板("订单 {orderId} 支付失败");c) 占位符传入上下文,避免字符串拼接。 (3)避免敏感信息:a) 不包含密钥、密码、token、完整 PII;b) 不暴露内部 SQL 与堆栈细节(对外);c) 日志可与对外 message 分离(日志含更多上下文,对外 message 精简)。 (4)规范:message 用模板 + 上下文参数,分"对外 message"(精简无敏感)与"内部日志"(含细节),错误码统一分类。

异常消息要"有上下文(操作/实体/参数)+ 参数化模板 + 错误码分类",同时避免敏感信息。对外 message 精简无敏感,内部日志含细节,模板化避免拼接与泄露。

// 参数化模板 + 错误码
throw new BusinessException(
    ErrorCode.PAYMENT_FAILED,
    "订单 {orderId} 支付失败:{reason}"   // 模板
        .replace("{orderId}", orderId)
        .replace("{reason}", sanitize(reason))  // 脱敏
);
#

16. 错误信息本地化与前端可读性的统一规范

错误信息本地化与前端可读性的统一规范如何设计?

  • 错误信息本地化(i18n)
  • 前端可读性
  • 对外 message 与本地化 key

(1)错误信息本地化:错误 message 应本地化(多语言),前端展示用户可读文本。但 message 通常由后端返回,需考虑"后端传 key、前端翻译"。 (2)规范:a) 后端返回的 message 用错误码 + 参数化结构,前端按错误码查找本地化文案;b) 或后端返回多语言 message(根据 Accept-Language);c) 前端展示用户可读文案,开发可读细节放日志。 (3)前端可读性:a) 面向用户的错误文案简洁、友好(不暴露技术细节);b) 可重试/可操作(如"请重试"、"请检查输入");c) 与业务错误码联动,前端能区分"可重试/不可重试"。 (4)统一:错误码是"稳定的本地化 key",message 是"具体文案",前端用错误码 + 参数渲染本地化文案,避免后端硬编码语言。

错误信息本地化与前端可读性 = 错误码作为稳定 key,前端按语言渲染本地化文案;后端不硬编码语言,返回错误码 + 参数。面向用户文案简洁友好、可操作,技术细节进日志。

// 前端本地化文案
{ "B1001": { "zh": "订单不存在", "en": "Order not found" } }
// 后端返回结构化错误(不硬编码语言)
{ "code": "B1001", "params": { "orderId": "8888" } }
#

17. 异常信息中敏感数据(堆栈、SQL、PII)泄漏的规范约束?

异常信息中敏感数据(堆栈、SQL、PII)泄漏的规范约束如何设计?

  • 敏感数据泄漏(堆栈、SQL、PII)
  • 日志脱敏
  • 对外与内部隔离

(1)敏感数据:堆栈(内部调用路径)、SQL(数据库结构、表名)、PII(手机号、邮箱、身份证)不应泄露到对外响应或日志。 (2)规范约束:a) 对外 HTTP 响应不返回堆栈与 SQL,只返回错误码 + 精简 message;b) 日志中 PII 脱敏(手机号打码 138****1234);c) 堆栈只进内部日志/监控,不进对外响应。 (3)落地:a) 全局异常处理器不把异常堆栈写入响应体;b) 日志框架配置脱敏(掩码 key、字典);c) 用 @Sensitive 字段注解 / 日志脱敏工具;d) 错误码与 message 分离,message 不含敏感细节。 (4)价值:防止通过错误响应与日志泄露内部结构、SQL、PII,降低安全风险。

敏感数据防护 = 对外响应不含堆栈/SQL/PII,只返回错误码+精简 message;日志 PII 脱敏;堆栈进内部监控。用全局异常处理 + 日志脱敏 + 字段注解落地。

// 对外响应不返回堆栈
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorBody> handle(Exception e) {
    log.error("uncaught", e);   // 堆栈进日志
    return new ResponseEntity<>(new ErrorBody("S0000", "系统繁忙"), 500); // 对外无细节
}
// 日志脱敏
log.info("user {} paid", maskPhone(phone)); // 138****1234
#

18. 异常的类型设计中自定义异常与继承体系?

异常的类型设计:自定义异常与继承体系如何设计?

  • 自定义异常的必要性
  • 继承体系设计
  • 异常分类与粒度

(1)自定义异常:当标准异常无法表达"业务错误"时,自定义 BusinessExceptionSystemException 等,携带错误码与上下文。 (2)继承体系:设计一个基类异常(如 AppException),派生出 BusinessException(业务错误)、SystemException(系统错误)、InfrastructureException(基础设施错误)。基类承载错误码、HTTP 状态、上下文。 (3)粒度:a) 异常类型按"错误类别"而非"每个业务场景"(避免异常爆炸);b) 具体场景用错误码区分,而非每个场景一个异常子类;c) 继承体系分层(基类 → 类别 → 可选场景)。 (4)规范:a) 自定义异常含错误码、message、上下文;b) 继承体系稳定,避免频繁改继承;c) 用错误码区分细粒度,异常类型区分粗粒度。

自定义异常继承体系 = 基类(AppException,含错误码/状态/上下文)+ 类别(Business/System/Infrastructure)。异常类型按"错误类别"分层,细粒度用错误码区分,避免异常爆炸。

public class AppException extends RuntimeException {
    private final ErrorCode code;
    private final HttpStatus status;
    public AppException(ErrorCode c, String msg) { super(msg); this.code=c; this.status=c.getStatus(); }
}
public class BusinessException extends AppException { ... }
public class SystemException extends AppException { ... }
#

19. 异常的性能成本中热路径中构造异常的代价与替代方案(Result/返回码),何时值得优化?

异常的性能成本:热路径中构造异常的代价与替代方案(Result/返回码),何时值得优化?

  • 构造异常的代价(堆栈填充)
  • 热路径中的异常
  • Result/返回码替代与优化时机

(1)构造异常代价:异常创建时填充堆栈(StackWalk)较昂贵,尤其在热路径、频繁抛异常时开销显著。 (2)热路径问题:若在热路径(高频循环、请求处理)用异常表达正常控制流,会频繁构造异常,性能下降。 (3)替代方案:a) 用 Result/返回码表达可预期失败(避免构造异常);b) 用 Optional 表达"可能无值";c) 预分配/复用异常(不推荐,丢堆栈)。 (4)优化时机:a) 已用 profiler 确认异常是热路径瓶颈;b) 可预期、频繁发生的错误用 Result/返回码替代异常;c) 不可预期、低频错误保留异常。避免过早优化(先用测量)。 (5)原则:冷路径用异常(可读),热路径的可预期错误用 Result/返回码(性能),以测量为准。

异常构造有堆栈填充成本,热路径频繁抛异常性能差。可预期、频繁的错误用 Result/返回码替代,不可预期、低频的保留异常。优化时机以 profiler 测量为准,避免过早优化。

// 热路径:Result 替代异常
public Result<Integer> parse(String s) {
    if (!isNum(s)) return Result.err("invalid");  // 无异常构造
    return Result.ok(parseInt(s));
}
// 冷路径:异常可接受
public void requireNonEmpty(String s) {
    if (s == null || s.isEmpty()) throw new IllegalArgumentException(s);
}