简介:PMD是开源的静态代码分析工具,用于检测Java源代码中的潜在问题与编码规范偏差。该压缩包提供一套PMD规则文件的XML配置合集,主要面向使用Eclipse PMD插件或对接持续集成流程的Java开发者,可帮助团队建立自定义代码检查策略。压缩包共10个文件,其中9个XML规则文件覆盖基础、空代码、设计、导入、finalizers、未使用代码等常见类别,另附1个说明文本,整体仅35KB,轻量易用。已有1057人浏览学习该资料。通过这些规则文件,读者可快速导入并运行PMD检查,掌握ruleset、rule、category、parameter、exclude等核心配置的作用,理解如何定制阈值、排除特定文件或方法,为日常代码质量管理提供可参考的样板。
1. PMD 的规则文件:先搞清楚它到底管什么
PMD 是 Java 静态代码分析工具里用得比较多的一款,而它的规则文件就是让 PMD 真正干活的指挥中心。这个文件本质上是 XML,里面描述了你希望 PMD 在扫描代码时检查哪些问题:方法太长、空 catch 块、未使用的变量、嵌套过深的 if、System.out.println 调用等等。没有规则文件,PMD 只是一台空转的扫描引擎;有了它,PMD 才能变成符合团队口味的代码警察。这篇笔记适合两类人:一类是用了默认规则集但被误报和漏报困扰的开发者,另一类是准备定制团队规范、打算动手写规则的新手。我会从结构拆到落地,再把我踩过的坑一次讲完。
2. 规则文件的结构拆解:从 ruleset 到 rule 再到 property
2.1 ruleset 根节点:命名空间与 schema 决定你的格式合法性
所有 PMD 规则文件的根节点都是<ruleset>。这个节点上有两个东西决定文件能不能被正确解析:xmlns命名空间和xsi:schemaLocation。PMD 从 6.x 开始统一使用http://pmd.sourceforge.net/ruleset/2.0.0这个命名空间,旧版的1.0.0虽然还能被兼容,但新规则特性不会生效。我一般直接复制自带规则集头部作为模板,而不是手打,因为 schema 的 URL 有大小写和斜杠的坑。
一个最小骨架如下:
<?xml version="1.0" encoding="UTF-8"?> <ruleset name="MyCustomRules" 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.net/ruleset_2_0_0.xsd"> <description>团队自定义规则集,用于业务服务模块的日常扫描</description> </ruleset>这个骨架里name属性会出现在 PMD 的报告中,建议用有业务含义的名字,比如MyServiceRules。description是给人看的,可以写清规则集的适用场景。如果name或者description空着不写,PMD 不会报错,但后续在 IDE 插件或 CI 报告里很难定位规则来源,属于给自己埋雷。
需要注意:schemaLocation里的 URL 在离线环境可能无法访问,但 PMD 在大多数情况下会优先读取本地 schema 兜底。如果出现无法解析 schema 的异常,常见做法是去掉xsi:schemaLocation属性,只保留xmlns。这个细节看起来小,却是我见过的最容易卡住新人的地方。
2.2 rule 节点的核心属性:language、class、message、priority
<rule>是规则文件里唯一真正干活的节点。每个 rule 节点定义一条规则,它有四个最核心的属性,缺一个或者写错一个都会导致规则异常跑不出结果。
name:规则的唯一标识,在同一个 ruleset 内不能重复,最好全局也不重复,否则多 ruleset 合并时可能冲突。language:指定规则作用于哪种语言,Java 规则写java,其他语言同理。PMD 会用它来匹配源代码的语言类型。如果写错,规则会被加载但永远不触发。class:规则实现类的全限定名。PMD 分为 Java 写死的规则和基于 XPath 的规则,后者 class 统一是net.sourceforge.pmd.lang.rule.XPathRule。message:规则触发时报告里输出的提示文本。这个要写成“可执行的建议”,而不是一句评语,比如“检测到空 catch 块,建议至少记录一条异常日志”。
priority不是属性,而是一个子元素。它表示规则严重级别,取值 1 到 5,1 是最严重。很多新手把 priority 当成属性写进<rule priority="3">,在 2.0.0 命名空间下这样写会直接解析失败。正确写法是独立的子标签:
<rule name="AvoidEmptyCatch" language="java" class="net.sourceforge.pmd.lang.rule.XPathRule" message="空catch块建议至少加一条日志"> <priority>3</priority> </rule>如果你想引用 PMD 自带的 Java 规则,class 属性会指向那个规则的具体实现类。常见做法是打开 PMD 自带的规则集 XML,复制一条现有 rule 节点,再改 name 和 message。这样做最稳,因为你不用记类的全限定名,PMD 自带规则集就是最好的模板库。
2.3 property 与 properties:参数怎么喂给规则实现
很多规则不是写死的,它们通过<property>子元素接收运行参数。例如一条限制方法行数的规则,会有一个max或threshold属性。属性定义有两种:一种写在<properties>容器里,另一种直接作为<rule>的子元素。我推荐统一用<properties>包裹,这样结构更清晰,也方便在多个规则间复制。
<rule name="ExcessiveMethodLength" language="java" class="net.sourceforge.pmd.lang.java.rule.design.ExcessiveMethodLengthRule" message="方法过长,请拆分子方法"> <priority>3</priority> <properties> <property name="max" value="150"/> <property name="ignoreComments" value="true"/> </properties> </rule>逻辑说明:这里max=150表示方法行数上限,ignoreComments=true表示不计注释行。修改参数时先看规则文档,再对照 XML 里 property 的类型。如果规则类是自定义的 Java 类,property 会被反射方式灌入同名 setter 方法;如果没找到对应 setter,PMD 会抛配置异常,这属于比较隐蔽的配置错误。
常见的 property 类型有 int、boolean、String、甚至正则表达式。写布尔属性时只认true/false,不要写1/0;写数字属性时 value 必须是纯数字,不能带单位。如果规则文档没有写清类型,最快的方式是在 PMD Designer 里打开这条规则,它会显示属性表单和默认值。
2.4 规则集引用:include、exclude、ref 组合出你的专属规则包
大型项目里你不会只有一个规则文件。常见做法是编写一个“主入口 ruleset”,通过<include>和<exclude>把其他规则文件或规则组合进来。例如主规则集只引用你需要的几个分类,并排除掉不适合的某条规则:
<ruleset name="MainRules" xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"> <include path="category/java/design.xml"/> <exclude name="ExcessiveMethodLength"/> <rule ref="category/java/errorprone.xml/AvoidAssertAsIdentifier"/> </ruleset>参数说明:include的path是相对于 PMD 库内规则的路径。exclude的name要写被包含规则集里规则的name,不是文件名。rule ref可以直接引用另一个 ruleset 文件,甚至用ruleset.xml/规则名引用其中某一条。注意:直接用rule ref引入规则时,被引用的规则里的<priority>会被保留,除非你在引用处覆盖。覆盖方式是在<rule ref="...">内再写<priority>,这个特性常用于统一团队告警级别。
这个结构看起来简单,但实际写起来碰到最多的就是路径和名字匹配问题。我遇过的情况是:include 的 path 写错时 PMD 不报错,只是静默跳过,导致你以为规则生效了,实际上扫描结果里一条相关告警都没有。所以引用外部规则集一定要先跑一次扫描并确认规则数量,而不是“写完就当它生效了”。
3. 手写一个可运行的规则文件:从零到告警的最小路径
3.1 准备一个最小 XML 骨架并放置在正确位置
假设我们的目标是:写一个规则文件,检查 Java 代码中不允许出现System.out.println调用。这是一个非常典型的练习,也是新团队最常用的“打招呼”规则。首先创建文件custom-rules.xml,放到项目根目录或rules目录下。我建议放在rules/子目录,这样和源码、CI 配置分开。
骨架如下:
<?xml version="1.0" encoding="UTF-8"?> <ruleset name="NoPrintlnRules" 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.net/ruleset_2_0_0.xsd"> <description>禁止在业务代码中使用System.out.println,请使用统一定义好的Logger。</description> <rule name="NoSystemOutPrintln" language="java" class="net.sourceforge.pmd.lang.rule.XPathRule" message="检测到System.out.println,请使用Logger"> <priority>2</priority> <properties> <property name="xpath"> <value><![CDATA[ //PrimaryExpression[PrimaryPrefix/Name[@Image='System.out.println']] ]]></value> </property> </properties> </rule> </ruleset>这里使用了 XPath 规则。//PrimaryExpression表示在 AST 中查找所有主表达式,通过限定 PrimaryPrefix 的 Name 的 Image 等于System.out.println来匹配具体调用。CDATA 包裹是必须的,因为 XPath 里会有[]等符号,不包的话 XML 解析器会误判断。
文件编码必须是 UTF-8,尤其当 description 里写了中文时。直接用系统记事本另存为可能会带上 BOM 头,PMD 解析时会把 BOM 当作字符导致根节点声明错误。我遇到过一次,整个规则文件被加载失败,报错信息却是“Premature end of file”,排查了很久才想到是编码问题。
3.2 理解 XPath 规则与 Java 规则的区别
上面这种直接写 XPath 的规则,优点是无需编译 Java 代码,修改规则后立刻能扫;缺点是比较难处理复杂的上下文逻辑,比如“只有捕获了异常却不记录的 catch 块才告警”这种,用 XPath 写会非常痛苦。PMD 内部把这两种规则分别叫作“XPath 规则”和“Java 规则”。如果是正经要做成产品级规范,我建议优先用现成的 Java 规则类,再不行就用 XPath 规则,最后才考虑写自定义 Java 规则类。
这里我补充一个选型理由:XPath 规则适合“结构匹配”,比如禁止某个类型的方法调用、禁止某种嵌套层级;需要分析数据流、跨方法状态时,XPath 就无解了,必须写 Java 规则。作为团队规范的第一版,XPath 规则能覆盖 80% 的“禁止某写法”诉求,而且没有编译门槛,改完 XML 再跑一遍 PMD 就能看到效果。
如果你决定写自定义 Java 规则,那你需要额外建一个 Java 工程,编译出 jar 包,再通过pmd命令的-auxclasspath参数把它加载进来。这会引入不少构建复杂度,所以我建议新手不要一上来就写 Java 规则,先把 XPath 规则玩熟,再逐步过渡。
3.3 在命令行和 Maven 里把规则文件跑起来
有了规则文件,下一步就是用 PMD 执行它。以命令行方式为例,假设项目目录是src/main/java,规则文件在rules/custom-rules.xml:
pmd check -d src/main/java -R rules/custom-rules.xml -f text参数说明:-d指定扫描目录,-R指定规则文件路径(可以是文件或目录),-f指定输出格式,text适合本地调试。如果pmd命令不在系统 PATH 里,可以用完整路径。需要注意:-R指向规则文件时,PMD 会读取文件内所有规则;如果你给了个目录,则会加载目录下所有 XML。
在 Maven 工程里,通常通过maven-pmd-plugin插件执行。配置示例:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-pmd-plugin</artifactId> <configuration> <rulesets> <ruleset>rules/custom-rules.xml</ruleset> </rulesets> <includeTests>false</includeTests> <failOnViolation>true</failOnViolation> </configuration> </plugin>插件版本请用你项目里已验证过的版本,不要随手抄一个最新版本号。failOnViolation=true表示只要 PMD 发现违规,Maven 构建就失败,这是做门禁的基本姿势。includeTests=false表示不扫描测试目录,因为测试代码里用 println 可能情有可原。
3.4 用一段触发代码验证规则真的生效
写完规则后必须验证。我们准备一个最简单的 Java 文件放在临时目录:
public class Demo { public void doSomething() { System.out.println("hello"); } }执行上面命令行后,预期输出里会有NoSystemOutPrintln的告警,并且 message 内容是“检测到System.out.println,请使用Logger”。如果没输出,不要急着改代码,先确认规则文件路径是否对、XML 是否合法。我一般会先执行pmd check -d src/main/java -R rules/custom-rules.xml -f text看有没有异常堆栈。如果规则文件有语法错误,PMD 通常直接抛异常;而如果规则文件合法但规则没匹配到,输出为空。这时可以用一个已经知道会触发问题的代码去测试,这就是最小验证。
这个流程走通后,我们才算真正拥有了一条自定义规则。下一步要考虑的是:为什么我们写了 XPath 却不生效?这里涉及 PMD 的 XPath 版本差异,我在避坑章单独讲。
4. PMD 规则文件避坑指南:5 个让我翻车的细节
4.1 坑 1:language 属性写错,规则静默失效
现象:规则文件加载正常,扫描执行不报错,但就是不出结果。
原因:PMD 根据language属性决定这条规则注册到哪个语言的规则引擎里。java写成了JAVA或者拼成jav,都会被归入错误的语言。PMD 的语言代码是小写,例如java、xml、ecmascript,写错后规则虽然被加载,但匹配不到任何 AST 节点。更坑的是,PMD 在早期版本不会对 language 做严格校验,静默失败。
解决:先确认你要扫描的源码语言,然后在规则文件里显式写明language="java"。不要依赖目录名或文件后缀去推断,因为规则文件是独立的,它不会自动继承。排查时可以在同一个 ruleset 里临时加一条内置规则作对照,看内置规则是否有输出。如果内置规则有输出而自定义规则没有,大概率就是 language 或 XPath 的问题。
4.2 坑 2:XPath 版本差异导致规则行为不一致
现象:同一段 XPath 在 PMD 6.x 的某个版本里生效,升级到新的主版本后突然失效或者报错。
原因:PMD 在大的主版本升级时会调整 AST 表示,特别是方法调用相关的节点类型会有变化。旧版PrimaryExpression可能在新版中被拆成多个节点类型,导致原来能匹配的 XPath 不再命中。
解决:先明确团队固定在哪个 PMD 主版本,然后只参考对应版本的文档和内置规则。调试 XPath 时打开 PMD 自带的 Designer 工具,粘贴触发代码,查看当前版本真正的 AST 节点名称,再据此调整表达式。我日常习惯是:先在旧版环境验证规则,再在目标版本跑一遍测试样例。如果团队里同时存在多个 PMD 大版本,规则文件必须单独维护,不要指望一份文件通吃所有版本。
4.3 坑 3:priority 范围搞混,告警级别不生效
现象:规则能触发告警,但严重级别没有出现在结果里,或者 CI 过滤条件匹配不上。
原因:PMD 的priority取值是 1 到 5,数字越小越严重。很多人按直觉认为 1 是低严重级别,把普通规则写成 1,导致 CI 认为所有文件都阻断构建;反过来,把该阻断的规则写成 5,它又不会阻止合并。
解决:先定义团队的严重级别映射,比如 priority 1-2 为阻断级,3 为警告级,4-5 为建议级,并写进规则文件里的 description。再检查 XML 中<priority>3</priority>是否被正确解析。注意:如果你想在引用内置规则时覆盖 priority,写法是:
<rule ref="category/java/design.xml/ExcessiveMethodLength"> <priority>1</priority> </rule>这里 priority 是rule ref的子元素,而不是属性。写错位置会导致覆盖不生效。
4.4 坑 4:外部规则集引用路径解析失败
现象:<include path="category/java/design.xml"/>加载后,扫描报告里只有零条规则,或者报 “Resource not found” 异常。
原因:path属性的解析基础是 PMD 内部规则目录,而不是你的项目文件系统。直接写path="rules/design.xml"会找不到。同样的,exclude的 name 必须和规则定义里的 name 完全一致,大小写、空格都不能差,否则排除不了。
解决:include 这种机制更多用于“包含 PMD 自带分类规则集”。如果想引用项目内另一个规则文件,应该使用rule ref="rules/other.xml"形式,或者用file:前缀指向绝对路径。我习惯把所有自定义规则放到同一个文件里,减少跨文件引用。如果你有很多规则需要拆分,建议用rule ref而不是include,因为 ref 的路径相对当前规则文件,而 include 相对 PMD 的类路径。
4.5 坑 5:property 类型配置错误,规则启动即失败
现象:PMD 启动时报错Cannot set property 'max'或者NumberFormatException。
原因:规则实现类定义了 int 类型的 setter,XML 里 property 的 value 却是空字符串或非数字。另一个常见情况是布尔属性写成"true"和"1",PMD 只认true/false字符串,写1会解析失败。
解决:查阅规则文档确认属性类型。遇到诡异错误时,把 property 写完整,包括<value>子元素形式,因为部分规则需要的是元素内容而不是value属性值。两种写法如下:
<property name="max" value="10"/> <!-- 等价写法 --> <property name="max"> <value>10</value> </property>这两种写法在绝大多数情况下可互换,但某些自定义规则或旧版规则只实现了对其中一种的解析。搞不定的时候换成另一种试试,能节省半天排查时间。
如果你用的是自定义 Java 规则类,property 名称会映射到该类的 setter 方法。例如<property name="max" value="10"/>会去找setMax(int)方法。如果类里没有这个方法,PMD 会直接抛异常。所以自定义规则时,property 的 name 必须与 setter 命名严格一致,这是最容易忽略的细节。
5. 把规则文件变成团队规范:增量落地与自检技巧
5.1 用“基线法”控制规则文件引入的告警量
直接全量开启一批新规则,团队基本会炸。我采用“基线法”:先在目标项目上跑一遍新规则文件,把现有告警全部记录为基线快照,之后只允许新增告警出现。具体做法是第一轮扫描结果存档为基线文件,CI 中对比当前输出与基线,只对超出的部分报错。规则文件本身在这种模式下很稳定,团队能逐步消化历史技术债,而不是一次性爆发几千条 warning。
5.2 为规则文件单独建立测试用例
规则文件也是代码,需要测试。常见的做法是用 PMD 自带的扩展测试框架,但如果你不想引入额外依赖,可以准备一组小 Java 文件,分别命名为“触发”和“不触发”,然后用脚本断言扫描结果的差异。例如下面的 bash 脚本:
pmd check -d ./trigger -R rules/custom-rules.xml -f text | grep -q "NoSystemOutPrintln" && echo "TRIGGER PASS" pmd check -d ./not_trigger -R rules/custom-rules.xml -f text | grep -q "NoSystemOutPrintln" && echo "TRIGGER FAIL"注意:这不是为了自动化而自动化,而是防止你改了一个规则的 XPath 后,把另一个不该触发的情况放大。给规则文件配套测试,是我维持稳定规则集的关键习惯。当然,如果团队规模不大,也可以用小型 CI job 来跑这套脚本,不必上完整单测框架。
5.3 用 PMD Designer 快速调试 XPath 规则
如果你手里只有规则文件,没有复杂调试环境,可以启动 PMD 自带的 Designer 组件。它的作用是:粘贴一段 Java 代码,查看该代码对应的 AST 树,再编写和验证 XPath 表达式。这比“改了扫描再翻报告”快得多。我通常在处理一个复杂规则前,先用 Designer 复制触发代码,确认 AST 节点名称,再写 XPath,最后贴回规则文件。这一步能避开不少前面提到的 XPath 版本坑。
另外,规则文件会随着时间膨胀,我建议每个规则节点都写清description,包含这条规则的业务背景。这样多人维护时,不会出现“看了 message 不知道这条规则在干嘛”的情况。规则文件本身不值得追求大而全,我见过维护了上千条规则的团队,最终大部分被 disable,不如用几十条核心规则配合基线机制来得有效。
最后说一个我的教训:以前为了追求“零告警”的指标,我把规则文件里的 priority 都改成了 4,结果整个团队开始无视告警。后来我重新把 priority 调回真实严重级别,并用基线法冻结历史告警,才让规则文件重新被信任。规则文件不是死的清单,它是你和团队关于代码质量的共识。希望帮到你。
本文还有配套的精品资源,点击获取