Maven static analysis
last modified November 13, 2026
In this article we check Java code with three static analysis tools run from Maven: Checkstyle, SpotBugs, and PMD. We configure their rules, bind them to the build lifecycle, and make the build fail on violations. The examples use Maven 3.10, Checkstyle plugin 3.6.0, SpotBugs plugin 4.10.5.0, PMD plugin 3.28.0, and Java 25.
Static analysis finds problems in code without running it. The three tools complement each other:
- Checkstyle checks the source code against coding conventions, such as braces, imports, and naming.
- SpotBugs analyzes the compiled bytecode and looks for bug
patterns, such as wrong comparisons or broken
equalscontracts. - PMD checks the source code for bad practices and error-prone constructs; its CPD tool finds copied code.
Each plugin has a goal that writes a report and a check goal
that fails the build. In a build we use the check goals.
The project
Our project keeps a small order. The Order class contains
several mistakes on purpose.
package com.example.orders;
import java.util.List;
import java.util.ArrayList;
public class Order {
private final String id;
private final List<String> items = new ArrayList<>();
private int discount;
public Order(String id) {
this.id = id;
}
public void addItem(String item) {
items.add(item);
}
public List<String> getItems() {
return items;
}
public boolean isExpress() {
return id == "EXPRESS";
}
public int parseQuantity(String text) {
try {
return Integer.parseInt(text);
} catch (NumberFormatException _) {
}
return 0;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Order other)) return false;
return id.equals(other.id);
}
public String getId() {
return id;
}
}
The class compares strings with ==, swallows an exception in
an empty catch block, overrides equals without
hashCode, and has an unused field. The code compiles without
any warning.
package com.example.orders;
import module java.base;
public class Main {
public static void main(String[] args) {
var order = new Order("A-1001");
order.addItem("keyboard");
order.addItem("mouse");
List<String> items = order.getItems();
System.out.println(order.getId() + ": " + items);
System.out.println("Express: " + order.isExpress());
System.out.println("Quantity: " + order.parseQuantity("3x"));
}
}
The main class uses a module import declaration, a Java 25 feature, which
imports all packages exported by the java.base module.
Checkstyle with defaults
We start with the Checkstyle plugin and no configuration.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.6.0</version>
</plugin>
</plugins>
</build>
The check goal of the plugin can run from the command line.
Without configuration, it uses the sun_checks.xml rule set.
The check fails before it reports any rule. The plugin 3.6.0 ships with Checkstyle 9.3 from 2022, whose parser does not know the module import declaration. The second command shows the Checkstyle version in the debug output of the plugin's dependencies.
$ mvn checkstyle:check BUILD FAILURE Failed during checkstyle configuration: Exception was thrown while processing Main.java: IllegalStateException occurred while parsing file Main.java. 3:14: mismatched input 'java' expecting ';': InputMismatchException $ mvn -X checkstyle:check | grep -m1 puppycrawl [DEBUG] +- com.puppycrawl.tools:checkstyle:jar:9.3 [compile]
Configuring Checkstyle
We add a newer Checkstyle as a dependency of the plugin, choose our own rules, and bind the check to the build.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.6.0</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>14.4.0</version>
</dependency>
</dependencies>
<configuration>
<configLocation>config/checkstyle.xml</configLocation>
</configuration>
<executions>
<execution>
<id>checkstyle</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
A dependency inside the plugin element replaces the version
the plugin would otherwise use.
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>14.4.0</version>
</dependency>
Checkstyle 14.4.0 understands Java 25 syntax. The Checkstyle plugin and Checkstyle itself are released independently, so it is a good practice to pin the Checkstyle version always.
<phase>validate</phase>
The check goal is bound to the verify phase by
default. Checkstyle reads source files only, so we move it to
validate, the first phase. A style violation then stops the
build before compilation and tests.
<?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">
<module name="FileTabCharacter"/>
<module name="LineLength">
<property name="max" value="100"/>
</module>
<module name="TreeWalker">
<module name="AvoidStarImport"/>
<module name="UnusedImports"/>
<module name="NeedBraces"/>
<module name="EmptyCatchBlock"/>
<module name="EqualsHashCode"/>
<module name="StringLiteralEquality"/>
<module name="MissingSwitchDefault"/>
<module name="ConstantName"/>
<module name="MethodName"/>
</module>
</module>
The configuration lists the checks to run. The sun_checks.xml
and google_checks.xml sets bundled with Checkstyle are strict;
on our two small classes, the Sun checks report 26 violations, most
of them missing Javadoc comments and parameters that are not
final. A short list of checks that the team agrees on is
easier to adopt.
The check goal fails on violations with the error
severity, which is the default severity of every check. A check with the
severity property set to warning is reported but
does not fail the build.
Checkstyle reports four violations with the file, line, and column, and the build fails.
$ mvn validate
There are 4 errors reported by Checkstyle 14.4.0 with config/checkstyle.xml ruleset.
src/main/java/com/example/orders/Order.java:[25,19] (coding) StringLiteralEquality: Literal Strings should be compared using equals(), not '=='.
src/main/java/com/example/orders/Order.java:[31,43] (blocks) EmptyCatchBlock: Empty catch block.
src/main/java/com/example/orders/Order.java:[36,5] (coding) EqualsHashCode: Definition of 'equals()' without corresponding definition of 'hashCode()'.
src/main/java/com/example/orders/Order.java:[38,9] (blocks) NeedBraces: 'if' construct must use '{}'s.
BUILD FAILURE
Failed to execute goal org.apache.maven.plugins:maven-checkstyle-plugin:3.6.0:check: You have 4 Checkstyle violations.
SpotBugs
SpotBugs is not an Apache plugin; its group ID is
com.github.spotbugs.
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.10.5.0</version>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
The check goal is bound to the verify phase by
default, after the code is compiled and packaged.
<effort>Max</effort> <threshold>Low</threshold>
The effort option makes the analysis more thorough at the cost
of time. The threshold option sets the lowest confidence of
reported bugs; Low reports everything.
We skip Checkstyle with the checkstyle.skip property to see
what SpotBugs finds. Each plugin has such a property:
spotbugs.skip, pmd.skip, and
cpd.skip. The check goal first runs the
spotbugs goal, which analyzes the classes, and then evaluates
the results.
SpotBugs finds four bugs. Two of them, the string comparison and the
missing hashCode, were also found by Checkstyle. The other two
come from the bytecode: getItems returns the internal mutable
list, so callers can change the order without addItem, and the
discount field is never used. SpotBugs marks
HE_EQUALS_USE_HASHCODE as a high-priority bug; such orders
would break in a HashSet.
$ mvn verify -Dcheckstyle.skip --- spotbugs:4.10.5.0:spotbugs (spotbugs) @ orders --- --- spotbugs:4.10.5.0:check (default) @ orders --- Total bugs: 4 Medium: com.example.orders.Order.getItems() may expose internal representation by returning Order.items [com.example.orders.Order] At Order.java:[line 21] EI_EXPOSE_REP Medium: Comparison of String objects using == or != in com.example.orders.Order.isExpress() [com.example.orders.Order] At Order.java:[line 25] ES_COMPARING_STRINGS_WITH_EQ High: com.example.orders.Order defines equals and uses Object.hashCode() [com.example.orders.Order] At Order.java:[lines 38-39] HE_EQUALS_USE_HASHCODE Medium: Unused field: com.example.orders.Order.discount [com.example.orders.Order] In Order.java UUF_UNUSED_FIELD BUILD FAILURE Failed to execute goal com.github.spotbugs:spotbugs-maven-plugin:4.10.5.0:check: failed with 4 bugs and 0 errors
PMD
We add the PMD plugin with a custom rule set.
<artifactId>maven-pmd-plugin</artifactId>
<version>3.28.0</version>
<configuration>
<rulesets>
<ruleset>config/pmd-rules.xml</ruleset>
</rulesets>
<printFailingErrors>true</printFailingErrors>
<linkXRef>false</linkXRef>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
<goal>cpd-check</goal>
</goals>
</execution>
</executions>
</plugin>
The check and cpd-check goals are bound to the
verify phase by default.
<printFailingErrors>true</printFailingErrors> <linkXRef>false</linkXRef>
By default, a failing check prints only the number of violations and the
path to target/pmd.xml. The printFailingErrors
option prints each violation. The linkXRef option turns off
links to the cross-referenced source in the report, which we do not
generate; it silences a warning.
<?xml version="1.0"?>
<ruleset name="orders"
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 rules for the orders project</description>
<rule ref="category/java/bestpractices.xml">
<exclude name="SystemPrintln"/>
</rule>
<rule ref="category/java/errorprone.xml"/>
</ruleset>
The rule set takes all rules from two PMD categories: best practices and
error-prone code. Single rules can be excluded; we exclude
SystemPrintln, since our program is a command line
application. Without a rule set, the plugin uses its own small default
set.
PMD reports five violations. The cpd-check goal does not run,
because the build fails first. The plugin uses PMD 7.17.0; a newer PMD
can be set with plugin dependencies, as with Checkstyle.
$ mvn verify -Dcheckstyle.skip -Dspotbugs.skip --- pmd:3.28.0:pmd (pmd) @ orders --- PMD version: 7.17.0 --- pmd:3.28.0:check (default) @ orders --- PMD Failure: com.example.orders.Order:10 Rule:UnusedPrivateField Priority:3 Avoid unused private fields such as 'discount'.. PMD Failure: com.example.orders.Order:25 Rule:CompareObjectsWithEquals Priority:3 Use equals() to compare object references.. PMD Failure: com.example.orders.Order:25 Rule:UseEqualsToCompareStrings Priority:3 Use equals() to compare strings instead of '==' or '!='. PMD Failure: com.example.orders.Order:31 Rule:EmptyCatchBlock Priority:3 Avoid empty catch blocks. PMD Failure: com.example.orders.Order:37 Rule:OverrideBothEqualsAndHashcode Priority:3 Ensure you override both equals() and hashCode(). BUILD FAILURE Failed to execute goal org.apache.maven.plugins:maven-pmd-plugin:3.28.0:check: PMD 7.17.0 has found 5 violations. For more details see: /home/user/project/orders/target/pmd.xml
Fixing the code
We fix all the reported problems.
package com.example.orders;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class Order {
private final String id;
private final List<String> items = new ArrayList<>();
public Order(String id) {
this.id = id;
}
public void addItem(String item) {
items.add(item);
}
public List<String> getItems() {
return List.copyOf(items);
}
public boolean isExpress() {
return "EXPRESS".equals(id);
}
public int parseQuantity(String text) {
try {
return Integer.parseInt(text);
} catch (NumberFormatException _) {
return 0;
}
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Order other)) {
return false;
}
return id.equals(other.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
public String getId() {
return id;
}
}
The getItems method returns an unmodifiable copy, strings are
compared with equals, the catch block returns a
default value, hashCode is consistent with
equals, and the unused field is gone. The unnamed variable
_ tells that the exception object is not used.
All three tools pass. Checkstyle runs first, in the validate
phase; SpotBugs, PMD, and CPD run at the end, in the verify
phase.
$ mvn verify --- checkstyle:3.6.0:check (checkstyle) @ orders --- You have 0 Checkstyle violations. --- spotbugs:4.10.5.0:check (default) @ orders --- --- pmd:3.28.0:pmd (pmd) @ orders --- --- pmd:3.28.0:check (default) @ orders --- --- pmd:3.28.0:cpd (cpd) @ orders --- --- pmd:3.28.0:cpd-check (default) @ orders --- BUILD SUCCESS $ java -cp target/classes com.example.orders.Main A-1001: [keyboard, mouse] Express: false Quantity: 0
Source
Apache Maven Checkstyle Plugin, SpotBugs Maven Plugin, Apache Maven PMD Plugin
In this article we have checked Java code with the Checkstyle, SpotBugs, and PMD plugins and made the Maven build fail on violations.
Author
List all Java tutorials.