主题
界面
主题
这是一节"投入产出比"极高的课:学一次,团队省下无数次争论。
它要解决的核心问题:
Code Review 里,两个人花了 20 分钟争论"这里该不该换行"
↓
而真正的 bug(空指针没判)就在旁边,没人看本节的答案:把"风格"交给工具,把"人"留给逻辑与安全 ✓
完成本节后,你应该能够:
先看三个真实的成本:
| 成本 | 说明 |
|---|---|
| Review 时间被占用 ⚠️ | 讨论缩进、命名、import 顺序 → 侵蚀了本该看逻辑的时间 ✗ |
| diff 噪音 ✗ | 有人改了一行,但因为格式化不同,diff 显示改了 50 行 → review 看不出真正改了什么 ✗✗ |
| 新人不知道规矩 ⚠️ | 规范只存在于"老同事的脑子里" → 新人靠被吐槽来学习 ✗ |
第二条尤其致命:
"格式化不一致"会让
git diff失去意义 ——而 code review 的核心工具就是 diff ✓
工具化能同时解决这三条 ✓
这是本节最重要的分类 ✓
| 类别 | 例子 | 谁负责 |
|---|---|---|
| ① 机器能判定的 ★ | 缩进、行长、import 顺序、命名格式、未使用的变量、魔法数字 | 工具(自动检查 + 自动修复)✓ |
| ② 需要人判断的 ★ | 这个抽象合理吗?边界情况处理了吗?有没有安全风险? | Code Review ✓ |
判断标准:
"这条规范,能不能写成一个确定性的规则(if/else 就能判断)?"
- 能 → 交给工具 ✓
- 不能 → 交给人 ✓
举例:
| 规范 | 类别 | 理由 |
|---|---|---|
| "缩进用 4 个空格" | ① 机器 | 可以精确判定 ✓ |
| "类名用大驼峰" | ① 机器 | 可以(正则)✓ |
| "方法不超过 50 行" | ① 机器 | 可以(计数)✓ |
| "变量命名要有意义" | ② 人 | "有意义"无法精确判定 ✗ |
| "这个设计合理吗" | ② 人 | — |
| "这个 SQL 会不会慢" | ② 人(或压测) | — |
⚠️ 一个常见的误用:把"机器能判定的"写进 review 清单 ✗ ——这是对人力的浪费 ✓
如果一条规则能被工具检查,却靠人来检查,那就是在用最贵的资源做最便宜的事 ✓
代表:Spotless(推荐)、google-java-format、IDE 自带格式化
Spotless 的配置(Maven):
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>2.43.0</version>
<configuration>
<java>
<googleJavaFormat>
<version>1.24.0</version>
<style>GOOGLE</style>
</googleJavaFormat>
<removeUnusedImports/>
<importOrder/>
</java>
</configuration>
<executions>
<execution>
<goals><goal>check</goal></goals> <!-- 绑定到 validate 阶段:构建时就检查 -->
<phase>validate</phase>
</execution>
</executions>
</plugin>两种用法:
| 命令 | 作用 |
|---|---|
mvn spotless:apply ★ | 自动修复(改你的代码)✓ |
mvn spotless:check | 只检查(不通过就构建失败)✓ |
实践建议:
| 建议 | 说明 |
|---|---|
本地用 apply(或 IDE 的 save action)✓ | 写代码时自动格式化,不产生 diff 噪音 ✓ |
CI 用 check ★ | 保证没人绕过 ✓ |
为什么"自动修复"很重要:
只报错不修复的工具,会让人产生抵触("又要我手工改 20 行缩进")✗
spotless:apply让"符合规范"变成一条命令的事 ✓这与 《Git Flow 与团队协作》 的"让正确的做法成为最容易的做法"是同一条原则 ✓
四类工具的分工(记住这个,选型不迷茫):
| 工具 | 查什么 | 典型问题 |
|---|---|---|
| Checkstyle | 代码风格 | 命名、缩进、行长、import 顺序 ✓ |
| PMD | 可疑的代码写法 | 空 catch 块、重复代码、过于复杂的表达式 ✓ |
| SpotBugs | 字节码层面的 bug ★ | 空指针风险、错误的 equals、资源未关闭 ✓ |
| ErrorProne | 编译期就报错 ★★ | 常见编码错误(在 javac 时就拦住)✓ |
各自的定位:
| 阶段 | 误报率 | 价值 | |
|---|---|---|---|
| Checkstyle | 源码文本 | 低 | 统一风格 ✓ |
| PMD | 源码 AST | 中 ⚠️ | 找出坏味道 |
| SpotBugs | 字节码 ★ | 中 | 能发现"人看不出"的问题 ✓ |
| ErrorProne | 编译期 ★★ | 低 | 反馈最快 ✓ |
实践建议:
| 建议 | 说明 |
|---|---|
| 从少量规则开始 ★ | 一上来开全部规则 → 几百个报错 → 没人愿意修 ✗ |
| 优先开"高价值"规则 | 空指针、资源泄漏、错误的 equals ✓ |
误报多的规则用 @SuppressWarnings 局部关闭 | 并写注释说明 ✓ |
⚠️ 一个重要的判断:
静态检查工具的目标是"发现问题",不是"消灭所有警告" ✓
如果一个规则产生的误报多于真问题,就关掉它 ——否则大家会开始忽略警告,工具就失效了 ✗✗
.editorconfig(放在仓库根目录):
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.{yml,yaml,json}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false它的价值:
| 价值 | 说明 |
|---|---|
| 跨编辑器/IDE ★ | IDEA、VS Code、Vim 都支持 ✓ |
| 新文件就符合规范 | 不用等格式化工具 ✓ |
| 解决"换行符"之争 | end_of_line = lf 一次性解决 Windows/Linux 混用的 diff 灾难 ✓✓ |
最后一条特别实用:
Windows(CRLF) 与 Linux(LF) 混用时,改动一个字符可能导致整个文件在 diff 里显示"全改了" ✗
.editorconfig+git config core.autocrlf能彻底解决 ✓
这是"自动校验"能真正生效的关键一环 ✓
原理:
工具运行完 → 退出码 0(通过)/ 非 0(有问题)
↓
CI 看到非 0 → 流水线失败 → 不允许合并 ✓本节的检查器就是这么设计的(System.exit(hasProblems() ? 1 : 0))✓
实测(本项目):
# 通过的情况
java "-Dstdout.encoding=UTF-8" labs/engineering/CodeStyleChecker.java labs/spring-boot-web/src
# …… 结果:通过 ✓ → 退出码 0
# 不通过的情况
java "-Dstdout.encoding=UTF-8" labs/engineering/CodeStyleChecker.java labs/spring-boot-web/src > $null 2>&1
$LASTEXITCODE # 0
java "-Dstdout.encoding=UTF-8" labs/engineering/CodeStyleChecker.java labs/data-access/src > $null 2>&1
$LASTEXITCODE # 1 ← ★ 有问题,CI 会失败三段式落地:
| 位置 | 用什么 | 目的 |
|---|---|---|
| ① 编辑器 | .editorconfig + IDE 配置 | 写的时候就对 ✓ |
| ② 本地提交前 | pre-commit 钩子 / spotless:apply | 早发现 ✓ |
| ③ CI ★ | spotless:check / Checkstyle | 强制(没人能绕过)✓✓ |
⚠️ 只做① 是不够的:
"编辑器配置"依赖每个人装对 ✗ ——CI 是唯一"绝对生效"的地方 ✓
这与 《Git Flow 与团队协作》 的"分支保护要在平台层配"是同一个道理 ✓
直接开全部规范检查 → 几千个报错 → 团队崩溃 → 放弃 ✗✗
三种渐进式做法(按推荐顺序):
| 做法 | 怎么做 | 优点 |
|---|---|---|
| ① 只检查"改动的文件" ★★ | 用 git diff 找出本次改了哪些文件,只检查它们 ✓ | 立即可用,不欠债 ✓✓ |
| ② 先 warn 后 error ★ | 第一阶段只打印警告(不失败),修完再改成错误 | 平滑 ✓ |
| ③ 加"豁免清单" | 记录"暂时不修"的文件,逐步减少 | 可控 ⚠️ |
做法 ① 最推荐:
# 只检查本次改动的文件(示意)
git diff --name-only origin/main...HEAD | grep '\.java$' > changed.txt
# 然后用工具检查 changed.txt 里的文件理由:
"老代码不动,新代码必须合规" ——这样规范立刻生效,且不需要一次性的大改造 ✓
随着时间推移,被改动过的文件越来越多,整个代码库自然就合规了 ✓
这是"绞杀者模式"在代码治理上的应用(与 《JSP 与 JSTL(技术演进理解)》 讲老系统迁移时的思路一致)✓
答案:都要,但优先级不同 ✓
| 形式 | 内容 | 为什么 |
|---|---|---|
| 代码(检查规则配置)★ | 所有"机器能判定的" | 可执行、不会忘记、不会争论 ✓✓ |
文档(CONTRIBUTING.md) | "需要人判断的"原则 + 工具的使用说明 | 人看的 ✓ |
关键:
凡是能写成规则的,一定要写成规则 ——文档会过时、会被忽略,规则不会 ✓
文档只写"工具检查不了的东西"(如"这个项目的分层约定")✓
一个反面例子:
# 代码规范(纯文档版)
1. 缩进 4 空格
2. 类名大驼峰
3. 方法不超过 50 行
4. 不要有魔法数字
……✗ 这四条全都能被工具检查 ——写成文档等于"靠人自觉" ✗
呼应 《Git Flow 与团队协作》 的 5.6 节 ✓
Reviewer 的时间应该花在:
| 优先级 | 看什么 |
|---|---|
| 1 | 正确性(逻辑、边界、并发)✓ |
| 2 | 安全性(注入、越权、敏感信息)★ |
| 3 | 设计(抽象合理吗?有没有重复?)✓ |
一个可操作的建议:在 PR 模板里加一句:
- [ ] 本地已跑过格式化与静态检查(`mvn spotless:apply && mvn verify`)这样"风格问题"在 reviewer 看到之前就被解决了 ✓
见第 7 节(本项目的 CodeStyleChecker.java)。
<!-- 格式化:validate 阶段就检查,格式不对直接构建失败 -->
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<!-- 配置见 5.2 -->
</plugin>## Review 清单
- [ ] 缩进正确
- [ ] 命名符合规范
- [ ] import 顺序正确
- [ ] 没有未使用的 import✗ 这四条工具都能做 ——让 reviewer 查这些是对人力的浪费 ✗
改成:
## Review 清单
- [ ] 边界情况处理了吗?(null、空集合、0、最大值)
- [ ] 有并发问题吗?
- [ ] 用户输入被正确校验/转义了吗?
- [ ] 错误信息会不会泄漏内部结构?
- [ ] 出问题能回滚吗?✓ 这些才是工具检查不了的 ✓
文件:labs/engineering/CodeStyleChecker.java
核心逻辑(只有两个要点:逐行检查 + 退出码):
// 逐行检查五条规则
for (int index = 0; index < lines.length; index++) {
String line = lines[index];
if (line.length() > MAX_LINE_LENGTH) {
tooLongLines.add(relative + ":" + (index + 1) + "(" + line.length() + " 字符)");
}
if (line.startsWith("\t") || line.contains(" \t")) { tabIndents.add(...); }
if (!line.isBlank() && (line.endsWith(" ") || line.endsWith("\t"))) { trailingSpaces.add(...); }
if (line.contains("TODO") || line.contains("FIXME")) { todoMarks.add(...); }
}
// ★ 退出码决定 CI 的成功/失败
System.exit(checker.hasProblems() ? 1 : 0);运行命令:
cd <仓库根目录>
java "-Dstdout.encoding=UTF-8" labs/engineering/CodeStyleChecker.java labs/spring-boot-web/src
java "-Dstdout.encoding=UTF-8" labs/engineering/CodeStyleChecker.java labs/data-access/src预期输出(一):通过的模块
===== 代码规范检查 =====
扫描目录:labs\spring-boot-web\src
扫描文件:18 个 .java
[1] 单行超过 120 字符:0 处 ✓
[2] 使用 Tab 缩进:0 处 ✓
[3] 行尾多余空格:0 处 ✓
[4] TODO / FIXME(技术债):0 处
TODO 不算违规,但应该可见——它提醒你欠了多少债
[5] 文件末尾缺换行:0 处 ✓
===== 结果:通过 ✓ =====预期输出(二):不通过的模块
===== 代码规范检查 =====
扫描目录:labs\data-access\src
扫描文件:10 个 .java
[1] 单行超过 120 字符:6 处
JdbcLesson.java:105(126 字符)
JdbcLesson.java:114(129 字符)
JdbcLesson.java:181(122 字符)
JdbcLesson.java:202(127 字符)
JdbcLesson.java:232(122 字符)
……还有 1 处
[2] 使用 Tab 缩进:0 处 ✓
[3] 行尾多余空格:0 处 ✓
[4] TODO / FIXME(技术债):0 处
[5] 文件末尾缺换行:0 处 ✓
===== 结果:不通过(6 处需修) =====两处最值得注意的:
| 观察 | 说明 |
|---|---|
| 同一个检查器在两个模块上给出了不同的结果 ★ | 它真的在检查,而不是"永远通过"的样子货 ✓ |
| 报告里给出了"文件:行号" ★ | 可定位——这是"有用的检查工具"与"噪音"的分界线 ✓ |
⚠️ 一个诚实的补充:把这个检查器指向它自己(labs/engineering/CodeStyleChecker.java),退出码也是 1 ——工具自身也有超长行。
这恰好说明了两件事:
| 说明 | 意义 |
|---|---|
| 工具也要被检查 | 没有"免检"的代码 ✓ |
| 真实项目里"全绿"是努力的结果 | 不是默认状态 ✓ |
MAX_LINE_LENGTH 改成 40 —— 观察报错数量暴涨(理解"阈值的设定很重要")⚠️;> $null 2>&1; $LASTEXITCODE —— 理解"CI 靠什么判断成败" ★;--fix 选项(把行尾空格自动删掉)——体会"自动修复"为什么更受欢迎 ✓。见 5.6 节。几千个报错 = 等于没有检查 ✗
"我本地配了 Spotless" ✗ ——别人没有,CI 也不检查 → 等于没有规范 ✓
修:CI 里跑 spotless:check ✓
见 5.1、5.8、6.3 节。
@SuppressWarnings @SuppressWarnings("all") // ✗ 把整个类的检查都关了修:
| 做法 | 说明 |
|---|---|
| 精确到具体规则 | @SuppressWarnings("unchecked") ✓ |
| 精确到最小范围 | 加在方法/局部变量上,而不是类上 ✓ |
| 写注释说明为什么 | 否则下一个人不知道能不能删 ✓ |
⚠️ 更根本的问题:如果 CI 因为"不好修"而被绕过,说明规则本身可能不合适 ——应该讨论调整规则,而不是批量加抑制 ✓
症状:diff 里既有"真正的改动"又有"格式化" ✗ → review 看不清 ⚠️
修:格式化单独一个提交(并在提交信息里注明 style: 统一格式)✓
这样 review 时可以跳过那个提交 ✓
症状:"用我导出的 IDEA 配置" ✗ ——IDE 版本不同会失效,VS Code 用户用不了 ✗
修:用 .editorconfig + Spotless(与 IDE 无关)✓
目标:亲手写一个能接 CI 的检查,并理解"渐进式接入"。
步骤:
java ... labs/engineering/CodeStyleChecker.java,观察退出码 ✓;MAX_LINE_LENGTH 改成 60,观察报错数量的变化(体会"阈值要切合实际");System.out.println"(提示:检查行内容包含它)——体会"加一条规则有多容易";--fix 模式:自动删掉行尾空格(体会"自动修复"的价值)✓;java "-Dstdout.encoding=UTF-8" labs/engineering/CodeStyleChecker.java labs/data-access/src
if ($LASTEXITCODE -ne 0) { Write-Host "CI 失败:代码规范检查未通过" ; exit 1 }验收方式:能说出第 6 步"CI 靠什么判断"、第 7 步"修完如何验证"。
扩展任务:把检查接进 Maven:
exec-maven-plugin 在 validate 阶段运行这个检查器;基础题
0 表示 ______,非 0 表示 ______。代码阅读题
public class OrderService {
public Order getOrder(Long id){
Order o=orderMapper.selectById(id);
if(o!=null){
return o;
}else{
return null;
}
}
}编程题
System.out.println(应该用日志框架,见 《日志框架与日志规范》);catch (Exception e) {} 会吞掉异常,见 《参数校验与全局异常处理》);综合任务
提示 1(方向):第 1 题的方向是"能自动的别让人做"。
提示 2(关键点):第 4 题逐行看——空格、大括号、冗余分支。
提示 3(接近答案):第 7 题"只做一件事"——考虑"哪件事能让后续所有事都变容易"(提示:格式化自动修复)。
对照 5.1 的分类:
| 规范 | 谁 |
|---|---|
| 缩进、命名格式、import 顺序 | 工具 ✓ |
| 逻辑正确性、安全性、设计 | 人 ✓ |
理由(5.1 的那句话):
如果一条规则能被工具检查,却靠人来检查,就是在用最贵的资源做最便宜的事 ✓
一个现实的类比:
就像"编译器"接过了"语法检查" ——没人会在 review 里检查"少了个分号",因为编译器会告诉你 ✓
格式化/静态检查工具接过的,是"风格与常见坏味道"这一层 ✓
为什么这是关键:
CI 并不"理解"你的工具——它只看命令的退出码
0 → 这一步成功 → 继续
非0 → 这一步失败 → 流水线中止 ✗所以:任何要接进 CI 的检查工具,都必须"有问题时返回非 0" ✓ ——这正是本节的检查器用 System.exit(1) 的原因 ✓
⚠️ 一个常见的坑:
// ✗ 只打印错误,但不改退出码 → CI 会认为成功
if (hasProblems()) {
System.out.println("有问题!");
}
// 程序正常结束 → 退出码 0 → CI 绿 ✗✗这个 bug 很隐蔽:日志里能看到"有问题",但流水线是绿的 ⚠️
三种做法的对比(5.6):
| 做法 | 存量债 | 新代码 | 可用性 |
|---|---|---|---|
| 全量检查 | 要一次修完 ✗✗ | ✓ | 不可用(几千个报错)✗ |
| 只检查改动的文件 ★ | 不动 | ✓ | 立即可用 ✓✓ |
| 先 warn 后 error | 逐步修 | ✓ | 平滑 ✓ |
"只检查改动文件"为什么最优:
| 理由 | 说明 |
|---|---|
| 零门槛 | 今天就能配上,不需要先做一次大改造 ✓ |
| 不欠新债 ★ | 所有新代码都合规 ✓ |
| 债务自然减少 | 文件被改一次就合规一次 → 时间会替你清理 ✓ |
| 心理负担小 | 没人需要面对"我引入了 3000 个报错" ✗ |
这就是"绞杀者模式"(《JSP 与 JSTL(技术演进理解)》):不一次性替换,而是让新的逐步蚕食旧的 ✓
第 4 题:机器能判定的问题(至少 6 处)
| # | 问题 | 工具 |
|---|---|---|
| 1 | getOrder(Long id){ 缺空格() 与 { 之间) | 格式化工具 ✓ |
| 2 | Order o=orderMapper 缺空格(= 两边) | 格式化 ✓ |
| 3 | if(o!=null){ 缺空格(if 与 ( 之间) | 格式化 ✓ |
| 4 | 缩进只有 4 空格但层级混乱(方法体应该是 8) | 格式化 ✓ |
| 5 | "if 返回 else 返回" 可简化 | PMD / IDE 检查 ✓ |
| 6 | 方法没有注释/Javadoc | Checkstyle(可配)✓ |
| 7 | Order o 命名过短 | Checkstyle(可配命名规则)✓ |
格式化后的样子:
public Order getOrder(Long id) {
Order o = orderMapper.selectById(id);
return o; // ← 化简:o 为 null 时本来就返回 null
}注意第 5 条:if (o != null) return o; else return null; 完全等价于 return o; ✓ ——这类"冗余分支"PMD 或 IDE 会直接提示 ✓
第 5 题:需要人判断的问题(工具查不出)
| # | 问题 | 为什么工具查不出 |
|---|---|---|
| 1 | id 为 null 怎么办? | "是否允许 null"是业务约定,工具不知道 ✗ |
| 2 | 查不到订单时返回 null,还是应该抛异常? ★ | 这是 API 设计的取舍(《参数校验与全局异常处理》 讨论过:抛异常能被框架统一翻译成 404)✓ |
| 3 | 返回 null 会不会导致调用方 NPE? | 需要看调用链 ⚠️(SpotBugs 能发现一部分,但不能全靠它) |
| 4 | Order 是可变对象,直接返回内部状态安全吗? | 设计问题 ✗ |
| 5 | 这个方法需要幂等/线程安全吗? | 看上下文 ✗ |
| 6 | Order o 这个命名在业务上合适吗? | "o" 工具能查(太短),但"该叫 order 还是 orderEntity"要人定 ✗ |
第 2 条最重要(这是本题的核心):
"查不到时返回 null 还是抛异常"是一个 API 设计决策 ——工具无法替你决定 ✓
回顾 《参数校验与全局异常处理》 的 5.5 节:我们选择了"抛
ResourceNotFoundException",由全局处理器翻译成 404 ✓这样 Controller 里不用判空 ——而这个"不用判空"的好处,只有人能设计出来 ✓
这正好说明了 5.1 的分类为何重要:
工具能保证"这段代码格式正确、没有空 catch",但"它该返回 null 还是抛异常"只能由人来决定。 ✓
// 在 checkFile 的循环里加三条规则(示意:给出核心判定逻辑与收集方式)
// 规则 A:禁止 System.out.println(应该用日志框架)
if (line.contains("System.out.print") && !line.trim().startsWith("//")) {
systemOutCalls.add(relative + ":" + lineNumber + " " + line.trim());
}
// 规则 B:禁止空 catch 块(检测 "catch (...) {" 之后紧跟 "}")
// 简化实现:单行 `catch (Exception e) {}` 或 `catch (...) { }`
if (line.matches(".*catch\\s*\\([^)]*\\)\\s*\\{\\s*}.*")
|| line.matches(".*catch\\s*\\([^)]*\\)\\s*\\{\\s*$")) {
emptyCatchBlocks.add(relative + ":" + lineNumber + " " + line.trim());
}
// 规则 C:类缺少注释(检查文件前 5 行里有没有 /** 或 //)
// 在 checkFile 开头判断
boolean hasHeaderComment = content.lines().limit(5)
.anyMatch(l -> l.trim().startsWith("/**") || l.trim().startsWith("//"));
if (!hasHeaderComment) {
missingClassDoc.add(relative);
}为什么这三条值得检查
System.out.println | 问题 | 说明 |
|---|---|
| 绕过日志框架 ✗ | 不受级别控制(生产环境也在打) |
| 没有时间戳、线程、traceId | 排查时几乎无用(《日志框架与日志规范》)✗ |
| 输出到 stdout | 容器化部署时可能丢失或混在一起 ⚠️ |
| 性能 | println 是同步 IO,在高频路径上会拖慢服务 ✗ |
"不检查会怎样":生产环境日志里混着一堆没有上下文的输出,既干扰排查又浪费资源 ⚠️
注意:这条规则应该"允许例外"——示例/教学代码(如本项目的 *Lesson.java)里用 println 是合理的 ✓ ——所以要能配置豁免路径 ✓
这是本组里最值得检查的一条 ✓
| 问题 | 说明 |
|---|---|
| 吞掉异常 ✗✗ | 错误发生了,但没人知道 ——这是最危险的一类代码 |
| 排查时毫无线索 | "为什么数据不对?"——因为某个异常被吞了,但没有任何记录 ✗✗ |
为什么"必须靠工具":
| 理由 | 说明 |
|---|---|
| 人眼极易漏过 ★ | catch (Exception e) {} 只有十几个字符,在 diff 里完全不显眼 ✗ |
| 写它的人往往"只是想让编译通过" | 不是有意为之,所以自己也不会复查 ⚠️ |
| 它的后果延迟暴露 | 可能几个月后才被发现 ✗ |
对比:"缩进错了"人眼一眼能看到;"空 catch"人眼基本看不到 ✓ ——这正是工具的价值所在 ✓
修复方式(至少做一条):
catch (Exception e) {
log.warn("解析配置失败,使用默认值 config={}", configName, e); // ✓ 记日志
}
// 或(确实要忽略时,写清楚为什么)
catch (InterruptedException e) {
Thread.currentThread().interrupt(); // ★ 至少恢复中断标志
// 业务上确实可以忽略:这里是关闭流程,中断无需特殊处理
}⚠️ 一个特别提醒:"吞掉 InterruptedException 而不恢复中断标志"是一个经典 bug ✓ ——这类"看起来无害"的代码,正是静态检查工具最能发挥作用的地方 ✓
| 价值 | 说明 |
|---|---|
| 促使你回答"这个类干什么用" ★ | 写不出来 = 职责不清 ✓ |
| 新人的入口 | 打开一个类,第一眼知道它的职责 ✓ |
⚠️ 但要克制:
"必须有 Javadoc"这条规则很容易执行过度 ✗
好的类注释回答"为什么存在、怎么用";"getUser 获取用户"这种注释毫无价值,还不如不写 ✗
建议:只对"公共 API / 复杂类"要求注释,简单 DTO、getter/setter 不要强制 ✓
一句话:
注释的价值在于"解释代码看不出来的东西"(为什么、怎么用、有什么坑)
复述代码的注释是噪音 ✗
四层防线
| 层 | 做什么 | 工具 | 何时反馈 |
|---|---|---|---|
| ① 编辑器 | 缩进、换行符、去行尾空格 | .editorconfig + IDE 配置 | 打字时 ✓✓ |
| ② 本地(保存/提交前) | 格式化 + 静态检查 | spotless:apply、pre-commit 钩子 | 提交前 ✓ |
| ③ CI ★ | 强制检查(不通过不能合并) | spotless:check、Checkstyle、SpotBugs | 推送后 ✓ |
| ④ Review ★ | 逻辑、安全、设计 | 人 | 合并前 ✓ |
每层的定位:
越靠前,反馈越快、成本越低 ✓ 但只有 CI 是"强制"的(其他都依赖人主动)✓✓
渐进式接入路线(四个阶段)
| 阶段 | 目标 | 做什么 |
|---|---|---|
| 第 1 周 | 止血 | 加 .editorconfig;格式化工具配好(能自动修复);CI 里跑 spotless:check,但先设成"不阻断"(只报告)✓ |
| 第 2~4 周 | 新代码合规 | 改成"只检查改动的文件";修掉新引入的问题 ✓ |
| 第 2~3 月 | 扩大规则 | 加 PMD/SpotBugs 的高价值规则(空 catch、资源泄漏);把"易修的存量问题"批量修一次(用 spotless:apply 全量跑一次,单独提交)✓ |
| 长期 | 持续收敛 | 定期回顾规则(关掉误报多的);把"反复出现的问题"加进规则 ✓ |
关键:第 1 阶段"先不阻断"很重要 ——让团队先看到"我们现在有多少问题",而不是直接被 CI 卡住 ✓
如何避免"工具变成负担"
| 做法 | 说明 |
|---|---|
| 规则集要小 ★★ | 只开"高价值 + 低误报"的规则 ——10 条有效规则 > 200 条吵闹的规则 ✓ |
| 必须有自动修复 ★ | 格式化类问题用 spotless:apply 自动改,不让人手工改 ✗ |
| 误报要能快速关闭 | 提供 @SuppressWarnings + 注释的规范用法 ✓ |
| 错误提示要可定位 | 给出"文件:行号 + 为什么 + 怎么改" ✓ |
| CI 失败要"看得懂" | 输出要能直接看到"哪一行、什么问题" ⚠️(否则没人会去查日志) |
| 定期修剪规则 ★ | 一个季度回顾一次:关掉"从没抓到真问题"的规则 ✓ |
度量方式(怎么知道在变好)
| 指标 | 怎么测 | 目标 |
|---|---|---|
| 检查通过率 | CI 里规范检查的失败次数 / 总构建次数 | 上升 ✓ |
| 存量违规数 | 定期全量跑一次,记录总数 | 下降 ✓ |
| "自动修复"的变更比例 | 格式化提交占所有提交的比例 | 稳定(说明大家在写之前就格式化了)✓ |
| Review 中"风格类评论"的数量 ★ | 人工统计或看 PR 评论关键词 | 趋近 0 ✓✓ |
| Review 中"逻辑/安全类评论"的数量 ★ | 同上 | 上升(说明时间用对了地方)✓✓ |
| 缺陷密度 | 线上 bug 数 / 代码行数 | 下降 ✓ |
后两个指标最有意思:
如果"风格类评论"下降、同时"逻辑/安全类评论"上升 ——说明工具成功地把 review 时间"挤"到了正确的地方 ✓✓
只做一件事,做哪件
答:配好"能自动修复的格式化工具"并接进 CI ✓
理由:
| 理由 | 说明 |
|---|---|
| 成本最低 | 加一个插件 + 一条命令 ✓ |
| 收益最直接 | 消灭 diff 噪音(这是 review 的最大干扰)✓ |
| 零争议 ★ | "缩进几个空格"这种事没有对错,用工具定一个就行 ——不需要团队达成共识 ✓ |
| 为后续铺路 ★★ | 格式统一之后,加其他静态检查才不会被"格式噪音"淹没 ✓ |
最后一条最关键:
格式化是"地基" ——先把大家的格式统一,之后新增的任何检查都能聚焦在"真正的问题"上 ✓
如果先加了 PMD 而没统一格式,报告里会混着"缩进问题"与"空 catch",后者会被淹没 ✗
对照 T1-018 与 《Git Flow 与团队协作》:共同的设计哲学
| T1-018 覆盖率门禁 | 《Git Flow 与团队协作》 分支保护 | 本节 规范检查 | |
|---|---|---|---|
| 形式 | 覆盖率低于阈值 → 构建失败 | 直接 push main → 被拒绝 | 规范违规 → 构建失败 |
| 执行者 | CI | 平台 | CI |
| 依赖人吗 | ❌ | ❌ | ❌ |
| 漏了会怎样 | 不合格的代码被合并 ✗ | 有人绕过 review ✗ | 不合规范的代码入库 ✗ |
共同原则(这是本知识库反复出现的主题):
① 把约定变成机制 ——"请大家注意"是不可靠的,"CI 失败"是可靠的 ✓
② 默认拒绝,显式放行 ——不配置就是"不检查"(等于允许);配了就是"必须过" ✓
③ 反馈要早 ——编辑器 > 本地 > CI,越早越便宜 ✓
④ 让人做机器做不了的事 ——机器判格式,人判逻辑 ✓
这三处(覆盖率、分支保护、规范检查)本质上都是同一件事:
把"应该做的事"从"人的自律"转移到"系统的约束"上。 ✓
因为人的注意力是稀缺且不可靠的,而系统不是。 ✓
这也是 《Spring Boot 配置管理与多环境》 的"默认安全配置"、《Git 分支、合并与回退》 的"--force-with-lease"、《JSP 与 JSTL(技术演进理解)》 的"默认转义"共享的同一条思路 ✓
apply 与 check 分别在什么时候用;.editorconfig 能解决"换行符之争";工程工具四节(《Git 分支、合并与回退》 / 《Git Flow 与团队协作》 / 《Maven 进阶:依赖管理与多模块》 / 《代码规范与自动校验》)到此完成。接下来可以:
无,可直接开始
暂无