Maven Quality Plugins

This section documents Spring Boot 4.1.x and Spring Framework 7.0.x on the Java 17/21+ baseline, as described by the official Spring Boot reference documentation and each sub-project’s own site (Spring Data, Spring for Apache Kafka, Micrometer, Project Reactor, springdoc-openapi, Spring gRPC, and the others listed in this section’s Bibliography) — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Spring Boot and its ecosystem continue to evolve; the examples here target the current 4.1.x / 7.0.x releases.

This section’s bibliography lists the reference material consulted while preparing these pages.

A Spring Boot build’s confidence comes as much from its quality plugins as from its application code: JaCoCo measures how much of that code the tests actually exercise, Surefire and Failsafe separate fast unit tests from slower integration tests, and Checkstyle/SpotBugs/PMD catch style and correctness issues before they reach review.

JaCoCo for coverage

The JaCoCo Maven plugin instruments classes at test-run time and reports line/branch coverage. The prepare-agent execution attaches a Java agent to the JVM that Surefire (and Failsafe) forks, and report renders the collected data as HTML/XML/CSV:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.12</version>
    <executions>
        <execution>
            <id>prepare-agent</id>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
        <execution>
            <id>check</id>
            <phase>verify</phase>
            <goals>
                <goal>check</goal>
            </goals>
            <configuration>
                <rules>
                    <rule>
                        <element>BUNDLE</element>
                        <limits>
                            <limit>
                                <counter>LINE</counter>
                                <value>COVEREDRATIO</value>
                                <minimum>0.80</minimum>
                            </limit>
                        </limits>
                    </rule>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

The check execution fails the build when coverage drops below the configured minimum, turning coverage into a gate rather than a report nobody reads.

Multi-module report-aggregate

In a multi-module reactor build, each module’s own report only covers that module’s tests. To get one combined report across every module — essential when, for example, a service module’s integration tests exercise classes from a core module — add a dedicated aggregator module that depends on all the others and runs report-aggregate instead of report:

<!-- coverage-report/pom.xml: a module with no code of its own -->
<project>
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>parent</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <artifactId>coverage-report</artifactId>
    <packaging>pom</packaging>

    <dependencies>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>core</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>service</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.12</version>
                <executions>
                    <execution>
                        <id>report-aggregate</id>
                        <phase>verify</phase>
                        <goals>
                            <goal>report-aggregate</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

report-aggregate reads the jacoco.exec data files produced by every dependency module’s own prepare-agent execution and merges them into a single site under coverage-report/target/site/jacoco-aggregate. The aggregator module must be built after the modules it aggregates, so list it last in the parent’s <modules>.

Surefire for unit tests, Failsafe for integration tests

The Surefire plugin runs during the test phase and is bound to the default Maven lifecycle, so mvn test (and every later phase, including package and install) runs it automatically. The Failsafe plugin runs during the separate integration-test and verify phases:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <configuration>
                <includes>
                    <include>**/*Test.java</include>
                    <include>**/*Tests.java</include>
                </includes>
                <excludes>
                    <exclude>**/*IT.java</exclude>
                </excludes>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-failsafe-plugin</artifactId>
            <executions>
                <execution>
                    <goals>
                        <goal>integration-test</goal>
                        <goal>verify</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <includes>
                    <include>**/*IT.java</include>
                </includes>
            </configuration>
        </plugin>
    </plugins>
</build>

Why two plugins and two naming conventions

Surefire’s default include pattern picks up classes named Test, *Tests, *TestCase, and Test; Failsafe’s default picks up IT, IT, and *ITCase. Following that *Test / *IT convention — rather than overriding the patterns — is what keeps the split working with zero configuration:

  • *Test (Surefire, test phase) — pure unit tests: no Spring context, no database, no network. They run on every mvn test and must stay fast, because they run constantly during development.

  • *IT (Failsafe, integration-test/verify phases) — integration tests, typically annotated @SpringBootTest and often backed by Testcontainers: they start a Spring context, talk to a real (or containerized) database, or hit HTTP endpoints. They are slower and more environment-sensitive, so they are kept out of the default test phase.

The phase separation also matters for failure handling: Surefire fails the build immediately on a test failure, which would leave any application or container started by the pre-integration-test phase (e.g. by spring-boot:start or the Testcontainers Maven plugin) running forever. Failsafe instead defers the failure check to its own verify goal, bound after the post-integration-test phase that tears such resources down:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <executions>
        <execution>
            <id>pre-integration-test</id>
            <phase>pre-integration-test</phase>
            <goals>
                <goal>start</goal>
            </goals>
        </execution>
        <execution>
            <id>post-integration-test</id>
            <phase>post-integration-test</phase>
            <goals>
                <goal>stop</goal>
            </goals>
        </execution>
    </executions>
</plugin>

With this in place, mvn verify runs unit tests, packages the app, starts it, runs *IT classes against the running instance, stops it, and only then fails the build if any integration test failed.

Static analysis: Checkstyle, SpotBugs, and PMD

The three tools check different things and are normally run together: Checkstyle enforces style (formatting, naming, import order) from source text, SpotBugs analyzes compiled bytecode for likely bugs (null dereferences, resource leaks, bad equals/hashCode), and PMD analyzes source for both style and structural problems (unused variables, overly complex methods, copy-pasted code via its CPD companion).

Checkstyle

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-checkstyle-plugin</artifactId>
    <version>3.6.0</version>
    <configuration>
        <configLocation>google_checks.xml</configLocation>
        <consoleOutput>true</consoleOutput>
        <failOnViolation>true</failOnViolation>
        <violationSeverity>warning</violationSeverity>
        <excludes>**/generated-sources/**/*,**/target/generated-sources/**/*</excludes>
    </configuration>
    <executions>
        <execution>
            <id>checkstyle-check</id>
            <phase>verify</phase>
            <goals>
                <goal>check</goal>
            </goals>
        </execution>
    </executions>
</plugin>

configLocation points at a rule set — a bundled one (google_checks.xml, sun_checks.xml) or a custom checkstyle.xml checked into the repository so every module shares the same rules. See Checkstyle.

SpotBugs

<plugin>
    <groupId>com.github.spotbugs</groupId>
    <artifactId>spotbugs-maven-plugin</artifactId>
    <version>4.9.3.2</version>
    <configuration>
        <effort>Max</effort>
        <threshold>Medium</threshold>
        <failOnError>true</failOnError>
        <excludeFilterFile>spotbugs-exclude.xml</excludeFilterFile>
    </configuration>
    <executions>
        <execution>
            <id>spotbugs-check</id>
            <phase>verify</phase>
            <goals>
                <goal>check</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Because SpotBugs analyzes .class files, generated sources (MapStruct mappers, JPA metamodel classes) are excluded implicitly whenever they are not on the analyzed outputDirectory, and explicitly via excludeFilterFile for any generated class that does end up compiled into the main output. See SpotBugs.

PMD

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-pmd-plugin</artifactId>
    <version>3.27.0</version>
    <configuration>
        <rulesets>
            <ruleset>category/java/bestpractices.xml</ruleset>
            <ruleset>category/java/errorprone.xml</ruleset>
            <ruleset>category/java/design.xml</ruleset>
        </rulesets>
        <excludes>
            <exclude>**/generated-sources/**</exclude>
            <exclude>**/mapper/**/*MapperImpl.java</exclude>
        </excludes>
        <failOnViolation>true</failOnViolation>
        <printFailingErrors>true</printFailingErrors>
    </configuration>
    <executions>
        <execution>
            <id>pmd-check</id>
            <phase>verify</phase>
            <goals>
                <goal>check</goal>
                <goal>cpd-check</goal>
            </goals>
        </execution>
    </executions>
</plugin>

See PMD.

Failing the build vs. reporting only

Each of the three plugins ships both a *:check goal (bound to a lifecycle phase such as verify, it fails the build past failOnViolation/threshold) and a reporting-only mode driven from the <reporting> section (or mvn site), which renders findings as an HTML report without affecting the build’s exit code:

<reporting>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-checkstyle-plugin</artifactId>
            <version>3.6.0</version>
            <!-- no <execution> bound to <build>: mvn site renders a report, mvn verify does not fail -->
        </plugin>
    </plugins>
</reporting>

A common rollout strategy is to start every new tool in reporting-only mode — so the team can see the backlog of existing violations without breaking CI — then flip it to a <build>-bound check execution with failOnViolation=true once the codebase is clean, so new violations are rejected at verify time going forward.

Excluding generated sources

Generated code — MapStruct mapper implementations, Lombok-expanded members (invisible to source-level tools but visible to SpotBugs' bytecode analysis), JPA metamodel classes, protobuf/gRPC stubs — should never be penalized by these tools, both because it wasn’t hand-written and because regenerating it can silently "fix" or reintroduce findings. Each plugin exposes an exclusion mechanism: Checkstyle and PMD accept <excludes> glob patterns matched against source paths (typically pointed at target/generated-sources/), while SpotBugs either omits the generated output directory from <sourceRoots>/<outputDirectory> or lists specific classes in an <excludeFilterFile>. Consistently excluding /generated-sources/ and /target/** across all three configurations keeps generated code out of every report.

Wiring everything into verify

Binding JaCoCo’s check, Failsafe’s verify, and the three static-analysis check goals all to the verify phase means a single mvn verify runs unit tests, integration tests, coverage enforcement, and static analysis in one command — the same command CI should invoke as its quality gate:

<build>
    <plugins>
        <!-- jacoco-maven-plugin: prepare-agent + report + check (see above) -->
        <!-- maven-surefire-plugin: default test phase -->
        <!-- maven-failsafe-plugin: integration-test + verify -->
        <!-- maven-checkstyle-plugin: check bound to verify -->
        <!-- spotbugs-maven-plugin: check bound to verify -->
        <!-- maven-pmd-plugin: check + cpd-check bound to verify -->
    </plugins>
</build>

A failure in any one of those checks fails mvn verify with a non-zero exit code, which is what a CI pipeline should treat as "do not merge."