error_handling.md 4.8 KB

1. 整体方案

后端服务(ai-server,Spring Boot 3)采用「自定义业务异常 + 全局异常处理器 + 统一响应体」三层结构;网关(ai-gateway,自研 Spring WebFlux/Filter 风格)则使用 JsonResponses 工具类直接返回 JSON。

  • 业务异常类型:com.zsjz.ai.common.exception.ServerException(继承 RuntimeException),承载 code、msg 两个字段,提供 spe(ErrorEnum) / spe(String) 静态工厂。
  • 错误码定义:com.zsjz.ai.common.base.ErrorEnum,集中声明 HTTP 级错误码(400/401/403/404/500)以及文件相关业务码(80001~80004)。
  • 全局拦截器:com.zsjz.ai.common.exception.GlobalExceptionHandler,标注 @RestControllerAdvice,将所有异常收敛为 Result<T> 响应。
  • 统一响应体:com.zsjz.ai.common.config.Result<T>,包含 code/message/data,提供 succeed() / failure(code, msg) 等工厂方法。

2. 关键文件

文件 作用
ai-server/src/main/java/com/zsjz/ai/common/exception/GlobalExceptionHandler.java 全局异常映射,将各层异常转为 Result
ai-server/src/main/java/com/zsjz/ai/common/exception/ServerException.java 业务异常基类
ai-server/src/main/java/com/zsjz/ai/common/base/ErrorEnum.java 错误码枚举
ai-server/src/main/java/com/zsjz/ai/common/config/Result.java 统一响应体
ai-gateway/src/main/java/com/zsjz/gateway/support/JsonResponses.java 网关侧统一 JSON 响应工具

3. 架构与约定

3.1 业务异常 → Result 的映射

GlobalExceptionHandler 中每个 @ExceptionHandler 方法都返回 ResponseEntity<Result<Void>>。核心映射规则:

  • ServerException:用其 code 作为业务码,通过 resolveHttpStatus(int) 映射到 HTTP 状态码后返回。
  • NotLoginException(Sa-Token):刻意返回 HTTP 200 + body.code=401,以便前端 defHttp.transformRequestHook 能识别并跳转登录页;注释明确说明了不直接返回 HTTP 401 的原因。
  • 参数校验异常(MethodArgumentNotValidException、BindException、ConstraintViolationException)→ HTTP 400 + Result.failure(400, message)。
  • 兜底 Exception:先尝试沿 cause 链 unwrapServerException 查找被 MyBatis/Spring 包装的 ServerException(注释解释原因:否则数据源层的业务提示会被吞掉),找不到则记录日志并以 HTTP 500 + "系统异常,请稍后重试" 返回。
  • 其他 Spring MVC 标准异常(NoHandlerFoundException、NoResourceFoundException、HttpRequestMethodNotSupportedException、HttpMediaTypeNotSupportedException、MaxUploadSizeExceededException、MissingServletRequestParameterException、MethodArgumentTypeMismatchException)均有独立 handler,返回语义化消息。

3.2 业务代码抛错的方式

模块代码通过两种方式抛出业务异常:

  1. throw new ServerException("用户可读消息") —— 使用默认 ErrorEnum.SERVER_ERROR 的 code(500)。
  2. throw ServerException.spe(ErrorEnum.XXX) —— 使用枚举中预定义的 code/message。
  3. throw new ServerException(code, msg) —— 在需要精确控制 HTTP 语义时使用(如 CaseAccessGuard 中对 caseId 为空、权限不足、案件不存在分别抛出 400/403/404)。WorkspacePathGuard 也遵循同样的模式。

3.3 网关侧的错误处理

ai-gateway 没有使用 Spring @ControllerAdvice,而是由各个 Filter/Service 自行捕获异常并通过 JsonResponses.fail(...) 或 JsonResponses.error(...) 返回 JSON。异常类型多为 IllegalArgumentException(配置校验失败)或运行时异常,没有被统一收敛到某个全局处理器。

4. 观察到的约定与约束

  • 所有后端接口响应体均为 Result<T>:由 GlobalExceptionHandler 保证,业务 Controller 不应直接返回裸字符串或 Map。
  • 业务异常必须走 ServerException:控制器和服务层中出现的业务分支失败均以 ServerException.spe(...) 或 new ServerException(...) 表达,避免使用普通 throw new RuntimeException。
  • HTTP 状态码与业务码解耦:resolveHttpStatus 负责把业务 code 映射到 HTTP 状态码,非法值回退到 400;NotLoginException 是特例,固定返回 HTTP 200 + body.code=401,该行为有显式注释说明。
  • MyBatis/System 包装异常会透传业务信息:handleException 中的 unwrapServerException 遍历 cause 链以恢复被框架包裹的 ServerException,防止上层只看到 "系统异常"。
  • 未登录访问不走 HTTP 401:这是有意为之的前端兼容性约定,见 handleNotLogin 的 Javadoc 注释。
  • 网关与后端错误模型不一致:网关侧不使用 Result/ServerException,而是 JsonResponses,两者没有统一的错误码枚举共享。