☰
PMD 静态代码分析规则文件配置与 CI 流水线落地指南
2026/10/27 10:47:02 网站建设 项目流程

简介:面向Java开发者的PMD静态代码分析规则配置包,适合希望在Eclipse中实施编码规范、排查潜在缺陷与代码坏味道的初中级开发者使用。包内共10个文件,以9个xml规则配置为主体,另有1个txt说明文档,压缩包约35KB,体积轻便,可直接导入;xml按设计、代码规模、空代码块、冗余导入、未使用代码、finalizer等主题分文件组织,便于按需启用或裁剪。

规则内容覆盖ruleset、rule、category、param与exclude等核心标签,读者可据此理解每条规则的ID、实现类、描述与阈值参数,并学会针对特定路径或方法排除检查。借助这些文件,团队可以在Eclipse的PMD插件中加载自定义规则集,把编码约定固化为可执行的检查项,也可作为接入Jenkins、GitLab CI等持续集成流水线的规则基础,让每次提交自动完成扫描。目前已有1059人学习下载。

1. 第一次把 PMD 挂进流水线,八成会翻在规则文件上

PMD 这套东西的核心不在那个 jar 包,而在ruleset.xml。规则怎么组合、阈值改到多少、哪些规则必须关掉,全在这几个 XML 里。我见过太多团队,构建脚本里加一行pmd:check就算接入了,第一次跑出几千条告警没人认领,第二天插件就被注释掉。装 PMD 本身十分钟,把规则文件调成团队能长期跑下去的样子,才是真正耗时间的地方。这套东西适合已经能跑构建、想给 Java 代码加一道检查闸门的开发者;也适合接手了别人留下的pmd.xml、打开一看全是注释掉的规则、改也不敢改的人。

2. 装 PMD 之前先定版本:JDK 17 与 6.55.0 的分岔口

PMD 的规则文件是跨版本兼容性最差的部分之一,规则名会改名、规则集路径会搬家、连自定义规则的类名都会换包。所以动手装之前,版本这道坎必须先迈过去,不然后面写多漂亮的 XML 都是白搭。

2.1 先看构建机的 JDK,再决定装 6 还是 7

这里有个特别容易混的点:被分析代码的 Java 版本,和跑 PMD 本身的 JDK 版本,是两回事。构建机上java -version出来是 8,那 PMD 选型基本就被钉死了;出来是 17 或 21,才有得挑。

PMD 7 起,运行环境要 JDK 17,这是硬门槛,JDK 8 上启动直接抛UnsupportedClassVersionError。PMD 6.55.0 是 6.x 系列最后一个版本,JDK 8 上还能正常跑,被分析的代码从 Java 5 到 Java 20 都能覆盖。很多老项目构建机还是 8 或 11,硬上 7 只会把 CI 弄挂,老老实实停在 6.55.0 更省事。

构建机 JDK建议版本原因
8 / 116.55.07 起无法启动,规则集路径也还是老的rulesets/java/
17 / 217.x规则集统一走category/java/,CLI 改成pmd check子命令
两套构建机并存6.55.0 过渡规则文件按 6 的写法维护,7 的规则名差异不兼容
新项目、无历史包袱7.x少一套历史规则名,后面不用做迁移

表里第三行是血泪经验。同一个仓库、两套构建机、两个 PMD 版本,规则文件里写category/java/errorprone.xml/EmptyCatchBlock,6 能认,7 也能认,看着岁月静好;但一旦有规则在 7 里改了名,6 的构建过了、7 的构建挂了,排查时你会怀疑人生。所以要么统一,要么就按低版本写。

提示:PMD 6.55.0 里rulesets/java/quickstart.xml和category/java/quickstart.xml两种路径都能解析,但 7 的文档统一用后者。新写的规则文件,前缀一律用category/java/,别再用rulesets/。

2.2 三种安装路径,选一种就别再混着用

PMD 的安装方式主要有三种:发行包、包管理器、构建插件内嵌。真正踩坑的场景往往不是“怎么装”,而是“装了两份,跑的不是同一份”。

发行包最直接,解压就能用,适合要在命令行上反复调规则文件的人:

# 解压到统一目录,版本号跟着文件名走,方便多版本共存 unzip pmd-bin-7.0.0.zip -d /opt/ # 建议用环境变量而不是硬编码路径,脚本里好复用 export PMD_HOME=/opt/pmd-bin-7.0.0 export PATH="$PMD_HOME/bin:$PATH" # 确认版本和 JDK 是否匹配 pmd --version java -version

这几行里值得说的是PMD_HOME。很多脚本直接写/opt/pmd-bin-7.0.0/bin/pmd,半年后升级版本,全仓库搜替换。用变量管起来,升级只改一处。另外pmd --version在 7 里是可行的,6 里对应的入口是run.sh pmd --version,两个版本的启动脚本名字不一样,这也是从 6 迁到 7 时最先撞上的差异。

包管理器那条路适合本地开发机:macOS 上brew install pmd,Linux 上多数发行版仓库里的 pmd 版本偏旧,装完记得pmd --version核一下,别在不合预期的版本上调规则。

构建插件内嵌(Maven / Gradle)是团队协作里最常见的方式,好处是版本跟着pom.xml走,所有人跑的是同一份 PMD。代价是插件默认绑定的 PMD 版本可能和你以为的不一样,必须用pmdVersion显式锁死,否则今天 6.55,明天依赖一升变成 7.x,规则文件直接解析失败。

2.3 装完先跑一次默认规则集,把基线拿到手

装完别急着写自己的ruleset.xml,先用官方默认规则集跑一遍,看看这个项目现在的告警量级。这一步决定了后面规则文件该激进还是该保守。

PMD 7 的命令:

pmd check \ -d src/main/java \ -R category/java/quickstart.xml \ -f text \ --no-progress

PMD 6 的命令长这样,注意没有check子命令:

$PMD_HOME/bin/run.sh pmd \ -d src/main/java \ -R rulesets/java/quickstart.xml \ -f text

参数逐个说清楚:-d是被分析的源码目录,可以写多个;-R是规则集,这是整篇文章的主角,后面几章全在讲它;-f是输出格式,调规则的时候用text最顺手,接 CI 看板换成xml或html,7 里还有sarif;--no-progress只是关掉进度条,让日志干净一点,在 CI 里跑的时候很有用。

跑完看结果。上千条告警的,说明这项目从来没做过静态检查,规则文件必须从“少而准”起步,别一上来全量;几十条甚至个位数的,可以直接上完整类别。这个基线数字我一般会记在规则文件的<description>里,写清楚“当前基线 X 条,目标三个月内降到 Y 条”,后来人一看就知道这套规则是为什么这么配的。

3. 规则文件怎么写:从根元素到 rule ref 的四种颗粒度

规则文件不是配置文件,是一份“规则清单”,它决定 PMD 加载哪些规则、给什么优先级、带什么参数。写得好不好,直接决定这套检查能不能活过第一周。

3.1 一个能跑的最小骨架

先看结构。所有自定义规则集都长这个样子:

<?xml version="1.0" encoding="UTF-8"?> <ruleset name="team-java-rules" 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>团队 Java 规则集,基线 87 条,目标 30 条以内</description> <!-- 引用单条规则:最细的颗粒度 --> <rule ref="category/java/bestpractices.xml/UnusedLocalVariable"/> <!-- 引用整个类别,再用 exclude 摘掉不要的 --> <rule ref="category/java/codestyle.xml"> <exclude name="ShortVariable"/> <exclude name="LongVariable"/> </rule> </ruleset>

几个关键元素:根节点ruleset的name属性随便写,但xmlns那一行不能省,命名空间写成2.0.0,从 PMD 5 一直沿用到现在,改成1.0.0会解析失败。xsi:schemaLocation指向官方 xsd,写上是给自己看的,编辑器里能补全,写错了也不影响运行,但要注意行尾别被格式化工具拆坏。

<description>不是装饰。规则文件被改过几轮之后,没人记得某条规则为什么被 exclude 掉,把原因写在 description 或者规则上方的注释里,是给三个月后的自己留后悔药。

3.2 引用粒度:整包、单条、排除、覆盖

<rule ref="...">有四种写法,颗粒度从粗到细:

第一种,引整包。<rule ref="category/java/errorprone.xml"/>,一次性把 errorprone 类别下所有规则拉进来。省事,但类别里往往夹着互相打架或者对本项目毫无意义的规则,全量引入的结果就是告警爆炸。

第二种,引单条。<rule ref="category/java/errorprone.xml/EmptyCatchBlock"/>,最后一个斜杠后面是规则名。这是最常见也最推荐的写法,每条规则的出现都是有意识的决定。

第三种,排掉。引了一整包之后,用<exclude name="规则名"/>摘掉不想要的。注意 exclude 里的名字不带路径前缀,只写规则名本身。

第四种,引用并覆盖。同一份规则被多个规则集引用时,可以就地改优先级和参数:

<rule ref="category/java/errorprone.xml/EmptyCatchBlock"> <!-- 优先级 1 最高,5 最低;这里降级,避免阻塞构建 --> <priority>3</priority> <properties> <!-- 允许 catch 块里只有注释的写法 --> <property name="allowCommentedBlocks" value="true"/> </properties> </rule>

<priority>这个元素容易被忽略。PMD 一共五档,1 是最高级别,很多团队把阻塞构建的规则定在 1~2,其余的放 3~4,跑报告不跑阻断。这个分层比“把所有规则都设成最高”有用得多,后者只会逼着大家把构建步骤注释掉。

3.3 properties 覆盖:把阈值改成团队能接受的数

规则文件真正有含金量的部分,是<properties>。PMD 的规则大多带参数,默认值往往偏严,直接用会误报一片。

几个常用参数:

规则参数默认建议
CyclomaticComplexitymethodReportLevel10老项目先放 15,再逐季度收
NPathComplexityminimum20030 人以上团队建议 300 起步
ExcessiveMethodLengthminimum100按行数算,150 更贴合实际
TooManyMethodsmaxmethods1010 确实太低,15~20 更常见
CommentRequiredmethodCommentRequirementrequired新项目 required,老项目改 allowed

改法就是套一层 properties:

<rule ref="category/java/design.xml/CyclomaticComplexity"> <properties> <!-- 圈复杂度阈值,团队第一个季度先用 15 --> <property name="methodReportLevel" value="15"/> <property name="classReportLevel" value="80"/> </properties> </rule>

参数名拼错是最隐蔽的坑,后面避坑章节会单独说。这里先记住一条:value永远是字符串,写value="true"而不是value=true;数字型参数写value="15",PMD 自己会转,写成value=15在某些版本上会解析出诡异结果。

3.4 include-pattern 与 exclude-pattern:别让生成的代码污染报告

规则文件里还能控制“哪些文件不参与检查”,这个在真实项目里几乎是必需的——protobuf 生成类、JAXB 生成的实体、老系统的legacy包,这些地方扫出来的东西没人会改。

<ruleset name="team-java-rules" ...> <!-- 规则集级别的排除,路径是相对于分析目录的正则 --> <exclude-pattern>.*/generated/.*</exclude-pattern> <exclude-pattern>.*/legacy/.*</exclude-pattern> <rule ref="category/java/errorprone.xml/EmptyCatchBlock"> <!-- 规则级别也能加,作用范围更小 --> <exclude-pattern>.*/test/.*</exclude-pattern> </rule> </ruleset>

两个注意点:路径分隔符统一用/,Windows 上也写正斜杠;正则默认是整段匹配,不是包含匹配,所以两头要加.*。我第一次写generated想当然以为能匹配,结果一条没排除出去,白排查了半小时。

4. 把规则文件接进构建:Maven、命令行和 XPath 自定义

规则文件写好了放在仓库里,接下来是让它真正跑起来。三种接入方式各有各的配置项,配错的地方也各不相同。

4.1 maven-pmd-plugin:三个必配项

Maven 项目里最常见,配置写进pom.xml的build/plugins:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-pmd-plugin</artifactId> <version>3.21.0</version> <configuration> <!-- 1. 锁死 PMD 版本,别依赖插件默认值 --> <pmdVersion>6.55.0</pmdVersion> <!-- 2. 指向仓库里的规则文件,路径相对项目根目录 --> <rulesets> <ruleset>config/pmd/ruleset.xml</ruleset> </rulesets> <!-- 3. 失败策略:先只报告不阻断,跑顺了再打开 --> <failOnViolation>false</failOnViolation> <printFailingErrors>true</printFailingErrors> <targetJdk>17</targetJdk> <includeTests>false</includeTests> </configuration> </plugin>

pmdVersion是最该显式写的一项。插件的默认 PMD 版本跟着插件自己走,你升级插件,PMD 版本也跟着跳,规则文件里的规则名可能就对不上了。锁死之后升级是主动动作,可控。

rulesets支持多个<ruleset>并列,比如把团队规则和框架专用规则分开维护,构建时一起加载。

failOnViolation建议第一次先设false,跑一轮看看总量,把规则调顺了再打开。直接开阻断,第一次构建必挂,然后就有人去改pom.xml了。

Gradle 那边配置更短:

pmd { toolVersion = '6.55.0' ruleSets = ['config/pmd/ruleset.xml'] ignoreFailures = true }

ignoreFailures对应 Maven 的failOnViolation,逻辑反过来,注意别搞混。

4.2 命令行调规则:几个能省时间的参数

写规则文件的时候,Maven 那一套太慢,改一行跑一次构建,一小时改不了几条。命令行直接跑单文件最快:

pmd check \ -d src/main/java/com/example/service \ -R config/pmd/ruleset.xml \ -f text \ --cache \ --no-progress \ --minimum-priority 3

--cache值得单独说。PMD 会把分析结果缓存在.pmd-cache之类的文件里,只重跑改动过的文件。规则文件改过之后,缓存里的结论就失效了,这时候要加--no-cache强制全量,否则你会看到“明明加了规则,怎么不报”——这个坑后面还会提。

--minimum-priority 3表示只输出优先级 3 及以上的告警,等价于过滤掉低优先级噪音。6 里这个参数叫-min,7 才改成现在这个写法。

另外-R是可以写多个的,逗号分隔:

pmd check -d src/main/java \ -R config/pmd/ruleset.xml,category/java/security.xml \ -f text

调规则初期我经常这么干:项目规则文件 + 一个官方类别,快速试某类规则到底会报多少。

4.3 自定义 XPath 规则:把团队约定写成规则

规则文件里最值钱的部分,是能把“我们团队的约定”变成可执行的规则。PMD 支持用 XPath 直接写针对 AST 的检查,写在同一个 ruleset 文件里:

<rule name="NoSystemOutInService" language="java" message="Service 层禁止直接使用 System.out,请改用日志框架" class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"> <description>团队约定:业务代码统一走日志门面</description> <priority>2</priority> <properties> <property name="xpath"> <value><![CDATA[ //MethodCall[ ./MethodName[@Image = 'println' or @Image = 'print'] and ../../..[@Image = 'Service'] ] ]]></value> </property> </properties> </rule>

几个要害:class属性指向XPathRule,PMD 6 的中后期版本和 7 用的是net.sourceforge.pmd.lang.rule.xpath.XPathRule,更早的 6.x 是net.sourceforge.pmd.lang.rule.XPathRule,少一段xpath,写错会抛ClassNotFoundException。xpath属性必须用CDATA包起来,否则 XPath 里的<、&会和 XML 语法打架。language写java,PMD 7 里语言名没变,但如果要写 Kotlin 之类的得换成对应语言标识。

自定义规则的 XPath 需要对着 AST 写,调试方式是把-f xml的输出拉出来,对着实际的节点结构改。这块上手有门槛,但一条调好的自定义规则,抵得上十条通用规则。

5. 排查:规则文件加载失败的五个现场

规则文件出问题,报错信息往往很含糊,PMD 在这一块不太友好。下面五条是按出现频率排的。

5.1 现象:RuleSet not found,可文件明明在那

现象:命令行报找不到规则集,路径肉眼可见是对的,ls也能看到文件。

原因:多数是相对路径的基准目录不对。-R config/pmd/ruleset.xml是相对当前工作目录算的,在target目录里被 Maven 调用时,基准目录变了,就找不到了。另一种情况是规则文件放在src/main/resources下,打包后在 classpath 里,命令行按文件路径找必然失败。

解决:命令行场景一律用绝对路径或者file:前缀;Maven 场景确认rulesets里的路径是相对项目根目录的,不是相对pom.xml所在子模块目录。多模块项目里这一条特别容易错,父pom.xml里配的规则文件路径,到子模块执行时就失效了,常见做法是把规则文件放到父模块,路径写成${maven.multiModuleProjectDirectory}/config/pmd/ruleset.xml。

5.2 现象:规则名报Cannot resolve rule

现象:规则集解析到某一行就挂,提示某个规则名找不到,但这条规则在文档里明明存在。

原因:版本对不上。PMD 7 重命名和删除了一批规则,比如UnusedPrivateField这类常见规则基本还在,但一些设计类、命名类的规则改动不小。更隐蔽的是类别搬家,某条规则从codestyle挪到design,名字没变但路径变了。

解决:用pmd check --help或者官方的规则索引,确认当前版本下的完整路径。最省事的办法是在发行包的lib目录下找规则定义文件,直接搜规则名,路径一目了然。如果近期从 6 升到 7,建议拿 6 的规则文件逐条比对,别指望自动兼容。

5.3 现象:properties 写了,但完全没生效

现象:把methodReportLevel从 10 改成 15,跑出来的告警数量一条没少。

原因:三种可能。一是参数名拼错,PMD 不报错,默默忽略;二是value的类型不对,布尔型参数写成value="yes";三是这条规则被两个规则集同时引用,后加载的覆盖了你的配置。

解决:参数名以官方规则文档为准,改完先跑-f xml看输出里规则的实际参数值。另外检查构建里是不是有多个<ruleset>,后加载的会覆盖前面的同名规则。这一条我踩过,项目规则文件里明明关了某条规则,构建里还是报,最后发现pom.xml里除了自己的规则文件,还挂着一个官方类别,把规则又加回来了。

5.4 现象:改了规则文件,构建结果纹丝不动

现象:规则文件里加了一条新规则,重新构建,告警一条没多。

原因:Maven 增量构建没重跑 PMD,或者 PMD 自己的分析缓存命中了。

解决:先mvn clean再跑一次;命令行场景加--no-cache;CI 环境里建议把缓存目录排除掉,或者用构建 ID 做缓存键,别跨构建复用。这个坑在本地调试阶段最耽误时间,明明规则写对了,就是看不到结果。

5.5 现象:中文注释的行,告警报得莫名其妙

现象:某几个文件报的违规位置完全对不上,行号偏移,或者把注释内容当代码报。

原因:文件编码不是 UTF-8,PMD 按默认编码读,读到一半就乱了。老项目里 GBK 编码的 Java 文件不少见。

解决:命令行加-e UTF-8显式指定编码,或者把-e写成项目实际编码;Maven 里用<sourceEncoding>或者project.build.sourceEncoding统一。更彻底的做法是把项目里所有源码统一成 UTF-8,一次性解决,后面接别的静态分析工具也不用再折腾这一遍。

6. 验证跑通:用一个违规样本,逐条对规则文件

规则文件写完,别急着往流水线上推。我的习惯是先写一个“故意违规”的样本类,逐条对输出,确认每条规则都真的生效了。这个动作花五分钟,能省掉后面几周的扯皮。

样本长这样,放在一个临时目录里:

package demo; import java.util.List; public class RuleSample { private int unusedField; // UnusedPrivateField public void parse(String input) { if (input == null) { } // EmptyIfStmt try { Integer.parseInt(input); } catch (Exception e) { } // EmptyCatchBlock System.out.println(input); // 自定义规则 NoSystemOutInService } }

跑一遍:

pmd check \ -d /tmp/pmd-sample/demo \ -R config/pmd/ruleset.xml \ -f text \ --no-cache \ --minimum-priority 1

对着输出核对,预期至少命中四条。如果哪条没出现,按下面的顺序查:

没出现的规则优先检查
EmptyCatchBlock规则是否被 exclude,properties 里allowCommentedBlocks是否被误设
UnusedPrivateField该规则所在类别有没有整体引入,privileged 修饰符是否影响判定
EmptyIfStmtPMD 7 里这条规则的归属类别是否变化
自定义 XPath 规则class属性全路径、language值、XPath 表达式本身

第二步验证阈值是否真的生效,把样本里那个方法的圈复杂度堆到 20 左右,再跑一次。阈值设成 15 应该报,改成 25 应该不报,两次结果都对上,才说明<properties>真的起作用了,而不是你以为它起作用了。

第三步验证排除规则。把样本复制一份到generated目录下,如果<exclude-pattern>写对了,这份文件应该完全不产生告警。很多人的规则文件就是漏了这一步验证,上线之后发现生成的代码报了几百条,再回来补排除规则。

这三步跑完,规则文件才算真的落地。从那以后我每次改ruleset.xml,不管改的只是一行参数还是加一条 exclude,都强制用这个样本跑一遍再提交,不敢再凭感觉。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询