Skip to content

代码规范与自动校验 ​

S3-032选修入门约 45 分钟JDK 21企业开发基础 · 工程工具与规范

1. 本节定位 ​

这是一节"投入产出比"极高的课:学一次,团队省下无数次争论。

它要解决的核心问题:

text
Code Review 里,两个人花了 20 分钟争论"这里该不该换行"
        ↓
而真正的 bug(空指针没判)就在旁边,没人看

本节的答案:把"风格"交给工具,把"人"留给逻辑与安全 ✓

2. 学习目标 ​

完成本节后,你应该能够:

  • 把规范分成两类(机器能判定的 / 需要人判断的),并知道各自该由谁负责;
  • 说出 Java 生态里常用的四类工具(格式化 / 静态检查 / 编辑器配置 / 构建集成);
  • 说出"退出码"为什么是接进 CI 的关键;
  • 说出老项目渐进式接入规范检查的三种做法;
  • 说出"规范应该写成代码还是文档"以及理由;
  • 能写一个最小的规范检查器并接进 CI。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看三个真实的成本:

成本说明
Review 时间被占用 ⚠️讨论缩进、命名、import 顺序 → 侵蚀了本该看逻辑的时间 ✗
diff 噪音 ✗有人改了一行,但因为格式化不同,diff 显示改了 50 行 → review 看不出真正改了什么 ✗✗
新人不知道规矩 ⚠️规范只存在于"老同事的脑子里" → 新人靠被吐槽来学习 ✗

第二条尤其致命:

"格式化不一致"会让 git diff 失去意义 ——而 code review 的核心工具就是 diff ✓

工具化能同时解决这三条 ✓

5. 核心概念 ​

5.1 规范分两类 ​

这是本节最重要的分类 ✓

类别例子谁负责
① 机器能判定的 ★缩进、行长、import 顺序、命名格式、未使用的变量、魔法数字工具(自动检查 + 自动修复)✓
② 需要人判断的 ★这个抽象合理吗?边界情况处理了吗?有没有安全风险?Code Review ✓

判断标准:

"这条规范,能不能写成一个确定性的规则(if/else 就能判断)?"

  • 能 → 交给工具 ✓
  • 不能 → 交给人 ✓

举例:

规范类别理由
"缩进用 4 个空格"① 机器可以精确判定 ✓
"类名用大驼峰"① 机器可以(正则)✓
"方法不超过 50 行"① 机器可以(计数)✓
"变量命名要有意义"② 人"有意义"无法精确判定 ✗
"这个设计合理吗"② 人—
"这个 SQL 会不会慢"② 人(或压测)—

⚠️ 一个常见的误用:把"机器能判定的"写进 review 清单 ✗ ——这是对人力的浪费 ✓

如果一条规则能被工具检查,却靠人来检查,那就是在用最贵的资源做最便宜的事 ✓

5.2 格式化工具:自动修复 ​

代表:Spotless(推荐)、google-java-format、IDE 自带格式化

Spotless 的配置(Maven):

xml
<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 与团队协作》 的"让正确的做法成为最容易的做法"是同一条原则 ✓

5.3 静态检查工具:找出可疑代码 ​

四类工具的分工(记住这个,选型不迷茫):

工具查什么典型问题
Checkstyle代码风格命名、缩进、行长、import 顺序 ✓
PMD可疑的代码写法空 catch 块、重复代码、过于复杂的表达式 ✓
SpotBugs字节码层面的 bug ★空指针风险、错误的 equals、资源未关闭 ✓
ErrorProne编译期就报错 ★★常见编码错误(在 javac 时就拦住)✓

各自的定位:

阶段误报率价值
Checkstyle源码文本低统一风格 ✓
PMD源码 AST中 ⚠️找出坏味道
SpotBugs字节码 ★中能发现"人看不出"的问题 ✓
ErrorProne编译期 ★★低反馈最快 ✓

实践建议:

建议说明
从少量规则开始 ★一上来开全部规则 → 几百个报错 → 没人愿意修 ✗
优先开"高价值"规则空指针、资源泄漏、错误的 equals ✓
误报多的规则用 @SuppressWarnings 局部关闭并写注释说明 ✓

⚠️ 一个重要的判断:

静态检查工具的目标是"发现问题",不是"消灭所有警告" ✓

如果一个规则产生的误报多于真问题,就关掉它 ——否则大家会开始忽略警告,工具就失效了 ✗✗

5.4 EditorConfig:编辑器层面统一 ​

.editorconfig(放在仓库根目录):

ini
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 能彻底解决 ✓

5.5 接进 CI:退出码是关键 ​

这是"自动校验"能真正生效的关键一环 ✓

原理:

text
工具运行完 → 退出码 0(通过)/ 非 0(有问题)
        ↓
CI 看到非 0 → 流水线失败 → 不允许合并 ✓

本节的检查器就是这么设计的(System.exit(hasProblems() ? 1 : 0))✓

实测(本项目):

powershell
# 通过的情况
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 与团队协作》 的"分支保护要在平台层配"是同一个道理 ✓

5.6 老项目怎么接入 ​

直接开全部规范检查 → 几千个报错 → 团队崩溃 → 放弃 ✗✗

三种渐进式做法(按推荐顺序):

做法怎么做优点
① 只检查"改动的文件" ★★用 git diff 找出本次改了哪些文件,只检查它们 ✓立即可用,不欠债 ✓✓
② 先 warn 后 error ★第一阶段只打印警告(不失败),修完再改成错误平滑 ✓
③ 加"豁免清单"记录"暂时不修"的文件,逐步减少可控 ⚠️

做法 ① 最推荐:

bash
# 只检查本次改动的文件(示意)
git diff --name-only origin/main...HEAD | grep '\.java$' > changed.txt
# 然后用工具检查 changed.txt 里的文件

理由:

"老代码不动,新代码必须合规" ——这样规范立刻生效,且不需要一次性的大改造 ✓

随着时间推移,被改动过的文件越来越多,整个代码库自然就合规了 ✓

这是"绞杀者模式"在代码治理上的应用(与 《JSP 与 JSTL(技术演进理解)》 讲老系统迁移时的思路一致)✓

5.7 规范该写成代码还是文档 ​

答案:都要,但优先级不同 ✓

形式内容为什么
代码(检查规则配置)★所有"机器能判定的"可执行、不会忘记、不会争论 ✓✓
文档(CONTRIBUTING.md)"需要人判断的"原则 + 工具的使用说明人看的 ✓

关键:

凡是能写成规则的,一定要写成规则 ——文档会过时、会被忽略,规则不会 ✓

文档只写"工具检查不了的东西"(如"这个项目的分层约定")✓

一个反面例子:

markdown
# 代码规范(纯文档版)
1. 缩进 4 空格
2. 类名大驼峰
3. 方法不超过 50 行
4. 不要有魔法数字
……

✗ 这四条全都能被工具检查 ——写成文档等于"靠人自觉" ✗

5.8 与 Code Review 的分工 ​

呼应 《Git Flow 与团队协作》 的 5.6 节 ✓

Reviewer 的时间应该花在:

优先级看什么
1正确性(逻辑、边界、并发)✓
2安全性(注入、越权、敏感信息)★
3设计(抽象合理吗?有没有重复?)✓
4风格 → 交给工具 ✓

一个可操作的建议:在 PR 模板里加一句:

markdown
- [ ] 本地已跑过格式化与静态检查(`mvn spotless:apply && mvn verify`)

这样"风格问题"在 reviewer 看到之前就被解决了 ✓

6. 递进示例 ​

6.1 最小示例:一个能接 CI 的检查器 ​

见第 7 节(本项目的 CodeStyleChecker.java)。

6.2 业务示例:格式化 + 静态检查 ​

xml
<!-- 格式化:validate 阶段就检查,格式不对直接构建失败 -->
<plugin>
  <groupId>com.diffplug.spotless</groupId>
  <artifactId>spotless-maven-plugin</artifactId>
  <!-- 配置见 5.2 -->
</plugin>

6.3 反例:把风格规则写进 review 清单 ​

markdown
## Review 清单
- [ ] 缩进正确
- [ ] 命名符合规范
- [ ] import 顺序正确
- [ ] 没有未使用的 import

✗ 这四条工具都能做 ——让 reviewer 查这些是对人力的浪费 ✗

改成:

markdown
## Review 清单
- [ ] 边界情况处理了吗?(null、空集合、0、最大值)
- [ ] 有并发问题吗?
- [ ] 用户输入被正确校验/转义了吗?
- [ ] 错误信息会不会泄漏内部结构?
- [ ] 出问题能回滚吗?

✓ 这些才是工具检查不了的 ✓

7. 完整可运行代码 ​

文件:labs/engineering/CodeStyleChecker.java

核心逻辑(只有两个要点:逐行检查 + 退出码):

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);

运行命令:

powershell
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

预期输出(一):通过的模块

text
===== 代码规范检查 =====
扫描目录:labs\spring-boot-web\src
扫描文件:18 个 .java

[1] 单行超过 120 字符:0 处  ✓

[2] 使用 Tab 缩进:0 处  ✓

[3] 行尾多余空格:0 处  ✓

[4] TODO / FIXME(技术债):0 处
    TODO 不算违规,但应该可见——它提醒你欠了多少债

[5] 文件末尾缺换行:0 处  ✓

===== 结果:通过 ✓ =====

预期输出(二):不通过的模块

text
===== 代码规范检查 =====
扫描目录: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 ——工具自身也有超长行。

这恰好说明了两件事:

说明意义
工具也要被检查没有"免检"的代码 ✓
真实项目里"全绿"是努力的结果不是默认状态 ✓

8. Debug 观察点 ​

  1. 故意在一行末尾加个空格,再跑检查器——观察它能被第 [3] 项抓到 ✓;
  2. 把 MAX_LINE_LENGTH 改成 40 —— 观察报错数量暴涨(理解"阈值的设定很重要")⚠️;
  3. 观察退出码:> $null 2>&1; $LASTEXITCODE —— 理解"CI 靠什么判断成败" ★;
  4. 在故意违规的文件上跑:体会"报告要给出文件名与行号"有多重要 ✓;
  5. 对比"只报错"与"能自动修复":给检查器加一个 --fix 选项(把行尾空格自动删掉)——体会"自动修复"为什么更受欢迎 ✓。

9. 常见错误 ​

错误 1:一上来就开启全部规则 ​

见 5.6 节。几千个报错 = 等于没有检查 ✗

错误 2:只配置工具,不接 CI ​

"我本地配了 Spotless" ✗ ——别人没有,CI 也不检查 → 等于没有规范 ✓

修:CI 里跑 spotless:check ✓

错误 3:把"机器能判定"的规则留给 review ​

见 5.1、5.8、6.3 节。

错误 4:为了"让 CI 变绿"而滥用 @SuppressWarnings ​

java
@SuppressWarnings("all")        // ✗ 把整个类的检查都关了

修:

做法说明
精确到具体规则@SuppressWarnings("unchecked") ✓
精确到最小范围加在方法/局部变量上,而不是类上 ✓
写注释说明为什么否则下一个人不知道能不能删 ✓

⚠️ 更根本的问题:如果 CI 因为"不好修"而被绕过,说明规则本身可能不合适 ——应该讨论调整规则,而不是批量加抑制 ✓

错误 5:格式化与手工修改混在一次提交里 ​

症状:diff 里既有"真正的改动"又有"格式化" ✗ → review 看不清 ⚠️

修:格式化单独一个提交(并在提交信息里注明 style: 统一格式)✓

这样 review 时可以跳过那个提交 ✓

错误 6:规范没有版本化(靠 IDE 导出配置) ​

症状:"用我导出的 IDEA 配置" ✗ ——IDE 版本不同会失效,VS Code 用户用不了 ✗

修:用 .editorconfig + Spotless(与 IDE 无关)✓

10. 动手实验 ​

目标:亲手写一个能接 CI 的检查,并理解"渐进式接入"。

步骤:

  1. 运行第 7 节的两条命令,对比两个模块的结果;
  2. 把检查器指向它自己:java ... labs/engineering/CodeStyleChecker.java,观察退出码 ✓;
  3. 改阈值:把 MAX_LINE_LENGTH 改成 60,观察报错数量的变化(体会"阈值要切合实际");
  4. 给检查器加一条规则:例如"禁止 System.out.println"(提示:检查行内容包含它)——体会"加一条规则有多容易";
  5. 加一个 --fix 模式:自动删掉行尾空格(体会"自动修复"的价值)✓;
  6. 模拟 CI:
    powershell
    java "-Dstdout.encoding=UTF-8" labs/engineering/CodeStyleChecker.java labs/data-access/src
    if ($LASTEXITCODE -ne 0) { Write-Host "CI 失败:代码规范检查未通过" ; exit 1 }
  7. 修掉那 6 处超长行(手工换行),再跑一次直到变绿 ✓。

验收方式:能说出第 6 步"CI 靠什么判断"、第 7 步"修完如何验证"。

扩展任务:把检查接进 Maven:

  • 用 exec-maven-plugin 在 validate 阶段运行这个检查器;
  • 要求:不通过时构建失败 ✓;
  • 说明:为什么"接进构建"比"写个脚本让大家手动跑"更可靠?
  • 思考:如果团队用的是 Gradle(《Gradle 概览》),同样的检查要怎么接?

11. 练习题 ​

基础题

  1. 判断:代码规范都应该写进 Code Review 清单,由 reviewer 检查。(对 / 错)
  2. 填空:接进 CI 的关键是工具的 ______;0 表示 ______,非 0 表示 ______。
  3. 简答:为什么老项目接入规范检查要用"只检查改动的文件"这种方式?

代码阅读题

java
public class OrderService {
    public Order getOrder(Long id){
        Order o=orderMapper.selectById(id);
        if(o!=null){
            return o;
        }else{
            return null;
        }
    }
}
  1. 这段代码有哪些"机器能判定"的规范问题?列出来。
  2. 有哪些是"工具查不出、需要人判断"的问题?

编程题

  1. 给检查器加三条规则:
    • 规则 A:禁止 System.out.println(应该用日志框架,见 《日志框架与日志规范》);
    • 规则 B:禁止空的 catch 块(catch (Exception e) {} 会吞掉异常,见 《参数校验与全局异常处理》);
    • 规则 C:类必须有 Javadoc 或至少一行注释(提示:检查文件前几行);
    • 要求:每条都要能定位到文件与行号,并说明为什么这条值得检查(不检查会怎样);
    • 回答:为什么"空 catch 块"这种问题必须靠工具?(提示:人眼很容易漏过它)

综合任务

  1. 为一个团队设计"代码质量保障体系":
    • 背景:8 人团队,代码库 12 万行(Java),之前没有规范检查,现在想建立;
    • 要求:
      • 设计四层防线(编辑器 / 本地 / CI / Review),每层做什么、用什么工具;
      • 设计渐进式接入路线(分几个阶段?每个阶段的目标是什么?);
      • 说明如何避免"工具变成负担"(提示:规则集要小、要有自动修复、误报要能快速关闭);
      • 设计度量方式(怎么知道"代码质量在变好"?);
      • 说明:如果只能做一件事,做哪件?为什么?
    • 额外要求:把这套设计对照 T1-018 的"质量门禁"与 《Git Flow 与团队协作》 的"分支保护",说明它们的共同设计哲学。
分级提示

提示 1(方向):第 1 题的方向是"能自动的别让人做"。

提示 2(关键点):第 4 题逐行看——空格、大括号、冗余分支。

提示 3(接近答案):第 7 题"只做一件事"——考虑"哪件事能让后续所有事都变容易"(提示:格式化自动修复)。

12. 参考答案 ​

基础题
  1. 错。"机器能判定的"应该交给工具,reviewer 只管"需要人判断的" ✓

对照 5.1 的分类:

规范谁
缩进、命名格式、import 顺序工具 ✓
逻辑正确性、安全性、设计人 ✓

理由(5.1 的那句话):

如果一条规则能被工具检查,却靠人来检查,就是在用最贵的资源做最便宜的事 ✓

一个现实的类比:

就像"编译器"接过了"语法检查" ——没人会在 review 里检查"少了个分号",因为编译器会告诉你 ✓

格式化/静态检查工具接过的,是"风格与常见坏味道"这一层 ✓

  1. 退出码(exit code);通过;有问题(失败) ✓

为什么这是关键:

text
CI 并不"理解"你的工具——它只看命令的退出码
  0   → 这一步成功 → 继续
  非0 → 这一步失败 → 流水线中止 ✗

所以:任何要接进 CI 的检查工具,都必须"有问题时返回非 0" ✓ ——这正是本节的检查器用 System.exit(1) 的原因 ✓

⚠️ 一个常见的坑:

java
// ✗ 只打印错误,但不改退出码 → CI 会认为成功
if (hasProblems()) {
    System.out.println("有问题!");
}
// 程序正常结束 → 退出码 0 → CI 绿 ✗✗

这个 bug 很隐蔽:日志里能看到"有问题",但流水线是绿的 ⚠️

  1. 因为"一次性修完"不现实,而"永远不修"又等于没有规范 ✓

三种做法的对比(5.6):

做法存量债新代码可用性
全量检查要一次修完 ✗✗✓不可用(几千个报错)✗
只检查改动的文件 ★不动✓立即可用 ✓✓
先 warn 后 error逐步修✓平滑 ✓

"只检查改动文件"为什么最优:

理由说明
零门槛今天就能配上,不需要先做一次大改造 ✓
不欠新债 ★所有新代码都合规 ✓
债务自然减少文件被改一次就合规一次 → 时间会替你清理 ✓
心理负担小没人需要面对"我引入了 3000 个报错" ✗

这就是"绞杀者模式"(《JSP 与 JSTL(技术演进理解)》):不一次性替换,而是让新的逐步蚕食旧的 ✓

代码阅读题(第 4、5 题)

第 4 题:机器能判定的问题(至少 6 处)

#问题工具
1getOrder(Long id){ 缺空格() 与 { 之间)格式化工具 ✓
2Order o=orderMapper 缺空格(= 两边)格式化 ✓
3if(o!=null){ 缺空格(if 与 ( 之间)格式化 ✓
4缩进只有 4 空格但层级混乱(方法体应该是 8)格式化 ✓
5"if 返回 else 返回" 可简化PMD / IDE 检查 ✓
6方法没有注释/JavadocCheckstyle(可配)✓
7Order o 命名过短Checkstyle(可配命名规则)✓

格式化后的样子:

java
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 题:需要人判断的问题(工具查不出)

#问题为什么工具查不出
1id 为 null 怎么办?"是否允许 null"是业务约定,工具不知道 ✗
2查不到订单时返回 null,还是应该抛异常? ★这是 API 设计的取舍(《参数校验与全局异常处理》 讨论过:抛异常能被框架统一翻译成 404)✓
3返回 null 会不会导致调用方 NPE?需要看调用链 ⚠️(SpotBugs 能发现一部分,但不能全靠它)
4Order 是可变对象,直接返回内部状态安全吗?设计问题 ✗
5这个方法需要幂等/线程安全吗?看上下文 ✗
6Order o 这个命名在业务上合适吗?"o" 工具能查(太短),但"该叫 order 还是 orderEntity"要人定 ✗

第 2 条最重要(这是本题的核心):

"查不到时返回 null 还是抛异常"是一个 API 设计决策 ——工具无法替你决定 ✓

回顾 《参数校验与全局异常处理》 的 5.5 节:我们选择了"抛 ResourceNotFoundException",由全局处理器翻译成 404 ✓

这样 Controller 里不用判空 ——而这个"不用判空"的好处,只有人能设计出来 ✓

这正好说明了 5.1 的分类为何重要:

工具能保证"这段代码格式正确、没有空 catch",但"它该返回 null 还是抛异常"只能由人来决定。 ✓

编程题(第 6 题)
java
// 在 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);
}

为什么这三条值得检查

规则 A:System.out.println ​

问题说明
绕过日志框架 ✗不受级别控制(生产环境也在打)
没有时间戳、线程、traceId排查时几乎无用(《日志框架与日志规范》)✗
输出到 stdout容器化部署时可能丢失或混在一起 ⚠️
性能println 是同步 IO,在高频路径上会拖慢服务 ✗

"不检查会怎样":生产环境日志里混着一堆没有上下文的输出,既干扰排查又浪费资源 ⚠️

注意:这条规则应该"允许例外"——示例/教学代码(如本项目的 *Lesson.java)里用 println 是合理的 ✓ ——所以要能配置豁免路径 ✓

规则 B:空 catch 块 ★ ​

这是本组里最值得检查的一条 ✓

问题说明
吞掉异常 ✗✗错误发生了,但没人知道 ——这是最危险的一类代码
排查时毫无线索"为什么数据不对?"——因为某个异常被吞了,但没有任何记录 ✗✗

为什么"必须靠工具":

理由说明
人眼极易漏过 ★catch (Exception e) {} 只有十几个字符,在 diff 里完全不显眼 ✗
写它的人往往"只是想让编译通过"不是有意为之,所以自己也不会复查 ⚠️
它的后果延迟暴露可能几个月后才被发现 ✗

对比:"缩进错了"人眼一眼能看到;"空 catch"人眼基本看不到 ✓ ——这正是工具的价值所在 ✓

修复方式(至少做一条):

java
catch (Exception e) {
    log.warn("解析配置失败,使用默认值 config={}", configName, e);      // ✓ 记日志
}

// 或(确实要忽略时,写清楚为什么)
catch (InterruptedException e) {
    Thread.currentThread().interrupt();        // ★ 至少恢复中断标志
    // 业务上确实可以忽略:这里是关闭流程,中断无需特殊处理
}

⚠️ 一个特别提醒:"吞掉 InterruptedException 而不恢复中断标志"是一个经典 bug ✓ ——这类"看起来无害"的代码,正是静态检查工具最能发挥作用的地方 ✓

规则 C:类缺少注释 ​

价值说明
促使你回答"这个类干什么用" ★写不出来 = 职责不清 ✓
新人的入口打开一个类,第一眼知道它的职责 ✓

⚠️ 但要克制:

"必须有 Javadoc"这条规则很容易执行过度 ✗

好的类注释回答"为什么存在、怎么用";"getUser 获取用户"这种注释毫无价值,还不如不写 ✗

建议:只对"公共 API / 复杂类"要求注释,简单 DTO、getter/setter 不要强制 ✓

一句话:

注释的价值在于"解释代码看不出来的东西"(为什么、怎么用、有什么坑)

复述代码的注释是噪音 ✗

综合任务(第 7 题)

四层防线

层做什么工具何时反馈
① 编辑器缩进、换行符、去行尾空格.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(技术演进理解)》 的"默认转义"共享的同一条思路 ✓

13. 掌握标准 ​

  • [ ] 能把规范分成"机器能判定"与"需要人判断"两类;
  • [ ] 知道四类 Java 质量工具的定位(Checkstyle / PMD / SpotBugs / ErrorProne);
  • [ ] 知道 Spotless 的 apply 与 check 分别在什么时候用;
  • [ ] 知道 .editorconfig 能解决"换行符之争";
  • [ ] 知道"退出码"是接进 CI 的关键,且工具必须"有问题时返回非 0";
  • [ ] 能说出老项目渐进式接入的三种做法(推荐"只检查改动的文件");
  • [ ] 知道规范应该"能写成规则的就写成规则";
  • [ ] 知道 Review 的时间应该花在逻辑与安全,而不是风格。

14. 扩展资源 ​

15. 下一步学习 ​

工程工具四节(《Git 分支、合并与回退》 / 《Git Flow 与团队协作》 / 《Maven 进阶:依赖管理与多模块》 / 《代码规范与自动校验》)到此完成。接下来可以:

前置知识

无,可直接开始

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引