简介:PMD是一款开源的Java静态代码分析工具,这份压缩包提供了其核心的规则配置XML文件,面向需要在Eclipse等IDE中开展代码质量检查的Java开发者。规则集按设计、代码规模、空值处理、导入规范、finalizers、未使用代码、基础规范等类别划分,覆盖了从潜在Bug、冗余代码到可读性问题的常见检查场景,可通过PMD插件自定义导入并灵活调整参数与排除项。包内共10个文件,以9个XML规则文件和1个说明TXT为主,整体仅35KB,结构清晰、便于直接复用。通过这套规则文件,开发者可以快速建立项目级的代码审查标准,实时定位违反规范的位置,也可作为自定义PMD规则的参考模板,让静态检查更好地贴合团队编码约定。目前已有1056人学习下载,适合希望提升代码质量、落地持续集成的Java开发者。
1. 规则文件才是 PMD 的“灵魂”,别只盯着官方规则列表
给 Java 项目上静态检查,多数人的第一步是打开 PMD 官方规则列表,勾勾选选。真正负责落地的工程师,动手改的第一样东西却是 PMD 的规则文件(ruleset.xml)。同样一套 PMD,默认扫描能报出一堆不痛不痒的命名建议,而一份贴合项目的规则文件,可以只留下空 catch、高危魔法数字、System.out 这类 code review 真正会拦截的问题,十几条规则就能让整个团队的提交质量上一个台阶。规则文件本质是“检查白名单 + 阈值表”,决定 PMD 这个黑匣子吐出来的是噪音还是炮弹。想固化团队规范的人把它当制度文件维护,想搞懂规则的人靠它反推一条检查到底靠什么触发。这篇就把规则文件从头拆到尾。
2. 拆开一个 ruleset.xml:头信息、rule 元素与 properties 三层结构
2.1 根节点、namespace 与 description:规则集的身份信息
任何一个 PMD 规则文件都是 XML,根节点固定为ruleset。见过不少同事直接复制网上的片段,把第一行缩进都改了,结果 PMD 启动就报 schema 解析失败。头三行不是摆设,xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"这个 namespace 决定了 PMD 用哪一代规则集语法来解析文件。PMD 6 和 PMD 7 都兼容这个 2.0.0 命名空间,但内部字段的宽松程度不同,这一点在第 5 章会专门讲。
<?xml version="1.0" encoding="UTF-8"?> <ruleset name="Company-Java-Core" xmlns="http://pmd.sourceforge.net/ruleset/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd"> <description> 核心规范:只保留对上线有影响的检查项,命名类问题一律压低优先级。 </description> </ruleset>name是规则集的对外名字,在 CI 报告和日志里会出现,建议用“公司/小组 + 语言 + 场景”的命名,比如Company-Java-Core。description写清楚这套规则的定位,它会在 PMD Designer 和 HTML 报告里展示,团队新成员先看这段就能理解为什么要有这套规则。schemaLocation里的 URL 只是给 IDE 做校验提示用的,PMD 运行时不联网拉它,所以别因为内网环境去删除这行。
2.2 rule 元素:六个字段各管什么事
规则集的核心单位是<rule>。一个我维护了两年多的最小规则文件里,只放了一条规则,也把六个关键字段都占满了:
<rule name="NoSystemOut" language="java" message="不要直接使用 System.out / System.err,请走 SLF4J" class="net.sourceforge.pmd.lang.rule.XPathRule"> <priority>2</priority> <properties> <property name="xpath"> <value> //PrimaryExpression[PrimaryPrefix/Name[@Image='System.out' or @Image='System.err']] </value> </property> </properties> <example> <![CDATA[ System.out.println("debug: " + x); // 违规 log.info("debug: {}", x); // 符合规范 ]]> </example> </rule>各字段的职责拆开看:
| 字段 | 作用 | 常见取值 |
|---|---|---|
name | 规则唯一 ID,@SuppressWarnings和报告里都用它 | NoSystemOut |
message | 违规时输出的提示语,支持{0}占位符 | 不要使用 System.out |
class | 规则的实现类,决定这条规则怎么扫描 | net.sourceforge.pmd.lang.rule.XPathRule或自定义类的全限定名 |
language | 规则作用于哪种语言,PMD 7 起必填 | java/xml/plsql/apex |
priority | 严重程度,1 最高、5 最低,默认是 5 | 1~5 |
externalInfoUrl | 团队规范 wiki 的链接,报告里会导出 | 公司 Confluence 地址 |
name要当代码里的标识符来取,别用中文也别带空格。message是给人看的,写清楚“为什么不行 + 替代方案”,比 PMD 默认的英文消息有用得多。class若是net.sourceforge.pmd.lang.rule.XPathRule,说明这条规则靠 XPath 表达式匹配 AST 节点;若是自定义 Java 类,就是走访问者模式扫描,两种形态在第 4 章展开。priority不是随便填的——CI 里通常只对 1、2 级规则 fail 构建,3 级以下只出报告。
2.3 properties:规则的参数区,配置最容易出错的地方
properties是规则文件真正值钱的部分。官方规则大多带可调参数,比如AvoidUsingHardCodedIP有ignoreNetmask,EmptyCatchBlock有allowCommentedBlocks。自定义 XPath 规则的xpath属性本质也是一个 property。引用官方规则并覆盖参数,是定制规则文件最常见的动作:
<rule ref="category/java/bestpractices.xml/AvoidUsingHardCodedIP" message="禁止硬编码 IP,测试环境请走配置中心"> <priority>1</priority> <properties> <property name="ignoreNetmask" value="false" /> </properties> </rule>ref指向官方规则集里的规则 ID,格式是“分类路径/规则名”。PMD 6 之后规则按category/java/...组织,不要再用老教程里的rulesets/java/...路径,后者在 PMD 7 里已经失效。覆写参数时注意,property 是“整表替换”,不是增量修改——如果你只想改一个值,最好把原规则在ruleset.xml里用ref引用后把要动的属性完整写出来。
同一套规则文件里,经常出现“同一规则两套阈值”的需求:Web 项目允许方法 5 个参数,核心账务模块只允许 3 个。做法是建两个ruleset.xml,各自ref同一规则并填不同properties,在模块的 pom 或 gradle 里分别引用。规则文件这时候就是配置文件,路径、命名、参数都该走配置管理的评审流程。
3. 从零写一个能跑的最小规则文件:先打通验证链路再谈规则
3.1 最小规则集:只放一条规则,验证文件本身没问题
我搭任何新规则文件的习惯是:先写一条必然命中的规则,确认 PMD 能读文件、能匹配、能输出,再往里面加业务规则。这条“冒险规则”用//CompilationUnit这种匹配任意 Java 文件的 XPath,只要 PMD 正常解析,它必报。
<?xml version="1.0" encoding="UTF-8"?> <ruleset name="Canary-Ruleset" xmlns="http://pmd.sourceforge.net/ruleset/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd"> <description>验证 PMD 管线是否正常的探针规则集</description> <rule name="PipelineCanary" language="java" message="规则链路正常,这条必然触发" class="net.sourceforge.pmd.lang.rule.XPathRule"> <priority>5</priority> <properties> <property name="xpath"> <value> //CompilationUnit </value> </property> </properties> </rule> </ruleset>//CompilationUnit在 PMD 6 和 PMD 7 里都匹配任何 Java 源文件的根节点,所以它一定会触发。文件保存为rulesets/canary.xml,路径自己定,但建议所有规则文件统一放源码库的rulesets/目录,跟随项目走版本管理。
3.2 用 pmd check 命令当场验证:命令和输出怎么看
规则文件是文本,配错了不会立刻炸,要跑一次才见分晓。PMD 6.29 之后的命令行子命令是pmd check,老写法的pmd -R xxx -d xxx在新版本已废弃:
pmd check -R rulesets/canary.xml -d src/main/java -f text参数含义:-R指定规则文件路径,可以逗号分隔多个;-d指定被扫描的目录或单个文件;-f text是纯文本输出格式。如果没配--no-cache,PMD 会在同目录生成.pmd缓存,第二次扫描只增量检查变化文件,调试规则时最容易踩这个缓存坑——改完规则重新跑,结果还是老的。调试阶段我习惯加一个参数:
pmd check -R rulesets/canary.xml -d src/main/java -f text --no-cache正常输出长这样:
src/main/java/App.java:3: PipelineCanary: 规则链路正常,这条必然触发格式是“文件:行号:规则名:消息”。看到这一行,说明规则文件被正确加载、AST 解析成功、输出通道工作正常。如果这条探针规则没报,问题一定出在规则文件或命令行,而不是业务代码。
3.3 挂进 Maven 和 Gradle:让 CI 替你跑规则文件
本地验证通过后,规则文件要在构建链路里生效。Maven 项目用maven-pmd-plugin,配置里最容易漏的是<rulesets>标签不写——不写就只跑官方默认规则集,你的自定义规则一条都不会执行:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-pmd-plugin</artifactId> <version>3.21.2</version> <configuration> <rulesets> <ruleset>rulesets/canary.xml</ruleset> <ruleset>rulesets/company-core.xml</ruleset> </rulesets> <includeTests>false</includeTests> <failOnViolation>false</failOnViolation> </configuration> <executions> <execution> <goals> <goal>check</goal> </goals> </execution> </executions> </plugin>Gradle 侧结构类似,但有个坑:Gradle 自带默认规则集,如果你只想跑自己的规则文件,必须把ruleSets清空:
pmd { ruleSetFiles = files("rulesets/company-core.xml") ruleSets = [] toolVersion = "6.55.0" ignoreFailures = false }ignoreFailures = false表示违规即构建失败。第 6 章会讲渐进式放量,新规则刚上线时这里应该设true,只出报告不阻断。版本号建议锁定:Gradle 的toolVersion不写默认用内置老 PMD,Maven 插件默认带的 PMD 版本也偏旧,团队统一锁版本才能避免“本地报、CI 不报”的玄学问题。
4. 规则文件的两种实现形态:XPath 表达式还是 Java 类
4.1 XPath 规则:适合“看结构”的检查,改起来最快
规则文件里class写成XPathRule时,规则体就是一段 XPath 表达式。PMD 把源码解析成 AST 语法树,XPath 在树上做模式匹配。上一章的NoSystemOut就是一个典型://PrimaryExpression定位所有表达式节点,PrimaryPrefix/Name[@Image='System.out']限定节点内容。
结构类检查用 XPath 性价比极高,比如“禁止单字符字段名”:
<rule name="SingleCharFieldName" language="java" message="字段名不能是单个字符,请用有业务含义的名字" class="net.sourceforge.pmd.lang.rule.XPathRule"> <priority>3</priority> <properties> <property name="xpath"> <value> //FieldDeclaration/VariableDeclaratorId[string-length(@Image) = 1] </value> </property> </properties> </rule>表达式拆开看://FieldDeclaration匹配所有字段声明,VariableDeclaratorId拿到变量名节点,string-length(@Image) = 1过滤出名字长度为 1 的情况。写 XPath 规则最怕“凭直觉写”,强烈建议用 PMD Designer 工具。Designer 能加载你的测试代码,实时展开 AST 树,左侧写 XPath、右侧即时高亮匹配节点,比一遍遍跑pmd check调试快一个量级。
4.2 Java 规则类:适合需要上下文和数据流的检查
XPath 的短板是“看不懂上下文”。比如“魔法数字”规则,字面量60是违规常量,但static final int SECONDS_PER_MINUTE = 60里的60又是合法的——XPath 很难区分这两种场景,Java 规则类可以。自定义规则类继承AbstractJavaRule,重写对应节点的visit方法:
package com.company.pmd.rules; import net.sourceforge.pmd.lang.java.ast.ASTLiteral; import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule; /** * 自定义规则:检测方法体内的魔法数字。 * PMD 6 的 visit 返回 Object;PMD 7 改成 Void,升级时注意签名。 */ public class AvoidMagicNumberRule extends AbstractJavaRule { @Override public Object visit(ASTLiteral node, Object data) { // 只处理数值字面量,字符串和字符不管 if (node.isIntLiteral() || node.isLongLiteral() || node.isDoubleLiteral()) { // 跳过 0 和 1,这两个数字出现太频繁,误报会很高 String image = node.getImage(); if ("0".equals(image) || "1".equals(image)) { return data; } addViolationWithMessage(data, node, "魔法数字 " + image + " 应提取为具名常量"); } return data; } }这个类只做了两层判断:先认字面量类型,再跳过0和1。实际落地时你还需要判断“当前节点是否在静态常量初始化里”“是否在注解参数里”,那些要访问父节点链,ASTLiteral的jjtGetParent()一路往上找。规则类写好后,在规则文件里注册成普通<rule>就行:
<rule name="AvoidMagicNumber" language="java" message="魔法数字应提取为具名常量" class="com.company.pmd.rules.AvoidMagicNumberRule"> <priority>3</priority> <example> <![CDATA[ int total = count * 60; // 违规,60 是魔法数字 int total = count * SECONDS_PER_MINUTE; // 符合规范 ]]> </example> </rule>class写全限定名,规则类的 jar 要打进 PMD 的 classpath。Maven 项目里把这个模块打成依赖,在maven-pmd-plugin的<dependency>里引入;Gradle 里pmd配置块加dependencies { pmd project(':pmd-rules') }。
4.3 抑制机制:NOPMD 和 @SuppressWarnings 的边界
规则文件定得再细,总有“这行我就想这么写”的场景。PMD 给两条后门,但边界要讲清楚。// NOPMD是行级注释,必须放在违规行末尾:
int retry = 3; // NOPMD - 重试次数固定为 3,不需要配置化@SuppressWarnings("PMD.RuleName")是类级或方法级抑制,作用范围是整个代码块:
@SuppressWarnings("PMD.AvoidMagicNumber") public int legacyCompute() { return 86400; // 历史接口,已冻结,不做重构 }注意注解里的规则名必须和<rule name>完全一致。误用抑制是规则文件执行效果变差的第一杀手,我的经验是:// NOPMD必须带一句原因,@SuppressWarnings必须走 code review;否则下个季度回头看,满屏都是抑制注释,规则形同虚设。
5. 规则文件排查:五次“规则不生效”的翻车记录
5.1 Maven 里配了规则文件,CI 却跑的是官方默认规则集
现象:本地pmd check -R能报出违规,CI 构建却一片绿,自定义规则一条没触发。
原因:maven-pmd-plugin的<rulesets>没写,或写在了插件<dependencies>里而不是<configuration>里。插件没拿到规则文件路径,就退回默认的 bestpractices、design 等官方规则集。
解决:把<rulesets>写进<configuration>,同时给pmd:check的执行阶段加<phase>verify</phase>,确认插件在verify阶段确实执行。最直接的办法是用mvn pmd:check -X看调试日志里加载的规则集路径。
5.2 从 PMD 6 升到 PMD 7,XPath 规则集体失灵
现象:升级 PMD 版本后,原本正常的NoSystemOut等 XPath 规则不再报违规,且没有任何报错。
原因:PMD 7 重写了 AST 节点体系。ASTPrimaryExpression、ASTPrimaryPrefix、ASTPrimarySuffix这一组节点被移除,方法调用统一为ASTMethodCall;ASTClassOrInterfaceDeclaration拆成了ASTClassDeclaration和ASTInterfaceDeclaration。旧 XPath 表达式里的节点名在新树上根本不存在,匹配结果为空。
解决:每一条 XPath 规则都用 PMD Designer(新版本)重新对照 AST 树改写。NoSystemOut在 PMD 7 里大致等价于匹配MethodCall节点的QualifiedName属性,但每个 PMD 小版本 AST 定义都可能微调,以 Designer 实际展示为准。升级前在 CI 里加上探针规则做回归对比,能第一时间发现这类静默失效。
5.3 XPath 表达式里的<让 XML 解析直接报错
现象:新增一条“方法行数小于 10”的 XPath 规则,PMD 启动报Content is not allowed in prolog或The element type ... must be terminated。
原因:XPath 里的比较运算符<在 XML 里是非法字符,必须转义。很多人写了//MethodDeclaration[count(...) < 10],<把 XML 解析器搞翻了。
解决:所有小于号写成<。这类规则建议用not(... >= ...)来绕开<,比如行数小于 10 写成not(count(...) >= 10),可读性差一点但避免转义遗漏。写完用xmllint --noout rulesets/xxx.xml先校验 XML 合法性,再跑 PMD。
5.4 properties 里配了数字,规则类读到的是 null
现象:自定义规则类里定义了ignoredNumbers属性,规则文件里<property name="ignoredNumbers" value="0,1,2"/>,运行时getProperty("ignoredNumbers")返回 null。
原因:属性是按声明类型转换的。规则类里没有对这个属性做definePropertyDescriptor声明,PMD 不知道它是什么类型,字符串值进不来。
解决:规则类构造函数里先声明属性描述符:
public AvoidMagicNumberRule() { definePropertyDescriptor( PropertyFactory.stringProperty("ignoredNumbers") .defaultValue("0,1") .desc("不视为违规的数字列表,逗号分隔") .build()); }声明后 PMD 才会把规则文件里的 value 按 string 类型注入。规则文件里配了但读不到,九成是属性描述符没定义,先查构造函数。
5.5 规则改名之后,抑制注解和缓存里的旧名字静默失效
现象:规则从AvoidSystemOut改成NoSystemOut,老代码里的@SuppressWarnings("PMD.AvoidSystemOut")不再起作用,这些代码重新开始报违规。
原因:抑制注解按规则名精确匹配,改名即失效,PMD 不会给任何警告。
解决:改名时全局搜索两个地方:.java里的@SuppressWarnings("PMD.旧名")和行尾的// NOPMD注释。更稳妥的做法是名字定下来就不改,规则名是团队规范的公共 API。另外,PMD 的增量缓存也会存规则签名,规则改名或改参数后,CI 里先mvn clean或删掉.pmd缓存再回归,否则容易在“以为修好了”的状态下误判。
6. 规则文件要进版本库:单测、灰度与一根“保险丝”
规则文件是配置,但它值得像代码一样被测试。pmd-test框架提供RuleTester,对规则文件里的每条规则都能做定点验证。Maven 依赖引入net.sourceforge.pmd:pmd-test后,规则测试写成 JUnit 用例:
public class NoSystemOutRuleTest { @Test public void testDetectSystemOut() { RuleTester tester = new RuleTester(); // 从规则文件加载指定规则 tester.addRule("rulesets/company-core.xml", "NoSystemOut"); // positive:这段代码应该报违规 tester.positive("System.out.println(x);", "不要直接使用 System.out"); // negative:这段代码不应该报违规 tester.negative("log.info(\"x={}\", x);"); } }positive和negative的具体方法签名随 pmd-test 版本有细微出入,落地时以你依赖版本的 javadoc 为准。这套测试的价值在于:规则文件后续任何改动,都能靠测试用例先拦住“把合法代码误判成违规”的回归。
新规则上线不要直接failOnViolation = true。我一般分三步走:第一周只出报告,ignoreFailures = true,把违规清单导出来人工过一遍,误报率高就调阈值或加抑制;第二周把误报率压到 10% 以下,failOnViolation打开但只针对 priority 1、2;一个月后把规则和它的测试用例一起转正。灰度期间看 SARIF 或 CSV 报表,按模块统计违规密度,比看总数更能判断规则是否合理。
最后说一个我保留至今的习惯:每个规则文件里放一条像第 3 章那样的探针规则,priority设 5,永远不删。规矩很简单——如果一次扫描里探针规则没触发,说明 PMD 链路是断的,这份报告不算数。很多团队排查“规则怎么不生效”查了半天,最后发现是缓存、工具链或配置路径出了问题,探针规则能用一条必报项把整条链路钉死。每次接手新项目或升级 PMD 版本,先跑探针,再谈规则优化,这个顺序帮我少翻了不少车。希望帮到你。
本文还有配套的精品资源,点击获取