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,testphase) — pure unit tests: no Spring context, no database, no network. They run on everymvn testand must stay fast, because they run constantly during development. -
*IT(Failsafe,integration-test/verifyphases) — integration tests, typically annotated@SpringBootTestand 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 defaulttestphase.
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."