接手一个历史项目,第一件事往往是翻 git log。最刺眼的那条不是某个复杂重构,而是一条+842 -831的提交——点进去发现只改了一个判空条件。罪魁祸首基本只有一个:有人对着文件按了Ctrl+Alt+L,让 IDEA 的 Reformat Code 把整个文件从头到脚重排了一遍。这个功能用对了,是团队代码风格统一成本最低的方案;用错了,就是 git blame 的灾难现场,未来半年你查一行代码是谁写的,都会被那一次格式化带偏。
Reformat Code 就是 IntelliJ IDEA 里的代码重排版功能,Windows/Linux 上的默认快捷键是Ctrl+Alt+L,macOS 上是Cmd+Option+L。它管的事情很纯粹:把缩进、空格、换行、大括号位置、空行数量、方法链换行、import 顺序这些纯排版层面的东西,按你配置的 Code Style Scheme 重新算一遍。它不重命名变量,不抽方法,不改逻辑,也不做任何语义层面的优化。
这篇文章面向三类人:一类是刚上手 IDEA、只知道快捷键却不知道背后规则的开发者;一类是要在多人协作项目里统一代码风格、又不想被大规模 diff 淹没的技术负责人;还有一类是已经踩过坑、想把格式化塞进提交前检查或者持续集成流水线的人。下面我从它的行为边界讲起,一路讲到配置分层、批量执行、自动化落地,以及我自己踩过的那些坑。
1. Reformat Code 到底改了什么:先把边界划清楚
1.1 它动的只是排版,但有三类例外值得单独说
先明确它能改什么:行首缩进、运算符两侧空格、逗号后空格、大括号换行与不换行、else是否另起一行、连续空行压缩成一行、注解参数换行、方法链点号换行、extends/implements的换行位置、import的分组与排序。这些都是"视觉层"的东西,改完代码的编译结果和运行行为完全一致,所以它才敢做成一个无脑快捷键。
但"只动排版"这句话有三类例外,必须单独拎出来。
第一类是文本块(Text Block)和多行字符串。Java 从 15 开始支持"""文本块,里面的缩进是内容的一部分——文本块会用结尾的"""所在的缩进量做一次公共缩进剥离,一旦格式化把"""的位置挪了,最终字符串内容就变了。所以如果你的代码里有拼 SQL、拼 JSON、拼模板的多行字符串,格式化之后一定要跑一遍相关单测。
第二类是注释和 Javadoc。IDEA 会把 Javadoc 按Hard wrap at的宽度重新折行,也会调整*的对齐。这本来是好意,但如果你在 Javadoc 里用 Markdown 或者对齐过的表格,重排之后排版就毁了。更隐蔽的是行尾注释,格式化可能把它挪到上一行去,//后面的内容语义没变,可读性却变了。
第三类是 formatter 标记之间的代码。IDEA 支持@formatter:off/@formatter:on这对注释标记,标记之间的内容会被彻底跳过。这个开关默认在Settings > Editor > Code Style > Formatter Control里,需要确认它处于开启状态,否则标记就是普通注释,一点作用都没有。我见过有人写了@formatter:off结果没生效,就是因为这个开关是关的。
提示:格式化不会碰字符串字面量内部的空格,也不会动
//后面的文字内容,但它会动文本块的整体缩进,这两者不是一回事,别混为一谈。
1.2 三个入口,别只会按快捷键
大部分人只知道一个快捷键,其实 IDEA 给了三个入口,用途完全不同。理清它们,能省掉很多"为什么格式化和我想的不一样"的困惑。
| 入口 | 触发方式 | 适用场景 | 是否需要提前选范围 |
|---|---|---|---|
| 直接格式化 | Ctrl+Alt+L/Cmd+Option+L | 当前文件、当前选中片段,想快速对齐 | 想只格式化片段就先选中代码 |
| 带参数对话框 | 右键Code > Reformat File... | 需要控制范围、是否清 import、是否重排代码 | 对话框里可以逐项勾选 |
| 代码清理 | Code > Code Cleanup | 批量应用检查项修复,比纯格式化更重 | 需要先选 Cleanup Profile |
带参数对话框是这三个里最容易被忽略、但最好用的一个。它弹出来之后你能看到几个关键选项:范围可以选Whole file、Selected text、Changed lines;下面有一组复选框,包括Optimize imports、Rearrange code、Cleanup code;再往下是过滤条件,可以按文件扩展名掩码或者按 Scope 限定。这些选项决定了它是"温柔地整理排版"还是"把整个文件重造一遍"。
Code Cleanup是更重的一档,它不只是排版,还会应用一批 inspection 的自动修复,比如把匿名内部类转成 lambda、删掉多余的final、简化if返回。这个东西默认配置下手很重,建议在团队里明确"只有需要时才用",不要和 Reformat Code 混为一谈。我见过有人把Code Cleanup绑到保存动作上,结果代码写一行变一行,最后只能重装配置。
1.3 Changed Lines:被严重低估的一个选项
Changed lines这个范围选项,是我认为 Reformat Code 里最被低估的功能。它的含义是:只格式化"相对版本控制有改动的那几行"。
为什么它重要?因为团队协作里格式化最大的成本不是执行,而是审查。你把一个 2000 行的文件整体重排,diff 里 1800 行是噪音,真正有效改动只有 3 行。评审的人要么放弃看,要么花半小时去猜你到底改了什么。用了Changed lines,diff 就只保留你真正动过的部分,同时你新写的代码又自动符合团队风格,两头都占。
它的另一个价值是 git blame。整个文件重排之后,几乎所有行的 blame 都会指向那次格式化提交,而这条提交通常信息写得很敷衍。半年后你要查某行逻辑为什么这么写,点进去看到"format code",那种绝望感是很具体的。
我现在的操作习惯是:日常改代码用Changed lines范围,在提交前处理,绝对不整文件重排;只有在新建文件、或者文件本身已经被改得面目全非时,才会用Whole file。这个习惯养成之后,我在团队里几乎没再因为格式化跟人起过争执。
2. 格式化规则从哪来:三层配置和它们的优先级
2.1 IDE 级、项目级、.editorconfig 三层怎么区分
IDEA 的代码风格配置不是一份,而是三层,很多人只改了一层,结果发现格式化结果和自己改的不一致,问题就出在这。
| 层级 | 存放位置 | 影响范围 | 是否随仓库走 |
|---|---|---|---|
| IDE 级 Scheme | IDEA 配置目录 | 本机所有项目 | 否 |
| 项目级 Scheme | 项目.idea/codeStyles/ | 当前项目 | 是,能提交 |
| EditorConfig | 项目根目录.editorconfig | 当前项目(需开启支持) | 是,能提交 |
三者的关系是叠加覆盖的。IDE 级是兜底,项目级在项目内覆盖 IDE 级,而.editorconfig在它声明过的属性上优先级最高。也就是说,你在设置面板里把缩进改成 2,但仓库里.editorconfig写着indent_size = 4,那实际生效的就是 4——这时候你会觉得设置面板"骗人",其实是优先级没搞明白。
.editorconfig的支持开关在Settings > Editor > Code Style > Enable EditorConfig support。这个开关默认是开的,但如果你是从很老的版本 upgrade 上来的配置目录,它有可能是关的,这一点值得花十秒钟确认一下。开启之后,IDEA 会在编辑区右下角或者文件树里给出提示,说明当前文件被 EditorConfig 接管了。
至于哪一层该放什么,我的建议是:.editorconfig只放跨语言通用的那几项,比如indent_style、indent_size、end_of_line、charset、trim_trailing_whitespace;语言相关的细节,比如 Java 的大括号位置、方法链换行策略,放在项目级的 Code Style Scheme 里。分工会清晰很多。
2.2 真正影响观感的就那几组参数
Code Style 面板里选项看着很多,实际上真正影响日常观感的就六组。把这几组吃透,剩下的基本都是默认值就够用了。
第一组是缩进。Tab size、Indent、Continuation indent三个值。Tab size 是制表符的显示宽度,Indent 是一次缩进的空格数,Continuation indent 是"折行之后的续行"额外缩进多少。Java 默认是 4 / 4 / 8。续行缩进给 8 是有道理的:续行缩进比正常缩进大一倍,能让人一眼看出来"这行是上一行的延续",而不是一个新的代码块。如果你把它改成 4,多行方法调用的参数和循环体就会视觉上混淆。
第二组是制表符策略。Use tab character和Smart tabs。绝大多数 Java 项目应该是"不勾选 Use tab character",全用空格。混用 tab 和空格是格式化问题里最经典的一类,.editorconfig里的indent_style能强制统一,但前提是团队所有人的编辑器都遵守它。
第三组是硬换行宽度。Hard wrap at,Java 默认 120。这个值的取舍逻辑是:太小,一行稍微长点就被拆开,代码变得又高又碎;太大,一行奔着 200 字符去,代码评审和并排对比都难受。120 是个比较稳的中间值,宽屏窄屏都能接受。
第四组是换行与括号。Braces placement决定左大括号是跟在行尾还是另起一行,else on new line决定else是否另起一行,Keep line breaks决定已有的手动换行要不要保留。这几个选项直接影响 diff 的形态——一旦改了,全团队的代码都会重排,属于"改一次震动一次"的参数,务必要在项目启动时就定下来,不要中途改。
第五组是空行。Blank lines那一栏里能配置"包声明后留几行"、"字段之间至少留几行"、"方法之间至少留几行"。合理设置能显著提升可读性,但设置得太多会让文件变得特别长。我一般设成方法之间至少 1 行、类的第一个成员前留 1 行,其余保持默认。
第六组是 Keep when reformatting。里面能勾选"保留第一列的注释"、"保留换行符"、"保留手动换行"。这几个选项的意义是给格式化留出"人工干预的余地"。尤其是"保留第一列的注释",很多项目用它来区分"区块注释"和"普通行内注释",勾上之后,从第一列开始写的注释就不会被缩进吞掉。
2.3 Import Layout 和星号导入阈值:团队矛盾高发区
Import 相关的配置是团队里最容易吵起来的地方。两个参数最要命:Import Layout的顺序,以及Class count to use import with '*'这个星号导入的阈值。
先说 Import Layout。它决定了import语句按什么顺序分组、组间留几个空行。IDEA 自带的默认顺序是它自己的一套习惯,很多国内团队更习惯把java/javax放最前,然后是第三方库,最后是项目内的包。要给项目定一套,直接点击Import Layout那一栏,用列表编辑器拖顺序就行。一个常见配置长这样:
import java.* import javax.* <blank line> import all other imports <blank line> import com.yourcompany.*再说星号导入。这个阈值的含义是:同一个包下的类被导入超过 N 个时,合并成import java.util.*。Java 默认是 5,静态导入的默认阈值是 3。从我个人经验看,这个功能弊大于利。原因有两个:一是星号导入之后,代码里看到List你不知道它是java.util.List还是别的同名类,尤其是用反射或者注解处理器的时候;二是很多团队的静态检查工具(Checkstyle、Spotless 的默认规则)会直接报错。
我的建议是把Class count to use import with '*'和Names count to use static import with '*'都调到一个很大的值,比如 999,相当于关闭星号合并。这个改动很小,但能避免后面一堆麻烦。
3. 高频场景:把格式化调成团队想要的样子
3.1 Java 风格逐项调参,每一项说清取舍
前面讲的是参数在哪儿,这一节讲具体怎么填、为什么这么填。下面这份是我在多数后端项目里用的配置,不是标准答案,但每一项的取舍逻辑可以直接拿去讨论。
| 参数项 | 建议值 | 取舍理由 |
|---|---|---|
| Tab size / Indent | 4 / 4 | 与业界主流 Java 项目一致,减少新人认知成本 |
| Continuation indent | 8 | 续行与普通缩进拉开一倍差距,多行参数一眼可辨 |
| Use tab character | 不勾选 | 全空格,避免 tabs 与 spaces 混排导致的对齐错乱 |
| Hard wrap at | 120 | 兼顾宽屏与代码评审窗口,超长链式调用也不会太碎 |
| Braces placement(类/方法) | End of line | 左括号跟行尾,版面紧凑,diff 行数更少 |
| else on new line | 不勾选 | } else {一行解决,纵向长度更短 |
| Method chain wrap | Wrap always / Chop down if long | 长链式调用拆行,每个.起一行,便于逐行读 |
| Keep line breaks | 勾选 | 尊重作者的手动断行,避免格式化"自作聪明" |
| Blank lines(方法之间) | 1 | 恰好分隔,不浪费纵向空间 |
| 星号导入阈值 | 调大(如 999) | 关闭星号合并,避免同名类歧义与静态检查报错 |
这里我特别想说一下Method chain wrap。Stream API 和 Builder 模式大量使用链式调用,一条链七八个.map()很常见。如果选择不换行,一行可能超过 200 字符;如果选择"总是换行",哪怕只有两个.也会被拆成三行,看着很空。折中方案是选"超过宽度才拆",并且让每个.单独起一行,这样链式调用的语义层次是最清楚的。
还有一个容易被忽略的点:Alignment那一栏。它控制连续赋值、连续字段声明、连续注释是否按列对齐。对齐确实好看,但它非常"脆"——只要有一行的变量名变长,整块都要重排,diff 会变得很大。我个人倾向是把这些对齐选项都关掉,用普通缩进就好。好看的东西往往维护成本高,这在格式化上是成立的。
3.2 保存即格式化:Actions on Save 怎么配才不添乱
配置好风格之后,最省心的落地方式就是"保存时自动执行",让格式化变成无感的。入口在Settings > Tools > Actions on Save,里面有几个复选框值得逐一说明。
Reformat code:勾上之后每次保存都重排。下面通常有子选项,可以选Whole file或者Changed lines。强烈建议选Changed lines,理由和前面一样,不污染 diff。Optimize imports:保存时清理没用到的 import,并按 Import Layout 重排。这个基本没副作用,放心勾。Rearrange code:按配置的规则重排类的成员顺序(字段、构造器、方法分区)。这个要慎重,因为它会真的把代码块搬来搬去,diff 会变得很夸张。除非团队一开始就用了这套规则,否则别开。Run code cleanup:前面说过,手很重,别开。
这里有个实操上的坑值得提醒:Actions on Save是"保存触发"的,而 IDEA 的自动保存(失焦保存、定时保存)也是保存。所以如果你把这些动作全勾上,切个窗口回来可能就发现自己正在编辑的文件被重排了。更稳妥的做法是关掉"自动保存时执行动作",只保留手动Ctrl+S时触发——这个开关在同一个设置页面里,仔细找一下是有的。
另外,早期大家用第三方插件(比如 Save Actions 那类)来实现同样的效果。现在内置的 Actions on Save 已经覆盖了主要场景,新项目没必要再装插件。插件和内置动作如果同时开着,会出现"格式化两次"的情况,虽然结果一致,但每次保存都要多花几百毫秒,文件大了手感很明显。
3.3 局部豁免:给不该被格式化的代码留个口子
前面提过@formatter:off和@formatter:on。用法很简单:
// @formatter:off String sql = "select id, name, age from user " + "where status = 1 " + "order by id desc"; // @formatter:on这对标记之间的代码会被完全跳过,适合放手工对齐过的 SQL、坐标矩阵、ASCII 表这类"排版本身就是信息"的代码。有一点要注意:标记本身是注释,所以它们会被保留在代码里,评审时看到不要觉得奇怪。另外不同语言的标记语法可能不一样,XML 里通常是<!-- @formatter:off -->,写之前确认一下当前语言的注释语法。
除了标记,还有"按目录/文件豁免"的做法。IDEA 的 Code Cleanup 和相关动作支持按 Scope 限定范围,你可以建一个 Scope 把生成代码目录(比如一些代码生成器产出的包)排除掉。这个做法比在文件里撒标记更干净,特别适合整个包都是自动生成的场景。
还有一种情况是文件类型本身不支持格式化。IDEA 只对它认识的语言有 Code Style 配置。像 GDScript 这种游戏脚本语言,IDEA 本身并不内置支持,得靠第三方语言插件,而插件的格式化能力参差不齐——插件里配的缩进规则可能和 IDEA 主设置完全不搭界。遇到"这个文件按快捷键没反应"的情况,先确认一下当前文件的语言是不是 IDEA 原生支持的,别一头扎进设置面板里翻半天。
3.4 粘贴格式化、自动缩进与行分隔符
还有三个小设置,日常存在感不强,但一旦出问题就很烦人。
Reformat on paste。位置在Settings > Editor > General > Smart Keys,有三个档位:None、Indent block、Indent each line。含义分别是"粘贴后完全不处理"、"只调整整块缩进"、"每一行都按上下文重新缩进"。从其他语言或者从网页上拷代码进来时,这个设置决定了你看到的是整齐的代码还是一坨。我一般选中间那档,兼顾整洁和不破坏原有排版。
Detect and use existing file indentation。这个选项在 Code Style 的通用设置里(不同版本位置略有差异,大致在Editor > Code Style顶层)。勾上之后,IDEA 打开一个缩进风格和当前配置不一致的文件时,会弹提示询问是否采用该文件的缩进。这个功能在维护老项目时非常有用——很多老代码用 2 空格或者 tab,你不想为了打开它就污染整个文件。缺点是弹窗有点烦,等你熟悉项目之后可以关掉。
行分隔符和编码。右下角状态栏能直接看到当前文件的行分隔符是LF还是CRLF、编码是UTF-8还是GBK。格式化只处理排版,不会帮你改这两项,但跨平台协作时它们才是"整个文件全变了"的真正元凶。团队里统一用LF加UTF-8,并在.editorconfig里写死end_of_line = lf和charset = utf-8,能省掉大量莫名其妙的 diff。至于.gitattributes里的text=auto,如果和.editorconfig冲突,是另一个层面的问题,不在格式化范畴内,这里不展开。
4. 批量与自动化:从单文件到整个仓库
4.1 项目级批量格式化的正确姿势
有时候你就是需要给整个模块甚至整个项目来一次统一格式化,比如接手一个从没定过规范的老项目。这种情况下无脑全选然后Ctrl+Alt+L是下策,正确顺序是这样的。
第一步,先保证工作区干净。git status必须是 clean,或者把未提交的改动 stash 起来。这一步的意义是:万一格式化结果不满意,一条git checkout .就能全部撤销,不会有任何真实改动被误伤。
第二步,拉一个独立分支。分支名最好直白点,比如chore/format-module-user,让后来的人一眼知道这条提交只是格式化,不用逐行看。
第三步,分模块执行,别一次性全项目。选中一个模块的源码根目录,右键Code > Reformat Code,在弹出的对话框里确认范围是当前目录、勾上Optimize imports、不要勾Rearrange code、也不要勾Cleanup code。分模块的好处是可以分批提交、分批验证,出问题也好定位。
第四步,跑测试。格式化原则上不改行为,但前面说过文本块和注释有例外,所以编译加跑一遍单测是必要的。至少要保证能编译通过。
第五步,用git diff --stat看一眼规模。如果一个模块 diff 出来上万行,先停下来想想是不是选错了目录,有没有把生成代码、第三方拷贝代码、资源文件一起卷进来。这类误伤很常见,尤其是项目里放着自动生成的*.pb.java、*.g.dart之类的目录。
注意:格式化提交一定要单独成一条 commit,信息写清楚"仅格式化,无逻辑改动"。这个习惯在代码评审和后续查历史时会救命。
4.2 命令行格式化器与持续集成校验
靠人记得按快捷键是不可靠的,真正能兜住团队底线的是自动化。IDEA 自带一个命令行格式化工具,位于安装目录的bin下,Windows 是format.bat,Linux/macOS 是format.sh。它的基本用法是把导出的代码风格文件和一个目标路径传进去:
# 先导出代码风格:Settings > Editor > Code Style > 齿轮 > Export > IntelliJ IDEA code style XML /path/to/idea/bin/format.sh \ -s ./config/codeStyleSettings.xml \ -r ./src/main/java-s指定风格配置,-r表示递归处理子目录。不同版本的开关可能有差异,执行前先跑一次-h看帮助输出,以实际输出为准。
把这个工具放进持续集成里,就有两种用法。一种是"检查模式":跑一遍格式化,然后用git diff --exit-code判断有没有产生改动,有改动就说明有人提交了不符合风格的代码,流水线失败。大致是这么写:
#!/usr/bin/env bash set -euo pipefail /path/to/idea/bin/format.sh -s ./config/codeStyleSettings.xml -r ./src/main/java if ! git diff --quiet; then echo "检测到不符合代码风格的改动,请本地格式化后再提交:" git diff --stat exit 1 fi不过说实话,用 IDEA 命令行格式化器做 CI 校验有个现实问题:它依赖完整的 IDEA 安装包,在流水线容器里体积大、启动慢。所以更主流的做法是用各语言生态里的专用工具,比如 Java 项目的 Spotless 或者 Checkstyle,Kotlin 项目的 ktlint。这类工具启动快、规则可版本化、和构建工具集成度高。
# Maven + Spotless 的常见用法 mvn spotless:check # 校验,不一致就失败 mvn spotless:apply # 本地一键修复# Gradle 里的等价命令 ./gradlew spotlessCheck ./gradlew spotlessApply这里有个关键的实操要点:CI 里的规则工具和 IDEA 里的 Code Style 配置必须对齐。如果两边不一致,会出现"IDE 里格式化完提交,CI 却报错"的死循环,非常消耗士气。对齐的办法是让 Spotless 的配置(Java 里常用 Google Java Format 或者 Eclipse JDT 的配置文件)和.idea/codeStyles里的设置保持同一套规则;如果实在没法完全对齐,至少把"哪个是最终裁判"定下来——通常让 CI 当裁判,IDE 配置向它靠。
4.3 和 Git 协作:把一次性大 diff 藏起来
如果项目确实需要做一次全量格式化(比如从混乱状态第一次统一),有一招能让后续的git blame保持干净:把这次格式化提交的哈希记进.git-blame-ignore-revs文件。
# .git-blame-ignore-revs # 全量代码格式化,仅排版改动 a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0然后在本地配置一次:
git config blame.ignoreRevsFile .git-blame-ignore-revs配置之后,git blame会自动跳过这些提交,把责任行归到格式化之前的那次真实改动上。git log -L、部分 IDE 的 annotate 功能也支持这个文件。这个技巧在大规模重构和格式化场景下特别值,属于知道了就再也不想丢掉的东西。
另外,git diff也有一组参数能对抗格式化噪音。比如git diff -w会忽略所有空白差异,在审查一次格式化提交时,用它可以快速确认"是不是真的只有空白变化"。如果git diff -w输出为空,那就说明这次提交确实只是排版,可以放心跳过。
5. 常见问题排查实录
5.1 按了快捷键没反应,先查这几处
这是问得最多的一类问题:按下Ctrl+Alt+L毫无反应。按下面顺序查,基本能覆盖九成情况。
一是文件是否只读。左下角或者标题栏会显示锁图标,只读文件不能格式化,先解锁。
二是当前文件语言是否被支持。纯文本文件、日志文件、IDEA 不认识的扩展名,都不会有格式化动作。可以看右下角状态栏的语言标识,如果是Plain text,那自然什么都不发生。
三是是不是被 Scope 或文件掩码排除了。在Reformat File对话框里如果设置了过滤器,可能会把当前文件排除在外。检查一下过滤器里有没有误加的规则。
四是.editorconfig里有没有异常配置。比如有的仓库写了insert_final_newline = false,或者某个属性值写得不对导致解析异常,表现就是格式化行为完全不符合预期。打开Settings > Editor > Code Style,看对应语言那一栏上方有没有 EditorConfig 的接管提示。
五是缓存问题。IDEA 的索引和缓存偶尔会出问题,表现是各种诡异的不一致。File > Invalidate Caches之后重启,能解决相当一部分"理论上有反应但实际没反应"的情况。这个操作代价是重启后要重新建索引,大项目可能要几分钟,所以只在确实怀疑缓存时用。
5.2 格式化之后代码反而变乱了
比"没反应"更麻烦的是"有反应但不对"。几种典型表现和成因我整理一下。
表现一:格式化后代码全红,编译报错。优先怀疑文本块。文本块的内容依赖"""的缩进,格式化挪了位置,字符串内容就变了,可能导致 SQL 语法错、JSON 解析失败。回滚这个文件,把文本块用@formatter:off包起来。
表现二:注解被拆得乱七八糟。长注解参数列表在Hard wrap at比较小的时候会被拆成很多行,看着很不舒服。可以针对注解单独设置换行策略,把Annotation parameters的Wrap always改成不换行或者只在超宽时换行。
表现三:枚举和方法链换行位置很怪。这类问题通常是Keep line breaks没勾选,导致作者的手动换行被抹掉重新计算。勾上它,尊重原有换行,问题基本消失。
表现四:整个文件每一行都在 diff 里。十有八九是行分隔符或者空白字符的问题。检查右下角的LF/CRLF,检查文件里有没有混入 tab,用Editor > General > Strip trailing spaces on Save配合统一行分隔符处理。
表现五:注释里的对齐表格被重排。Javadoc 重排是默认行为,没法关掉单个功能,只能把整个 Javadoc 块用格式化标记包住,或者干脆不用表格形式。
5.3 常见问题速查表
下面这张表我放在项目 wiki 里,新人问格式化问题的时候直接甩链接,能省掉大量重复解释。
| 现象 | 最可能的原因 | 处理方式 |
|---|---|---|
| 快捷键无反应 | 文件只读 / 语言不支持 | 解锁文件,确认右下角语言标识 |
| 格式化结果与设置面板不一致 | .editorconfig覆盖了 IDE 设置 | 检查根目录.editorconfig对应属性 |
| 保存时没自动格式化 | Actions on Save 未勾选,或自动保存未触发 | 检查Settings > Tools > Actions on Save |
@formatter:off不生效 | Formatter Control 开关未开启 | 打开Code Style > Formatter Control |
| 文本块内容被改 | 格式化挪动了"""的缩进 | 用格式化标记包住,并补一条单测 |
| import 顺序总是被改回来 | Import Layout 与 Optimize imports 冲突 | 统一 Import Layout 后再让保存动作执行 |
| 出现星号导入 | 星号导入阈值过小 | 把类/静态导入阈值调到很大或关闭 |
| 格式化后 diff 上万行 | 误把生成代码目录纳入范围 | 用 Scope 排除生成目录,分模块执行 |
| CI 报风格错误但 IDE 正常 | IDE 与 CI 工具规则不一致 | 以 CI 规则为准,反向同步 IDE 配置 |
| 格式化很慢,界面卡住 | 单次范围过大或文件巨大 | 拆分目录执行,必要时关掉粘贴实时格式化 |
6. 几条踩过之后才明白的经验
6.1 不要在功能分支上顺手格式化整个文件
这是我踩得最深的一个坑。早年间改一个 bug,顺手按了Ctrl+Alt+L,整个文件重排。提交上去之后,评审的同事在评论区沉默了十分钟,然后回了一句"你能告诉我你改了哪三行吗"。更难受的是两周后排查另一个问题,git blame顶到最上面的全是那次格式化,只能一层一层往下翻。
从那之后我给自己定了条死规矩:功能分支里只允许Changed lines范围的格式化,任何Whole file的重排都必须单独开一个chore/format-*分支。这条规矩看着麻烦,实际执行下来几乎不增加负担,但省下来的评审时间和排查时间非常可观。
6.2 把代码风格文件当构建产物来管理
.idea/codeStyles和.editorconfig这两个东西,很多团队是"谁配了谁本机生效",结果每个人机器上的格式化结果都不一样,互相提交之后反复打架。正确的做法是把它们当成构建配置来管理:有明确的 owner,变更走评审,在 README 里写清楚"改代码风格请提 PR 修改这两份文件,不要在本地私自改设置"。
至于.idea目录整体要不要提交,通常的做法是提交.idea/codeStyles、.idea/inspectionProfiles这类团队共享配置,忽略掉workspace.xml、usage.statistics.xml这类个人状态文件。这个边界划清楚之后,团队协作会顺畅很多。
6.3 大仓库里的性能与时机
项目代码量上来之后,格式化是有成本的。我测过一个大概四十万行的多模块后端项目,选中整个源码根目录执行一次全量格式化,期间界面上会出现一段明显的卡顿,索引也要重建。所以有两条经验:一是分模块执行,单次范围控制在一个模块内,体感好很多;二是把"保存时格式化"的范围限制在Changed lines,避免每次保存都全文件重算。
还有一个时机问题:格式化最好在提交前、写完一个小功能、准备创建提交的时候做,不要写一行保存一次。前者是一次性成本,后者是把成本摊到每一次保存上,叠加起来非常影响手感。
6.4 给新人的接入清单
新人入职配环境的时候,我会给四个动作,抄完基本就不会在格式化上犯错:
- 确认
Settings > Editor > Code Style > Enable EditorConfig support处于开启状态,让仓库里的.editorconfig生效。 - 确认
Settings > Tools > Actions on Save里勾了Reformat code(范围选Changed lines)和Optimize imports,不要勾Rearrange code和Run code cleanup。 - 确认
Settings > Editor > Code Style > Formatter Control里 formatter markers 是开启的,方便后面做局部豁免。 - 本地跑一次
mvn spotless:check或者./gradlew spotlessCheck,确认自己的机器和流水线的判断标准一致。
我自己在这套流程上折腾了好几年,中间经历过"全量格式化把 blame 冲掉"、"CI 和 IDE 规则打架导致来回改"、"保存动作开太猛每次打字都被重排"这些坑,最后沉淀下来的其实就是几句话:格式化只在提交前做,日常只做增量,规则交给仓库文件而不是个人设置,最终裁判是流水线。听起来很朴素,但每一条都是拿实际的返工时间换来的。