目录

周一早上,后端开发小陈提交了一个用户注册接口的 PR,三百多行代码,涉及参数校验、数据库写入、消息队列推送。

reviewer 老周扫了一眼,写了句 “LGTM”(Looks Good To Me),点了通过。

两周后线上出了事。

注册接口被注入了一条恶意数据,数据库写脏了,消息队列狂推了八千条重复消息,下游消费者直接被打爆。

复盘的时候翻到那个 PR,发现代码里有三个问题。

SQL 语句用的字符串拼接、消息推送没做幂等、参数校验漏了邮箱格式。

任何一个被指出来,都不至于上线。

老周说:“我当时看了一眼,觉得逻辑大概没问题,就过了。”

再看另外一个案例。

reviewer 把每个缩进、每个命名都挑一遍,语气还冲。

“你这变量名起的什么玩意?”

“为什么不加注释?你是给自己看的?”

“这种低级错误也犯?”

看到这些消息后提 PR 的人心态炸了。

“行行行,你厉害,你说的都对,改完行了吧。”

下次再提 PR,能躲就躲,能合就合,代码质量没提升,团队氛围倒是搞差了。

这两种情况,一个太松,一个太碎。

其实问题都出在同一个地方。

都没搞清楚 Code Review 到底在 review 什么,以及怎么 review 才真正有效。


Code Review 到底在 Review 什么

定义 Code Review(代码评审)是团队协作中由代码作者以外的成员对代码变更进行审查的实践,目的是在代码合入主干之前发现逻辑错误、安全风险、设计缺陷和可维护性问题,同时通过评审过程中的交流促进团队编码风格和知识经验的对齐。

最最最最核心的关键在于:Code Review 检查的是代码,不是写代码的人。

但话说回来,既然有人参与其中,就不可能完全脱离”人”这个因素。

代码是人写的,review 是人做的,反馈也是人接的。

因此 Code Review 的过程中要对事要足够的严谨,对人要足够的尊重。

虽然你可能确实很牛,但是请牢记:Code Review 不是你秀优越感的地方。

Code Review 的目的,是让被 review 的人把注意力放在代码上,而不是放在自我防卫上。

说”对事不对人”容易,做到很难。

难在哪?难在沟通方式。

同样一个问题,“这段逻辑跟需求文档不一致,看看是不是漏了”和”你怎么连需求都没看清楚就写了?”

说的是同一件事,但接收者的感受完全不同。

前者会去看需求文档,后者会进入防御状态。

因此 Code Review 实践中要用两条腿走路。

一条是技术审查能力,看代码能不能发现问题。

另一条是沟通表达能力,发现问题后要考虑怎么说出来对方才能听得进去。

两条腿都得有,缺一条都走不稳。

关注维度图

Code Review 的检查范围可以拆成五个维度,每个维度回答一个核心问题:

flowchart TB
    PR["代码变更 PR"] --> R{"Review 检查"}

    R --> D1["规范一致性<br/>风格是否统一"]
    R --> D2["业务正确性<br/>逻辑是否成立"]
    R --> D3["安全性<br/>有没有漏洞"]
    R --> D4["架构设计<br/>耦合是否合理"]
    R --> D5["可维护性<br/>别人能不能看懂"]

五个维度不是并列的,有优先级。

业务正确性和安全性是硬伤,必须拦住。

架构设计影响长远,要认真看。

规范一致性和可维护性属于质量提升,可以提建议但不必每次都卡。

对事不对人的底层逻辑

“对事不对人”这句话在 Code Review 里有具体的操作含义:

维度对事对人
描述问题“这段代码在第 42 行有空指针风险”“你怎么又没判空?”
提出建议“这里用参数化查询会更安全”“你不知道 SQL 拼接会注入吗?”
表达期望“这个 PR 需要补充单元测试”“你能不能上点心?”
讨论方案“方案 A 和方案 B 各有利弊,你倾向哪个?”“你选的方案不行,换一个”

区别在哪?

对事的表达聚焦于代码本身的事实(哪一行、什么问题、什么风险)。

对人的表达夹杂了对作者能力或态度的评价(你怎么、你能不能、你不知道、你问问谁谁谁、你看谁怎么怎么样)。

人是会带情绪的。

被评价能力的时候,本能反应是防御,而不是反思代码。

Code Review 要有效,就得让被 review 的人把注意力放在代码上,而不是放在自我防卫上。

这就是”对事不对人”的实操含义:把你发现的问题用客观事实描述出来,不加对人的判断。


能解决什么问题

问题场景没有 CR 会怎样有了 CR 会怎样
逻辑漏洞(边界条件没处理)上线后偶发崩溃,排查花半天review 时发现,改完再上线
安全风险(SQL 拼接、敏感信息泄露)被人利用,出安全事故review 时拦住,从源头杜绝
魔法数字和硬编码后续维护改一个数字要全局搜索review 时指出,抽成常量
过度耦合的设计改一个功能牵连三个模块review 时讨论方案,控制耦合范围
缺少测试回归测试全靠手工,漏了就出事review 时要求补测试
团队风格不统一每个人写的代码像不同项目review 逐步拉齐风格
知识孤岛核心逻辑只有作者懂review 过程中知识自然扩散

注意看这张表,CR 解决的不只是”找 bug”。

找 bug 是最低层价值,更高层的价值在于架构把关、知识共享和团队风格统一。

很多团队把 CR 用成了”查错工具”,这就浪费了一大半作用。


什么时候做 Code Review

合适的时机

  • 每次代码合入主干之前:这是 CR 的标准时机。代码一旦进了主干,影响面就大了,再发现问题修复成本也高了。
  • PR 提交后 4 小时内开始:及时响应是 CR 效率的关键。拖两天再 review,写代码的人上下文都忘光了,沟通成本翻倍。
  • 功能开发到可测试状态:不需要等整个功能全做完,一个可独立测试的子功能就可以提 PR 做 review,保持小步提交。

不适合用 CR 替代的场景

场景为什么不适合应该怎么做
大规模架构重构改动量大,review 很难看全先做设计评审,方案定好再写代码
探索性技术验证代码可能随时推翻验证完确定方向后再 review
紧急线上故障修复时间不允许走完整流程修完上线后补 review,把问题记录下来
自动生成的代码不是人写的,review 没意义review 生成器的配置和模板

CR 是代码合入前的质量关卡,但它替代不了前期的设计评审和后期的测试验证。

它是一道防线,不是唯一防线。


常见误区与陷阱

误区一:把 CR 当形式走过场 “LGTM” 式 review 等于没 review。

原因要么是 reviewer 没时间仔细看,要么是不想”得罪人”。

但 CR 走过场的危害比不做还大:给了虚假的安全感,出了问题还多了一份”已 review”的责任记录。

应对方法

建立 review 检查清单,reviewer 需要逐项确认。

对超过一定规模(比如 400 行)的 PR,要求至少两人 review。

误区二:纠结鸡毛蒜皮,忽略核心逻辑

花五分钟争论变量名用 userInfo 还是 user_info,却没花三十秒看一眼 SQL 语句是不是拼接的。

这种 review 看起来很认真,实际上把精力花在了低价值的地方。

应对方法

规范层面的问题交给 Lint 工具自动检查,人工 review 的精力放在业务逻辑、安全风险和架构设计上。

误区三:用攻击性语言提问题

这是技术人最容易犯的错。

两种杀伤力巨大的句式,看看你说过(或听过)没有?

祈使句:“把这里改掉。""加个注释。""重新写。”

祈使句的潜台词是”你在下位,我在上位”,是一种命令。

在技术团队里,大家职级可能差不多,突然来一句祈使句,对方的感受是”你凭什么命令我”。

反问句:“你不知道这里要判空吗?""这种写法没问题你不觉得吗?""为什么要用这个方法?”

反问句的潜台词是”你应该知道但你没做到”,是一种质疑。

被质疑的人第一反应不是反思代码,而是找理由为自己辩护。

两种句式都有个共同的特点:把焦点从代码转移到了人身上。

一旦焦点转移,讨论就变成了辩论,辩论就变成了情绪对抗。

应对方法

把祈使句改成陈述句或疑问句,把反问句改成中性提问。

后面有一张话术对照表,也未必都对,参考着实践就可以。

误区四:PR 太大,review 不过来

一个 PR 两千行,reviewer 打开一看,心态直接崩了。

人的注意力是有限的,研究表明,一次 review 超过 400 行代码,发现问题的概率会急剧下降。

reviewer 要么草草扫一遍点通过,要么拖几天不看。

应对方法

控制 PR 规模。每次提交建议不超过 300 到 400 行。

大改动拆成多个小 PR,分批次提交。

如果一个功能确实很大,按模块或层级拆分,每个 PR 聚焦一个独立的子功能。

误区五:所有问题一视同仁

有些问题不改就没法上线(比如 SQL 注入),有些问题改了更好但不改也没事(比如变量名不够语义化)。

如果不区分优先级,被 review 的人看到一长串意见,可能先挑容易的改(改变量名),把重要的(补测试、修逻辑)放后面,甚至遗漏。

应对方法

每条意见标注级别。

级别含义示例
Blocking必须修改,不改不能合入SQL 注入风险、空指针、逻辑错误
Suggestion建议修改,改了更好命名优化、提取公共方法、补充注释
Nitpick可选修改,纯属个人偏好缩进风格、括号位置、注释措辞

标注级别的好处:被 review 的人知道哪些必须先改,哪些可以后面再说,不会眉毛胡子一把抓。


怎么做:检查清单与沟通技巧

Review 检查清单 拿到一个 PR,按以下顺序检查。

总体原则是先看大局再看细节,先看逻辑再看格式。

第一步:看大局

打开 PR 先别急着逐行看。

花两分钟搞清楚以下几个问题:

  • 这个 PR 做了什么?(看标题和描述)
  • 为什么做?(看关联的需求或 issue)
  • 怎么做的?(快速浏览文件列表和主要改动)

搞清楚这几点,再开始逐行 review。

不了解意图就开始挑毛病,容易提出跟需求无关的意见。

第二步:逐维度检查

维度检查内容常见问题
代码规范命名风格、文件组织、导入顺序是否跟团队规范一致变量名风格混用(驼峰和下划线混搭)、魔法数字、未使用的导入
业务逻辑代码是否正确实现了需求,边界条件是否处理空指针、数组越界、负数处理、并发竞态、事务边界不清
安全性是否存在注入、泄露、越权等风险SQL 拼接、XSS、硬编码密钥/密码、敏感信息记日志、权限校验缺失
架构设计改动是否符合现有架构,是否引入不必要的耦合跨层调用、循环依赖、上帝类膨胀、接口职责不清
可维护性代码是否可读、可测试、可扩展缺少注释、注释与代码不一致、无单元测试、方法过长、圈复杂度过高

每个维度应关注的重点如下:

代码规范:这一层主要靠工具。团队的 Lint 规则配置好之后,格式问题、未使用变量、缺少分号之类的自动就能拦住。人工 review 在规范层面重点看的是命名是否语义清晰和文件组织是否合理,而不是缩进用几个空格。

业务逻辑:这是 review 的核心。重点看边界条件:空值怎么处理?数组为空怎么办?负数进来会不会出问题?并发场景下有没有竞态?事务边界画对了吗?这些地方出问题往往不是逻辑写错了,而是没想到某种输入情况。

安全性:安全问题的特点是”平时没事,出事就是大事”。重点查:SQL 语句是不是拼接的?用户输入有没有过滤?密码、密钥、Token 有没有硬编码在代码里?异常信息有没有把堆栈直接返回给用户?日志里有没有打印敏感数据?权限校验有没有遗漏?

架构设计:看改动是否跟现有架构一致。比如项目是分层架构,那业务逻辑就不应该出现在 Controller 里。如果改动引入了对其他模块的直接依赖,要问一下:这个依赖是必要的,还是可以通过接口抽象解耦?

可维护性:最基本的一条:注释是否与代码匹配。改了代码但忘了改注释,比没有注释还误导人。其次看方法长度和圈复杂度,一个方法超过 80 行或者嵌套超过 4 层,就该考虑拆分了。最后看有没有测试,核心逻辑有没有被测试覆盖。

第三步:魔法数字和硬编码专项

这一项单独拎出来强调,因为太常见了。

// 反面示例
if (user.getStatus() == 3) {
    Thread.sleep(5000);
    retryCount = 3;
}

// 正面示例
if (user.getStatus() == UserStatus.LOCKED.getCode()) {
    Thread.sleep(RETRY_INTERVAL_MS);
    retryCount = MAX_RETRY_COUNT;
}

反面示例的代码能跑,但没人知道 3 是什么状态,5000 毫秒是怎么定的,3 次重试的依据是什么。

三个月后改需求,谁也不敢动这些数字,因为不知道改了会影响什么。

review 的时候遇到魔法数字,直接指出来:“这里的 3 和 5000 建议抽成常量,加上命名说明含义。”

PR 规模控制

PR 行数Review 质量建议
< 100 行逐行看,问题发现率高理想状态
100-300 行能仔细看完正常范围
300-400 行需要分块看,注意力开始下降上限
400-1000 行草草扫一遍,容易漏拆分
> 1000 行基本等于没 review必须拆分

拆分策略:

  • 按功能模块拆:一个 PR 只做一个子功能
  • 按层级拆:先提数据层改动,再提业务逻辑,最后提接口层
  • 先提基础设施改动(新增工具类、配置文件),再提使用这些基础设施的业务代码

自动化工具搭配

人工 review 的精力有限,能交给工具的就不要自己看。

检查项工具说明
代码格式ESLint / Prettier / clang-format配置团队规范,CI 自动拦截
静态分析SonarQube / CodeQL检测潜在 bug、安全漏洞、代码异味
安全扫描Snyk / Dependabot依赖漏洞、已知 CVE 检测
单元测试覆盖率JaCoCo / IstanbulCI 中设置覆盖率门槛
类型检查TypeScript / mypy编译期拦截类型错误

工具能发现的,就不要让人看了;人看的,应该是工具发现不了的。

逻辑对不对、设计合不合理、命名清不清晰,这些是需要人判断的,交给工具也没用。

沟通话术对照表

这是整篇文章里最重要的部分。

技术实力大家基本都差不多,但是沟通能力和表达能力的差距就很大了。

同样一个问题,换个说法,效果会天差地别。

场景攻击性表达(避免)建设性表达(推荐)
指出逻辑错误“你这逻辑根本跑不通”“第 42 行的循环条件,当 list 为空时会跳过处理,跟需求里的’空列表返回默认值’不一致”
指出安全风险“SQL 拼接你不知道会注入?”“这里的 SQL 用了字符串拼接,建议改成参数化查询,参考第 88 行的写法”
要求加注释“加个注释这么难?”“这段逻辑比较复杂,加几行注释说明意图会更清晰”
建议换方案“你这方案不行”“方案 A 和方案 B 都能实现,A 更简单但扩展性差一些,你倾向哪个?”
要求补测试“核心逻辑没测试你敢提?”“这个方法没有对应的单元测试,建议补一下边界条件的用例”
指出命名问题“这变量名起的什么玩意”“data 这个命名太泛了,改成 userProfileList 会更明确”
指出魔法数字“这 3 是什么意思谁知道”“这里的 3 建议抽成常量,加上命名说明含义,比如 MAX_RETRY_COUNT”
要求拆分方法“这方法太长了谁看得完”“这个方法有 120 行,建议按职责拆成三个方法,可读性会好很多”

规律总结:

  • 描述事实,不加评价:“第 42 行有空指针风险”是事实,“你怎么又没判空”是评价。
  • 用”建议”代替”必须”(除非真的是 Blocking 问题):“建议改成参数化查询”比”必须改”更容易接受。
  • 给方向,不给命令:“参考第 88 行的写法”比”照着改”更有帮助。
  • 提问代替反问:“你这里用 ArrayList 的考虑是什么?“比”为什么不用 LinkedList?“更让人愿意回答。

这套话术的目的只有一个:把沟通的焦点始终锚定在代码上。

你讨论的是代码的问题,跟人的能力无关。


实战案例:一个用户注册接口的 Code Review

场景背景 后端开发小陈提交了一个用户注册接口的 PR,reviewer 是技术负责人老周。

PR 描述:新增用户注册接口,支持邮箱注册,包含参数校验、数据库写入、欢迎邮件发送。

代码主要部分(Java,仅为示意):

public class UserService {

    public boolean register(String email, String password) {
        // 参数校验
        if (email == null || password == null) {
            return false;
        }

        // 检查邮箱是否已注册
        String sql = "SELECT * FROM users WHERE email = '" + email + "'";
        List<User> existing = jdbcTemplate.query(sql, new UserRowMapper());
        if (!existing.isEmpty()) {
            return false;
        }

        // 密码加密并存储
        String encryptedPwd = MD5.encrypt(password);
        String insertSql = "INSERT INTO users (email, password, status) VALUES ('"
            + email + "', '" + encryptedPwd + "', 1)";
        jdbcTemplate.execute(insertSql);

        // 发送欢迎邮件
        sendWelcomeEmail(email);

        return true;
    }

    private void sendWelcomeEmail(String email) {
        // 发送邮件
        MailSender.send("smtp.company.com", 587, "noreply@company.com",
            "password123", email, "欢迎注册", "欢迎加入!");
    }
}

Review 过程

老周拿到 PR 后的 review 记录:


Blocking-1:SQL 注入风险(第 12 行、第 20 行)

第 12 行和第 20 行的 SQL 使用了字符串拼接,存在注入风险。如果 email 参数中包含单引号或其他特殊字符,可以构造恶意 SQL。建议改成参数化查询:

// 查询
String sql = "SELECT * FROM users WHERE email = ?";
List<User> existing = jdbcTemplate.query(sql, new Object[]{email}, new UserRowMapper());

// 插入
String insertSql = "INSERT INTO users (email, password, status) VALUES (?, ?, ?)";
jdbcTemplate.update(insertSql, email, encryptedPwd, 1);

Blocking-2:密码使用 MD5 加密(第 17 行)

MD5 已被证明不安全,存在彩虹表攻击风险。建议使用 BCrypt 或 Argon2,并加随机盐。可以参考项目中 AuthUtils.hashPassword() 方法,已经封装好了。


Blocking-3:硬编码敏感信息(第 28-29 行)

sendWelcomeEmail 方法中,SMTP 服务器地址、账号名、密码都硬编码在代码里。“password123” 这种明文密码出现在源码中是严重的安全隐患,代码一旦泄露密码就暴露了。建议所有邮件配置放到配置文件或环境变量中,通过 @Value 注入。


Suggestion-1:参数校验不完整(第 8-10 行)

目前的校验只判断了 null,没有校验格式。空字符串、非法邮箱格式(比如 “abc”)都能通过。建议补充:

  • email 用正则或 @Email 注解校验格式
  • password 校验长度和复杂度(至少 8 位,包含字母和数字)

Suggestion-2:缺少事务管理

register 方法中有数据库写入和邮件发送两个操作。如果邮件发送失败,用户已经写入数据库了,但没收到欢迎邮件。建议加事务管理,或者把邮件发送改成异步消息队列,解耦主流程。


Suggestion-3:缺少单元测试

核心逻辑没有对应的单元测试。建议补充以下用例:

  • 正常注册(合法邮箱+合法密码)
  • 邮箱已注册(应返回 false)
  • 邮箱格式非法(应返回 false)
  • 密码为空(应返回 false)

Suggestion-4:魔法数字(第 20 行)

第 20 行的 1 是用户状态码,建议抽成常量或枚举,比如 UserStatus.ACTIVE.getCode()。否则三个月后没人记得 1 代表什么状态。


Nitpick-1:方法职责过多

register 方法同时做了参数校验、查重、加密、存储、发邮件五件事,建议按职责拆分。不过这个可以放到后续重构中处理,不阻塞本次合入。


Review 结果

老周在 PR 中留了 3 个 Blocking、4 个 Suggestion、1 个 Nitpick,整体沟通语气保持客观,每条意见都给了具体位置和修改建议。

小陈收到 review 后的反应:先看 Blocking 的三条,SQL 注入和硬编码密码确实是自己疏忽了,MD5 是图省事用了旧工具类。三个 Blocking 全部修了。 Suggestion 里邮箱格式校验和单元测试这次补上,事务管理跟架构改动有关,跟老周讨论后决定放到下一个迭代。 Nitpick 记到技术债列表里,后续重构时处理。

改完后小陈更新了 PR,老周复核确认 Blocking 全部修复,点了通过。

这个案例说明了什么

  1. review 顺序是先看安全再看逻辑最后看规范:老周先拦住了 SQL 注入和硬编码密码,这些是上线就会出事的。参数校验和测试是 Suggestion,不卡但建议改。方法拆分是 Nitpick,记到技术债后面再说。
  2. 每条意见都给了具体位置和修改建议:光说”这里有问题”没用,要告诉你在哪一行、什么问题、怎么改。被 review 的人改起来才有方向。
  3. 沟通语气全程客观:没有”你怎么""为什么不”这类表达。每条意见都是”代码中的事实+建议”,焦点始终在代码上。
  4. 不是所有问题都要这次改完:Blocking 必须改,Suggestion 看情况,Nitpick 可以后面再说。分清主次,避免 review 变成无止境的改改改。

总结

Code Review 表面上审的是代码,实际上考验的是团队的沟通文化。

技术问题好发现,沟通方式难把握。

一个团队 review 做得好不好,不看出了多少条意见,看的是提意见的人和接意见的人,是不是都在讨论代码,而不是在较劲谁对谁错。

把焦点放在代码上,把尊重留给写代码的人。

这才是”对事不对人”真正该有的样子。