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

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

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

学习

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

更多

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

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
代码审查入门/课时/API 语义审查
RawBlame
课时目录12 节

代码审查入门

  1. 01API 语义审查
  2. 02实现语义
  3. 03文档审查
  4. 04测试审查
  5. 05代码风格
  6. 06Review 流程
  7. 07审查清单
  8. 08安全审查
  9. 09AI 辅助审查
  10. 10反馈技巧
  11. 11团队实践
  12. 12综合练习

API 语义审查

契约与命名一致性。

30 分钟

API 语义审查

这一课你将能做到

  1. 拿到一个 Spring Boot Controller 的 PR,能在 5 分钟内识别出 REST 语义错误(动词用错、状态码乱返、资源命名不规范)
  2. 判断接口契约变更是否破坏向后兼容,并给出迁移方案
  3. 审查 DTO、校验注解、错误响应结构是否一致且可演进

为什么要学

API 是后端和前端、和其他系统之间的「合同」。合同一旦签出去,改一个字都可能让调用方线上炸掉。Code Review 时审 API 语义,就是在签合同前帮双方把条款看清楚:路径是不是名词、方法是不是幂等、错误码前端能不能直接用、这次改动老版本 App 还能不能调通。

人话:实现写错了,改一行代码就行;API 语义设计错了,会跟着你三年,因为总有客户端在用旧版本。

先建立图像

一个语义清晰的接口长这样:

资源: /api/v1/courses                ← 名词复数,无动词
├─ GET    /api/v1/courses           200 列表查询(幂等)
├─ POST   /api/v1/courses           201 创建(返回 Location)
├─ GET    /api/v1/courses/{id}      200 / 404
├─ PUT    /api/v1/courses/{id}      200 全量更新(幂等)
├─ PATCH  /api/v1/courses/{id}      200 部分更新
└─ DELETE /api/v1/courses/{id}      204 删除(幂等)

错误响应统一结构:
{ "code": "COURSE_NOT_FOUND", "message": "...", "traceId": "..." }

审查时就拿这张图去对照:路径里出现 getCourse、deleteCourseById 这种动词的,一律打回。

跟着做

步骤 1:审路径和方法语义

❌ 反例(本站早期风格的坏味道):

@RestController
@RequestMapping("/api")
public class CourseController {

    @PostMapping("/getCourseList")          // 用 POST 做查询
    public List<CourseVO> list() { ... }

    @GetMapping("/deleteCourse")            // GET 做删除,浏览器预加载都会误删
    public void delete(@RequestParam Long id) { ... }

    @PostMapping("/updateCourseInfo")
    public Result update(@RequestBody Course course) { ... }
}

✅ 正例:

@RestController
@RequestMapping("/api/v1/courses")
public class CourseController {

    @GetMapping
    public PageResult<CourseVO> list(CourseQuery query) { ... }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        courseService.delete(id);
        return ResponseEntity.noContent().build();   // 204
    }

    @PutMapping("/{id}")
    public CourseVO update(@PathVariable Long id,
                           @Valid @RequestBody CourseUpdateReq req) { ... }
}

审查动作:逐条接口问三个问题——路径是纯名词吗?HTTP 方法幂等性匹配吗(GET 只读、PUT/DELETE 幂等)?状态码准确吗(创建返 201 而不是 200)?

步骤 2:审向后兼容

❌ 反例:直接改字段名、删字段、改类型

// 上一版
public class CourseVO {
    private String courseName;   // v1 已上线
}

// 这次 PR 直接改名
public class CourseVO {
    private String title;        // 老前端读 courseName 全是 null
}

✅ 正例:只增不改,废弃用 @Deprecated 标记并给迁移期

public class CourseVO {
    @Deprecated(since = "1.2", forRemoval = true)
    private String courseName;   // 保留并继续填充,下个大版本删
    private String title;
}

审查动作:凡是涉及线上已有接口,检查「老客户端还能不能正常工作」。破坏性变更必须走版本号(/api/v2/)或双字段过渡期。

步骤 3:审 DTO 与校验

❌ 反例:Controller 直接收实体、无校验、错误信息裸奔

@PostMapping
public Result create(@RequestBody Course course) {   // 实体直接暴露,createTime 都能被客户端塞
    courseService.save(course);
    return Result.ok();
}

✅ 正例:

public record CourseCreateReq(
        @NotBlank(message = "课程名不能为空")
        @Size(max = 64) String title,

        @NotNull @Positive(message = "价格必须大于 0")
        BigDecimal price
) {}

@PostMapping
public ResponseEntity<CourseVO> create(@Valid @RequestBody CourseCreateReq req) {
    CourseVO vo = courseService.create(req);
    URI location = URI.create("/api/v1/courses/" + vo.getId());
    return ResponseEntity.created(location).body(vo);   // 201 + Location
}

步骤 4:审错误响应结构

前端 Next.js 侧通常是统一封装 fetch 的,错误结构不稳定,前端就要为每个接口写 if-else。

❌ 反例:有时返 {"msg":"fail"},有时返 {"error":-1},有时直接抛 500 堆栈。

✅ 正例:全局异常处理统一结构,业务码可读、带 traceId 方便排查

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BizException.class)
    public ResponseEntity<ErrorBody> handle(BizException e, HttpServletRequest req) {
        return ResponseEntity.status(e.getStatus())
                .body(new ErrorBody(e.getCode(), e.getMessage(),
                        MDC.get("traceId")));
    }
}

步骤 5:审分页与排序契约

@GetMapping
public PageResult<CourseVO> list(
        @RequestParam(defaultValue = "1") @Min(1) int page,
        @RequestParam(defaultValue = "20") @Max(100) int size) { ... }

审查点:size 必须设上限,否则有人传 size=100000 直接拖垮 MySQL;分页参数风格全站统一(page/size 还是 offset/limit,别混用)。

讲清楚要点

  1. REST 的核心是资源而不是动作:URL 描述「什么东西」,HTTP 方法描述「干什么」。/courses/{id} + DELETE 才是删除,/deleteCourse 是 RPC 风格混进 REST。
  2. 幂等性是语义审查的灵魂:GET/PUT/DELETE 必须幂等(调一次和调十次结果一样),POST 不幂等所以要做防重复提交(唯一索引或幂等键)。
  3. 兼容性三原则:只加字段不改字段;不改字段类型;不改默认值语义。做不到就升版本。
  4. 状态码是 API 的一部分:200/201/204/400/401/403/404/409/422 各有语义,一律返 200 再在 body 里塞 code 是对 HTTP 的浪费(虽然国内很多团队这么干,但新项目建议语义化)。
  5. DTO 与实体必须分离:实体映射数据库,DTO 是契约。混用等于把数据库表结构暴露给前端,加字段都变成兼容风险。

你可能会懵的地方

  • POST 查询列表到底行不行? 复杂查询条件放 URL 里有长度限制且不便缓存,业内普遍接受 POST /courses/search 这类妥协。审查时允许,但要求团队统一约定,别同一个项目两种风格并存。

  • PUT 和 PATCH 到底差在哪? PUT 是全量替换,不传字段就该被清空;PATCH 只改传的字段。审查时看到 PUT 实现里只更新非 null 字段,说明实现是 PATCH 语义却挂了 PUT 的名字。

  • 删除成功返 200 还是 204? 没有 body 要返回就用 204;想返回被删资源快照用 200。两者都对,错的是同一个项目里随心情切换。

  • 业务错误码和 HTTP 状态码重复吗? 不重复。HTTP 状态码给网关、监控、浏览器看;业务码(如 COURSE_ALREADY_PUBLISHED)给前端做精确提示。审查时看两层是否都在。

  • 接口要不要版本号? 对外 API 建议带(/api/v1/),纯内部前后端一体部署可以不带。审查重点是有没有「版本策略」,而不是非要 v1。

错法对照

错法问题正解
@GetMapping("/deleteCourse")GET 有副作用,CDN/预加载会误触发DELETE /courses/{id}
更新接口直接改 VO 字段名老前端白屏双字段过渡或升 v2
Controller 直接收/返实体表结构泄露、被恶意赋值专用 Req/VO + MapStruct
所有错误都抛 RuntimeException前端拿到的全是 500 堆栈业务异常 + @RestControllerAdvice
size 无上限恶意大分页拖垮 DB@Max(100) 硬限制

卡点急救

  1. 前端说「接口改了不通知」:建立契约文件(OpenAPI/Springdoc 自动生成),PR 里契约文件 diff 必须人工确认;CI 加 springdoc-openapi 导出对比。
  2. 不确定这次改动兼不兼容:本地起两个版本,用老前端调新后端跑一遍核心路径;或者直接用契约测试(Spring Cloud Contract)。
  3. 团队为「200 vs 业务码」吵个不停:查项目里占比高的那种风格,跟多数派走,写进团队规范文档,别在单个 PR 里翻案。

面试常问

  • RESTful 的幂等性怎么理解?哪些方法幂等? 同一请求执行一次与多次,服务端资源状态相同。GET/PUT/DELETE/HEAD/OPTIONS 幂等;POST 不幂等。实际项目中 POST 创建订单要通过幂等键或唯一约束把「不幂等」兜住,防止重复下单。

  • 如何做接口版本管理? URL 路径版本(/v1/)、请求头版本(Accept: application/vnd.x.v2+json)、参数版本三种。国内最常用路径版本。关键是「什么时候必须升版本」:删字段、改语义、改类型。

  • 200 统一返回 + body 里 code,还是语义化状态码? 两种都见过。统一 200 的好处是穿透某些网关/代理时不被误处理,前端拦截器简单;语义化的好处是监控、缓存、SEO 友好。回答时讲清两者取舍和你项目的选择依据,比背答案加分。

自测三问

  1. PUT /courses/{id} 的实现里只更新非 null 字段,有没有问题?
  2. 一个已上线接口要把 price 从 Integer(分)改成 BigDecimal(元),怎么改才不炸老客户端?
  3. 为什么 Controller 不能直接把 JPA/MyBatis 实体当返回值?

小结一句话

API 语义审查就是替所有调用方看合同:路径是名词、方法讲幂等、状态码说真话、变更保兼容。

延伸

  • RFC 9110 HTTP Semantics
  • Springdoc OpenAPI 文档
  • 《RESTful Web APIs》(Leonard Richardson)
  • 下一课:实现语义——逻辑正确性与边界条件审查。
加载本节练习…
下一课 →实现语义