ZetCode

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:

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.

orders/src/main/java/com/example/orders/Order.java
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.

orders/src/main/java/com/example/orders/Main.java
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.

pom.xml (build)
<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.

pom.xml (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.

orders/config/checkstyle.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">
    <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.

pom.xml (SpotBugs plugin)
    <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.

pom.xml (PMD plugin)
    <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.

orders/config/pmd-rules.xml
<?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.

src/main/java/com/example/orders/Order.java
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Java tutorials.