Checkstyle:把 Java 源码规范变成可复现门禁
Checkstyle 看见什么
Checkstyle 读取 Java 源文件,将文本解析为 Token 和抽象语法结构,再由 Checker、TreeWalker 及各个 Check 判断命名、导入、布局、Javadoc 和局部结构。它不读取运行时状态,也不以字节码缺陷模式推断空指针或锁错误。把 Checkstyle 的绿灯写成“代码没有缺陷”,等于让工具替团队承诺它从未观察的事实。
稳定门禁至少固定四样东西:运行构建的 JDK、Checkstyle 引擎版本、规则文件提交和被扫描的 source set。本文实验锁定 Checkstyle 13.11.0;该版本只作为可复现基线,升级时仍以官方发行记录和目标构建插件的兼容性为准。
| 对象 | Checkstyle 能回答 | 不能据此推出 |
|---|---|---|
| Java 源文件 | 哪条源码规则在什么位置失败 | 编译后的控制流没有缺陷 |
checkstyle.xml | 哪些模块、属性和严重度生效 | 团队已经处理所有历史债务 |
| suppression | 哪个受控范围暂时不参与裁决 | 被抑制代码天然安全 |
| XML/SARIF 报告 | 当次输入有哪些违规 | 未扫描模块也符合规范 |
先锁引擎,再接 Maven 插件
Maven 插件和 Checkstyle 引擎是两个版本轴。父 POM 可以锁 maven-checkstyle-plugin,再用插件依赖显式覆盖引擎;只写进 pluginManagement 不会自动执行,真正参与构建的模块仍需在 plugins 中声明。
<properties>
<maven.checkstyle.plugin.version>3.6.0</maven.checkstyle.plugin.version>
<checkstyle.version>13.11.0</checkstyle.version>
</properties>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${maven.checkstyle.plugin.version}</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>${checkstyle.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>checkstyle-main</id>
<phase>validate</phase>
<goals><goal>check</goal></goals>
<configuration>
<configLocation>${maven.multiModuleProjectDirectory}/config/checkstyle/checkstyle.xml</configLocation>
<suppressionsLocation>${maven.multiModuleProjectDirectory}/config/checkstyle/suppressions.xml</suppressionsLocation>
<includeTestSourceDirectory>false</includeTestSourceDirectory>
<failOnViolation>true</failOnViolation>
<violationSeverity>warning</violationSeverity>
<consoleOutput>true</consoleOutput>
</configuration>
</execution>
</executions>
</plugin>failOnViolation 负责在规则命中后让构建失败;配置文件无法加载、规则类不存在或解析出错属于分析错误,也必须失败。验收时查看 ./mvnw help:effective-pom,确认最终插件版本、引擎依赖、路径和执行阶段,而不是只看父 POM 属性。
Gradle 的插件版本和工具版本不是一回事
Gradle 自带 Checkstyle 插件,toolVersion 决定分析引擎。公共配置适合放进 convention plugin,让每个 Java 子项目得到同一份输入边界。
plugins {
id 'java'
id 'checkstyle'
}
checkstyle {
toolVersion = '13.11.0'
configFile = rootProject.file('config/checkstyle/checkstyle.xml')
configProperties = [
'checkstyle.suppressions.file': rootProject.file('config/checkstyle/suppressions.xml').absolutePath
]
}
tasks.withType(Checkstyle).configureEach {
reports {
xml.required = true
html.required = true
}
}先运行 ./gradlew checkstyleMain --info,核对任务输入与报告路径;再运行 ./gradlew check,证明聚合生命周期确实依赖各模块任务。根任务成功而某个 Java 子项目从未注册 checkstyleMain,不能算全仓门禁通过。
规则文件只表达可执行约束
下面的配置把文件级检查留在 Checker,把需要语法树的检查放进 TreeWalker。规则 ID、说明和 owner 另存为团队规则目录,避免把长篇政策塞进 XML 注释。
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<property name="charset" value="UTF-8"/>
<module name="LineLength">
<property name="max" value="120"/>
</module>
<module name="SuppressionFilter">
<property name="file" value="${checkstyle.suppressions.file}"/>
</module>
<module name="TreeWalker">
<module name="AvoidStarImport"/>
<module name="NeedBraces"/>
<module name="TypeName"/>
<module name="MethodName"/>
</module>
</module>规则能否加载要和规则能否命中分开验证。配置解析成功只证明 XML 与模块名有效,不证明 source set、严重度和失败条件正确。
用一个坏样例证明门禁真的会失败
在临时分支增加明确违规,不改正式规则:
package lab;
import java.util.*;
final class bad_name {
void run(boolean ready) {
if (ready) System.out.println("ready");
}
}执行 Maven 或 Gradle 的 Checkstyle 专项任务,预期至少命中 AvoidStarImport、TypeName 和 NeedBraces,进程退出码非零,XML 报告能定位文件、行列和规则。随后修正类名、导入和花括号,再运行同一命令;只有坏样例失败、修正后通过,才证明配置、输入和裁决闭环成立。
若违规出现在控制台但退出码仍为零,先检查 failOnViolation、violationSeverity 与规则严重度。若报告为空,检查源码目录、includes/excludes、生成代码分区和任务是否实际执行。不得用“报告文件存在”代替命中验证。
抑制必须比规则更窄
抑制适合生成代码、协议生成器固定命名或短期迁移,不适合把历史目录整体静音。
<?xml version="1.0"?>
<!DOCTYPE suppressions PUBLIC
"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
"https://checkstyle.org/dtds/suppressions_1_2.dtd">
<suppressions>
<suppress files="GeneratedClient\.java" checks="MethodName"/>
</suppressions>按行号抑制会随代码移动漂移,files=".*" 会吞掉未来新增问题。需要 @SuppressWarnings 时还必须在规则树中配置对应 Holder 与 Filter;源码里出现注解不代表引擎已经识别。每次 suppression 变更都要在评审中展示新增匹配范围、原因、owner 和到期条件,并用一个相邻坏样例证明没有扩大到无关代码。
多模块、缓存与 CI
多模块仓库可以把规则文件做成独立构建制品,也可以放在根目录固定路径。前者适合跨仓复用,必须锁制品版本与摘要;后者便于同仓评审,必须保证子模块从同一根路径解析。生成代码和第三方源码应在 source set 层显式分区,不用大范围 suppression 猜测来源。
本机任务追求快速反馈,CI 负责不可绕过的全量复核。PR 运行主源码规则并保留 XML;主分支补充测试源码或聚合报告;候选规则和新版本先进入非阻断影子任务,比较新增、消失、解析错误和耗时。Checkstyle cache 只减少未变文件的重复分析,不是违规基线;怀疑缓存边界时,用一次无缓存执行与正常执行对照。
版本升级改变的是门禁含义
Checkstyle 新版本可能增加 Token 支持、修改默认属性、删除 Check 或改变违规位置。升级不能只看构建能否启动。旧引擎与新引擎要对同一提交运行,差异按“新增命中、消失命中、位置变化、配置错误”分组;规则文件、插件、引擎和运行 JDK作为一个回滚单元提交。
报告可能包含内部路径、包名和源码片段,按构建制品授权与留存。自定义 Check 是在构建进程内执行的代码,只从受控仓库加载并纳入依赖审查。最终门禁记录应能回答:扫描了哪些模块、用了哪个 JDK/插件/引擎/规则提交、失败发生在哪条规则、抑制为何存在,以及上一组已验证配置如何恢复。
