CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
API 设计实战入门/课时/错误处理
RawBlame
课时目录12 节

API 设计实战入门

  1. 01API 基础
  2. 02HTTP 协议层
  3. 03REST 资源设计
  4. 04错误处理
  5. 05认证
  6. 06授权模型
  7. 07API 安全
  8. 08API 风格
  9. 09架构与集成
  10. 10测试
  11. 11可观测与合规
  12. 12文档与工具

错误处理

RFC 7807 Problem Details。

30 分钟

错误处理

这一课你将能做到

  1. 设计统一的 API 错误响应格式,不再让调用方面对千奇百怪的错误体
  2. 实现 RFC 7807 Problem Details 标准,用 application/problem+json 返回结构化错误
  3. 在 Spring Boot 中搭建全局异常处理器,把异常翻译成正确的 HTTP 状态码和错误响应

为什么要学

错误处理是 API 的「售后服务」。调用方拿到 500 和一堆堆栈信息,跟拿到「课程不存在」的明确提示,体验天差地别。好的错误设计让前端能自动处理异常(比如 401 跳登录页),差的错误设计让联调变成猜谜。

先建立图像

错误响应演进:

Level 0:裸异常
HTTP/1.1 500 Internal Server Error
{"timestamp":"...","status":500,"error":"Internal Server Error","trace":"java.lang.NullPointerException..."}

Level 1:统一包装
HTTP/1.1 404 Not Found
{"code":404,"message":"课程不存在","data":null}

Level 2:Problem Details(RFC 7807)
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
  "type": "https://api.codeforge.com/problems/lesson-not-found",
  "title": "课程不存在",
  "status": 404,
  "detail": "ID 为 123 的课程不存在",
  "instance": "/api/lessons/123"
}

跟着做

步骤 1:统一响应包装

public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;

    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.code = 200;
        response.message = "success";
        response.data = data;
        return response;
    }

    public static <T> ApiResponse<T> error(int code, String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.code = code;
        response.message = message;
        return response;
    }

    // getter/setter
}

步骤 2:自定义业务异常

public class BusinessException extends RuntimeException {
    private final int code;
    private final String message;

    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
        this.message = message;
    }

    // getter
}

public class LessonNotFoundException extends BusinessException {
    public LessonNotFoundException(Long id) {
        super(404, "课程不存在: " + id);
    }
}

public class SubmissionDeniedException extends BusinessException {
    public SubmissionDeniedException() {
        super(403, "您没有权限提交此课程的作业");
    }
}

步骤 3:全局异常处理器

@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException e) {
        log.warn("业务异常: {}", e.getMessage());
        return ResponseEntity.status(e.getCode())
                .body(ApiResponse.error(e.getCode(), e.getMessage()));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<Void>> handleValidationException(
            MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
                .map(error -> error.getField() + ": " + error.getDefaultMessage())
                .collect(Collectors.joining("; "));
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ApiResponse.error(400, "参数校验失败: " + message));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleUnknownException(Exception e) {
        log.error("系统异常", e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error(500, "服务器内部错误,请稍后重试"));
    }
}

步骤 4:RFC 7807 Problem Details

public class ProblemDetail {
    private String type;      // 问题类型 URI
    private String title;     // 简短描述
    private int status;       // HTTP 状态码
    private String detail;    // 详细说明
    private String instance;  // 发生问题的资源 URI

    // 扩展字段
    private Map<String, Object> properties;

    // getter/setter
}
@RestControllerAdvice
public class ProblemDetailExceptionHandler {

    @ExceptionHandler(LessonNotFoundException.class)
    public ResponseEntity<ProblemDetail> handleLessonNotFound(LessonNotFoundException e,
            HttpServletRequest request) {
        ProblemDetail problem = new ProblemDetail();
        problem.setType("https://api.codeforge.com/problems/lesson-not-found");
        problem.setTitle("课程不存在");
        problem.setStatus(404);
        problem.setDetail(e.getMessage());
        problem.setInstance(request.getRequestURI());

        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .contentType(MediaType.APPLICATION_PROBLEM_JSON)
                .body(problem);
    }
}

步骤 5:在 Controller 中使用

@GetMapping("/{id}")
public ApiResponse<Lesson> getById(@PathVariable Long id) {
    Lesson lesson = lessonRepository.findById(id)
            .orElseThrow(() -> new LessonNotFoundException(id));
    return ApiResponse.success(lesson);
}

@PostMapping("/{id}/submissions")
public ApiResponse<Submission> submit(@PathVariable Long id,
        @RequestBody @Valid SubmissionRequest request,
        @AuthenticationPrincipal User user) {
    if (!submissionService.canSubmit(user.getId(), id)) {
        throw new SubmissionDeniedException();
    }
    Submission submission = submissionService.create(user.getId(), id, request);
    return ApiResponse.success(submission);
}

步骤 6:测试错误响应

# 404 课程不存在
curl -s http://localhost:8080/api/lessons/99999 | jq

# 400 参数校验失败
curl -s -X POST http://localhost:8080/api/lessons \
  -H "Content-Type: application/json" \
  -d '{"title":""}' | jq

# 403 无权限
curl -s -X POST http://localhost:8080/api/lessons/123/submissions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer invalid-token" \
  -d '{"code":"..."}' | jq

讲清楚要点

  1. 错误码和 HTTP 状态码要一致:别返回 200 然后在 body 里写 "code": 500。调用方的 HTTP 客户端(如 axios、fetch)会根据状态码决定走 then 还是 catch。
  2. 错误信息要对用户友好,对开发有用:「课程不存在」比「NullPointerException」好,但最好带上上下文:「ID 为 123 的课程不存在」。
  3. 异常分层处理:Controller 层不 try-catch,抛给全局处理器;Service 层抛业务异常;DAO 层抛技术异常。全局处理器统一翻译。
  4. Problem Details 是国际标准:RFC 7807 定义了 application/problem+json 格式,包括 type、title、status、detail、instance 五个标准字段,可扩展自定义字段。
  5. 敏感信息不能泄露:堆栈信息、SQL 语句、内部 IP 不能出现在错误响应里。开发环境可以详细,生产环境要脱敏。

你可能会懵的地方

  • 为什么不用 HTTP 状态码表达所有错误? HTTP 状态码是传输层语义,粒度粗(比如 400 可能是参数缺失、格式错误、业务规则不满足)。业务错误需要更细的错误码(如 COURSE_EXPIRED、QUOTA_EXCEEDED),但 HTTP 状态码保持 200/400/404/500 等标准值。

  • 校验失败应该返回 400 还是 422? 400 Bad Request = 请求格式错误(如 JSON 语法错误);422 Unprocessable Entity = 格式正确但语义错误(如邮箱格式不对、必填字段为空)。严格来说校验失败应该返 422,但很多框架和团队习惯返 400。Spring Boot 的 @Valid 默认抛 MethodArgumentNotValidException,通常映射到 400。

  • 错误码用数字还是字符串? 数字(如 40401)节省空间,但可读性差;字符串(如 LESSON_NOT_FOUND)可读性好,但传输略大。推荐:HTTP 状态码用数字,业务错误码用字符串。

  • 全局异常处理器会拦截所有异常吗? 会拦截 Controller 层抛出的异常。但 Filter、Interceptor、异步线程里的异常不会被 @RestControllerAdvice 捕获。需要额外配置 ErrorController 或 HandlerExceptionResolver。

  • 生产环境要不要返回堆栈? 绝对不要。堆栈包含类名、方法名、行号,可能泄露代码结构。生产环境只返回错误码和用户友好的 message,详细错误记日志。

错法对照

错法问题正解
所有错误都返 200,body 里写错误码调用方无法通过 HTTP 状态码判断成功/失败错误返 4xx/5xx,body 里给详细错误码
直接抛 RuntimeException("出错了")无法区分错误类型,无法映射状态码自定义业务异常,带 code 和 message
错误响应格式不统一前端需要写多套解析逻辑统一 ApiResponse 或 Problem Details
把 SQL 异常直接抛给前端泄露数据库结构,可能被注入攻击全局处理器捕获,返回「系统繁忙」

卡点急救

  1. 异常被吞了,没走到全局处理器:检查是否有 @Controller 忘了加 @RestController(返回视图而不是 JSON),或异常在 Filter/Interceptor 中被捕获。
  2. 校验异常没有自定义 message:@Valid 需要配合 BindingResult 或全局处理器才能拿到字段错误信息。
  3. 异步方法异常没处理:@Async 方法抛异常会走 AsyncUncaughtExceptionHandler,不是 @RestControllerAdvice。需要单独配置。

面试常问

  • Spring Boot 怎么做全局异常处理? @RestControllerAdvice + @ExceptionHandler,按异常类型匹配处理方法。能讲出处理顺序(精确类型优先于父类)、ResponseEntity 包装、ProblemDetail 标准。

  • HTTP 状态码和业务错误码怎么配合? HTTP 状态码表达传输层结果(200/400/404/500),业务错误码表达业务语义(LESSON_NOT_FOUND、QUOTA_EXCEEDED)。两者结合:HTTP 状态码让客户端快速分类处理,业务错误码让前端精确提示。

  • RFC 7807 是什么? Problem Details for HTTP APIs,定义了 application/problem+json 格式,包含 type、title、status、detail、instance 五个字段,让错误响应标准化、机器可读。

自测三问

  1. 如果用户提交作业时课程已下架,应该返回什么 HTTP 状态码和业务错误码?
  2. 为什么生产环境不能返回 Java 堆栈信息?
  3. 设计一个「库存不足」的错误响应,用 Problem Details 格式写 JSON。

小结一句话

错误处理的核心是「一致性」:统一的响应格式、正确的 HTTP 状态码、清晰的业务错误码;RFC 7807 让错误响应成为一等公民。

延伸

  • RFC 7807 Problem Details
  • Spring Boot 异常处理文档
  • 下一课:认证——JWT、OAuth2 与 API Key 管理。
加载本节练习…
← 上一课REST 资源设计下一课 →认证