Java代码规约落地:文档、插件与CI门禁实践
简介:面向Java开发团队、代码评审人员及需要规范编码习惯的开发者,这份阿里巴巴代码规约学习包聚焦落地《Java开发手册》中的命名、注释、集合、并发、异常日志与工程结构等约定,适合团队统一风格、新人培训和日常自查。压缩包共5个文件,以2份PDF手册、2个XML配置文件和1个ZIP插件包为主,整体约20.14MB:PDF用于系统阅读规约条文,XML承载格式化与代码模板配置,ZIP则收纳IDE规约检查插件,便于在Eclipse与IDEA环境中按需取用。已有488人学习下载,说明其在Java规范学习与团队协作场景中具有一定参考价值。资源同时提供详尽版与华山版手册,读者可对照不同版本梳理规则演进,再借助插件和模板把静态条款转化为编码时的即时提示与统一格式,从而减少评审争议、提升代码可读性与维护性。
1. 代码规约不是格式模板:先把它当成一份可执行的缺陷清单
接手一个跑了三年的 Java 服务,编译一次过,评审却总在同一个地方吵:日志里全是字符串拼接、异常被 catch 后什么都没做、集合初始化不指定容量。你缺的不是又一份格式模板,而是一套能落地的代码规约:一份可检索的代码规约文档、一个在 IDE 里实时提示的代码规约插件,以及能把规则接进构建的检查脚本。这套资源适合 Java 后端、代码评审负责人和正在统一多团队风格的人;不适合只想找格式化配置的人。下面按“怎么读文档、怎么装插件、怎么接流水线、哪里会翻车”拆开讲。
2. 拆开代码规约文档:七类规则与优先级怎么读
代码规约文档最容易犯的错,是把它当小说从头读到尾。读完之后只记得“命名要规范”“异常要处理”,落到代码里还是靠个人习惯。我的做法是把它当缺陷清单:每一条规则都要能回答三个问题——这条规则防的是什么线上问题、在哪个阶段检查、例外情况谁来批。文档里的“强制、推荐、参考”不是装饰,它直接决定这条规则是阻断构建,还是只在评审里提醒。
一套完整的 Java 代码规约,通常绕不开命名、常量、集合、并发、异常、日志、注释这七类。不同团队的条目数量不一样,但结构差不多。第一遍读的时候不要背条目,先给每类规则找一个真实故障场景。比如“线程池不允许用 Executors 直接创建”,不是因为写法难看,而是因为队列长度和最大线程数不可控,流量一冲就堆任务;“日志使用占位符”不是因为字符串拼接慢,而是因为拼接对象可能触发额外方法调用,还会让日志脱敏和检索变难。把规则和故障场景绑在一起,评审时才有说服力。
2.1 先分清强制、推荐、参考:别把参考项做成硬门禁
文档里的“强制”项适合进 CI,“推荐”项适合进 IDE 和评审,“参考”项更适合团队按业务场景决定。我见过一个反例:某项目把注释覆盖率设成硬门禁,结果开发为了过检,在 getter/setter 上批量生成“获取某某字段”的注释,评审质量反而下降。注释规则本身没错,错在把它放进了不该放的阶段。
比较稳的分层是这样:
| 类别 | 典型条目 | 默认级别 | 建议卡点 |
|---|---|---|---|
| 命名 | 类名使用 UpperCamelCase,常量全大写下划线 | 强制 | IDE + CI |
| 常量 | 不允许直接写魔法值 | 强制 | CI,测试代码可降级 |
| 集合 | 初始化时指定容量 | 推荐 | IDE |
| 并发 | 线程池显式创建,不允许无界队列 | 强制 | CI |
| 异常 | 不允许空 catch,不允许吞异常 | 强制 | CI |
| 日志 | 使用占位符,不拼接大对象 | 强制 | IDE + CI |
| 注释 | 类、公共方法写清用途和边界 | 推荐 | 评审 |
这张表不是让你照抄,而是让你在文档里补两列:检查阶段和例外说明。没有例外说明的规则,最后一定会被“历史代码”顶回来。比如魔法值规则,枚举、常量类、测试断言里都可能出现合理数字;线程池规则,框架托管线程池的场景也可能不适用。例外不是放水,是把口子写在明面上,避免每个人私下绕开。
2.2 把文档条目抽成表:从“看过”到“能查”
文档是给人读的,检查脚本要的是结构化数据。我的习惯是先把规则整理成 Markdown 表格,固定五列:规则编号 | 规则名称 | 级别 | 检查方式 | 例外说明。规则编号不要用“第一条”这种顺序号,后面加规则会乱;用NAMING-01、EXCEPTION-03这种带分类的前缀,评审 comment 里直接引用,沟通成本低很多。
下面这个脚本把 Markdown 表格抽成 CSV,方便排序、筛级别、分配给不同负责人。它只依赖 Python 标准库,不需要装额外包。
#!/usr/bin/env python3 # 把代码规约文档里的 Markdown 表格抽成 CSV,方便排序和分配负责人 import csv import sys from pathlib import Path def parse_rules(md_path: Path): rows = [] header = None for raw in md_path.read_text(encoding="utf-8").splitlines(): line = raw.strip() if not line.startswith("|"): continue cells = [c.strip() for c in line.strip("|").split("|")] if not cells: continue # 跳过 Markdown 表格的分隔行,例如 | --- | :---: | if all(set(c) <= {"-", ":", " "} for c in cells): continue if header is None: header = cells continue rows.append(dict(zip(header, cells))) return header, rows def main(): if len(sys.argv) != 3: print("用法: parse_rules.py <规则文档.md> <输出.csv>", file=sys.stderr) sys.exit(2) md_path, out_path = Path(sys.argv[1]), Path(sys.argv[2]) header, rows = parse_rules(md_path) if not header: print("没有找到 Markdown 表格表头", file=sys.stderr) sys.exit(1) with out_path.open("w", encoding="utf-8-sig", newline="") as f: writer = csv.DictWriter(f, fieldnames=header) writer.writeheader() writer.writerows(rows) print(f"写出 {len(rows)} 条规则到 {out_path}") if __name__ == "__main__": main()脚本逻辑很直白:逐行找以|开头的表格行,第一行当表头,分隔行跳过,其余行按表头映射成字典。utf-8-sig是为了让表格软件直接打开不乱码;sys.argv[1]是输入的规则文档,sys.argv[2]是输出的 CSV。它不支持合并单元格,也不支持一个单元格里再套表格,所以整理文档时尽量保持“一行一条规则”。如果表格列数不固定,zip会截断后面的列,跑完先抽几条核对,别直接拿去做门禁。
抽成 CSV 之后,下一步不是马上全量执行,而是做优先级排序。我一般按“修复成本 × 线上风险”分四象限:高风险、低成本的先做,比如空 catch、线程池直接创建、日志字符串拼接;高风险、高成本的排期做,比如包结构治理、历史模块拆分;低风险、低成本的交给 IDE 实时提示;低风险、高成本的先记录,不急着设门禁。一个团队一次能消化的规则通常不超过十条,先让这十条变成肌肉记忆,再谈全集。
2.3 先启用哪 30%:按“修复成本 × 线上风险”排序
新项目可以从强制项里挑五到八条先跑:命名、空 catch、日志占位符、线程池显式创建、集合初始化容量、equals常量在前、禁止System.out。这些规则争议小、修复快,跑一周就能看到 diff 变干净。老项目不要这么干,老项目要先做基线:把当前命中数记录下来,新代码零容忍,老代码按模块定下降目标。基线不是永久豁免,它是一张欠账表,每周看一次数字有没有往下走。
文档读到这个程度就够了:你知道哪些规则必须硬卡,哪些规则只适合提示,哪些规则需要例外说明。接下来才是插件和脚本的事。插件解决“写完才看到”的问题,脚本解决“有人绕过插件”的问题,两者缺一不可。
3. 代码规约插件落地:IDE 实时提示到 CI 兜底检查
代码规约插件最大的价值不是替你改代码,而是把问题暴露在编码现场。人写完一个方法,顺手看一眼黄色波浪线,比提交后等 CI 报错再回来改要省时间。但插件不能当唯一防线:有人用命令行提交、有人关掉实时扫描、有人换 IDE,构建侧必须有一个不依赖 IDE 的兜底检查。
3.1 IDE 插件安装与规则开关:先关掉三类噪音
常见安装路径是 IDE 的设置里找到插件市场,搜索“代码规约”,安装后重启。不同 IDE 的菜单名不一样,但逻辑差不多:插件市场安装、设置页启用、选择规则集、设置严重级别。装完先别全量开启,拿一个正在开发的小模块试跑,观察三类噪音:历史代码太多导致满屏告警、框架生成代码被误扫、团队约定和规则冲突。
我一般会做这几个设置:
- 扫描范围先选当前模块,不选整个工程。老项目全量扫描一次几千条,开发者第一反应是关插件。
- 严重级别分两档:空 catch、线程池、日志拼接这类设成 Error;命名、注释、import 顺序设成 Warning。Error 用来阻断,Warning 用来提醒。
- 生成代码目录加排除,比如
target/generated-sources、build/generated、src/gen。这些目录的代码不是人写的,扫描结果没有修复价值。 - 规则集锁定一份,和 CI 脚本使用同一份规则 ID。插件内置规则和团队自定义规则混在一起时,最容易出现“本地不报、CI 报”的扯皮。
插件装好之后,写一个故意违规的类测试:一个空 catch、一个System.out.println、一个字符串拼接日志。如果插件三条都能标出来,说明规则集加载成功;如果只标出一部分,先去设置页看规则开关,再去日志里看有没有规则解析失败。不要凭感觉判断插件生效,拿样例说话。
3.2 CI 兜底脚本:五类硬伤用 70 行 Python 卡住
构建侧我倾向于先上一个轻量脚本,不追求覆盖全部规则,只卡五类高价值、低争议问题:标准输出、printStackTrace、空 catch、日志字符串拼接、直接用Executors创建线程池。这个脚本不依赖 IDE,也不依赖特定构建工具,放在仓库tools/目录下,本地和流水线都能跑。
#!/usr/bin/env python3 # 轻量代码规约门禁:只查五类高价值、低争议问题,作为插件的兜底 import argparse import re import sys from pathlib import Path # 规则名 -> (正则, 说明) RULES = [ ("NO_SYSTEM_OUT", re.compile(r"\bSystem\.(out|err)\.print"), "禁止直接使用标准输出"), ("NO_PRINT_STACK_TRACE", re.compile(r"\.printStackTrace\s*\("), "异常不要只打印栈"), ("EMPTY_CATCH", re.compile(r"catch\s*\([^)]*\)\s*\{\s*\}"), "禁止空 catch"), ("LOG_CONCAT", re.compile(r"log(?:ger)?\.(?:info|warn|error|debug)\s*\(\s*\"[^\"]*\"\s*\+"), "日志使用占位符"), ("THREAD_POOL_BY_EXECUTORS", re.compile(r"Executors\.(newFixedThreadPool|newCachedThreadPool|newSingleThreadExecutor)\s*\("), "线程池应显式创建"), ] def iter_java_files(root: Path, include: str): yield from root.rglob(include) def scan_file(path: Path): hits = [] for lineno, line in enumerate(path.read_text(encoding="utf-8", errors="ignore").splitlines(), 1): stripped = line.strip() # 跳过整行注释,避免把示例代码当成违规 if stripped.startswith("//") or stripped.startswith("*"): continue for rule_id, pattern, message in RULES: if pattern.search(line): hits.append((rule_id, path, lineno, message, stripped)) return hits def main(): parser = argparse.ArgumentParser() parser.add_argument("--root", default="src/main/java", help="扫描根目录") parser.add_argument("--include", default="*.java", help="文件匹配模式") parser.add_argument("--max-violations", type=int, default=0, help="允许的最大违规数") parser.add_argument("--baseline", default="", help="基线文件,每行格式 path:line:rule") args = parser.parse_args() root = Path(args.root) if not root.exists(): print(f"扫描目录不存在: {root}", file=sys.stderr) return 2 all_hits = [] for path in iter_java_files(root, args.include): all_hits.extend(scan_file(path)) baseline = set() if args.baseline: baseline = { line.strip() for line in Path(args.baseline).read_text(encoding="utf-8").splitlines() if line.strip() } effective = [] for rule_id, path, lineno, message, code in all_hits: key = f"{path}:{lineno}:{rule_id}" if key in baseline: continue effective.append((key, message, code)) for key, message, code in effective: print(f"{key} {message} | {code}") print(f"命中 {len(effective)} 条,基线忽略 {len(all_hits) - len(effective)} 条") return 1 if len(effective) > args.max_violations else 0 if __name__ == "__main__": sys.exit(main())这个脚本的定位是“快速门禁”,不是完整静态分析。正则匹配 Java 有天然局限:多行空 catch、字符串里的System.out、被注释掉的代码都可能误判或漏判。所以规则集不要贪多,五条足够跑通流程。参数上,--root指定扫描目录,--include控制文件后缀,--max-violations是允许的违规数,--baseline用来吞掉历史欠账。基线文件的每一行是路径:行号:规则名,行号变化后需要重新生成,别把它当成永久免死金牌。
跑法很简单:
python3 tools/code_rule_check.py --root src/main/java --max-violations 0如果输出里出现NO_SYSTEM_OUT、EMPTY_CATCH这类规则 ID,说明脚本已经生效。下一步把它接进提交钩子和流水线,不要让开发者靠自觉执行。
3.3 规则分级表:哪些错误阻断,哪些只提示
插件和脚本都跑起来之后,一定要做规则分级,否则很快会出现“告警太多,没人看”的局面。我的分级标准是:会造成线上故障或排障困难的阻断,影响可读性的提示,依赖团队约定的先不卡。
| 规则 | 级别 | 卡点 | 处理方式 |
|---|---|---|---|
| 空 catch | 阻断 | CI | 必须补日志或抛出受控异常 |
| 线程池直接用 Executors | 阻断 | CI | 改为显式 ThreadPoolExecutor |
| 日志字符串拼接 | 阻断 | IDE + CI | 改为占位符 |
| 标准输出 | 阻断 | CI | 改为日志框架 |
| 集合初始化容量 | 提示 | IDE | 大集合必须指定,小集合可放过 |
| 注释缺失 | 提示 | 评审 | 公共方法必须写,私有方法看复杂度 |
| import 顺序 | 提示 | IDE | 保存时自动整理,不进 CI |
这张表要跟着项目走。新项目可以把阻断项设多些,老项目先把阻断项压到五条以内,等基线下降再逐步加。规则分级不是一次定终身,每季度回看一次:哪些规则从来没命中,哪些规则误报率高,哪些规则已经变成习惯可以降级。代码规约的生命力在于维护,不在于第一次发布有多全。
4. 把代码规约接进提交与评审:三条可复制的流水线
规则放在文档里,永远有人不看;规则放在插件里,永远有人关掉。真正让代码规约变成团队习惯的,是提交前有快速反馈、流水线有硬门禁、评审时有统一话术。这三件事不需要一次做完,可以先做提交钩子,再做 CI 增量,最后补评审清单。
4.1 pre-commit:只扫暂存区,三秒内给结果
提交钩子的原则是快。全量扫描一个老项目,开发者等三十秒就会想办法绕过。我的做法是只扫暂存区里的 Java 文件,复制到临时目录,复用前面的 Python 检查器。这样不管项目多大,检查范围都只跟本次提交有关。
#!/usr/bin/env bash set -euo pipefail # 只取暂存区里新增、修改、重命名的 Java 文件 files=$(git diff --cached --name-only --diff-filter=ACMR -- '*.java' || true) if [ -z "$files" ]; then exit 0 fi # 复制到临时目录,避免检查器扫到未暂存的改动 tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT while IFS= read -r f; do mkdir -p "$tmpdir/$(dirname "$f")" cp "$f" "$tmpdir/$f" done <<< "$files" python3 tools/code_rule_check.py --root "$tmpdir" --max-violations 0这段脚本的关键点在--diff-filter=ACMR:只取新增、修改、重命名、复制,删除的文件不检查。mktemp -d加trap保证临时目录退出时清理。--max-violations 0表示本次提交不允许新增违规。它不检查未暂存的文件,所以开发者如果只改了一半,钩子不会误报。缺点是开发者可以用--no-verify绕过,所以它只是第一道提醒,不能替代 CI。
钩子安装方式各团队不一样,常见做法是把脚本放到.git/hooks/pre-commit并加执行权限,或者用版本化的钩子管理工具统一安装。不要把一个几百行的钩子塞进仓库根目录就完事,最好放在tools/hooks/下,README 里写清楚安装命令。新人拉代码后跑一次安装脚本,比口头交代可靠。
4.2 流水线:全量基线 + 增量阻断
CI 侧我建议做两条线:全量扫描出报告,不直接失败;增量扫描只卡本次改动,失败就阻断合并。全量报告用来看趋势,增量门禁用来防止新债。历史债务用基线文件管理,每周挑一个模块把基线数字往下压,不要指望一次清零。
#!/usr/bin/env bash set -euo pipefail # 1) 全量扫描:只出报告,不阻断,用于观察历史债务趋势 mkdir -p build python3 tools/code_rule_check.py \ --root src/main/java \ --max-violations 999999 > build/code-rules-all.txt || true # 2) 增量扫描:只卡本次改动文件 base="${CI_BASE_REF:-main}" git fetch origin "$base" --depth=1 >/dev/null 2>&1 || true changed=$(git diff --name-only --diff-filter=ACMR "origin/$base"...HEAD -- '*.java' || true) if [ -z "$changed" ]; then echo "没有 Java 改动,跳过增量门禁" exit 0 fi tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT while IFS= read -r f; do mkdir -p "$tmpdir/$(dirname "$f")" cp "$f" "$tmpdir/$f" done <<< "$changed" python3 tools/code_rule_check.py --root "$tmpdir" --max-violations 0CI_BASE_REF用来指定对比分支,默认main,不同仓库按实际情况改。origin/$base...HEAD是三点 diff,表示从共同祖先到当前提交的改动,比两点 diff 更适合 PR 场景。全量扫描里的--max-violations 999999不是放任不管,而是让这一步永远成功,把结果写进build/code-rules-all.txt
本文还有配套的精品资源,点击获取