Build and Tooling

This section documents the current Java release line, with Java 25 LTS as the reference point (no specific patch version is pinned), as published at the Java developer portal, the Java Tutorials, and the Java SE API specification — 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 the official documentation before being relied on in production.

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

The JDK ships a complete set of command-line tools for compiling, running, packaging, inspecting, and documenting code. Real projects then drive those tools through a build system — Maven or Gradle — that adds dependency management, a standard directory layout, and a repeatable lifecycle. This page covers the core tools, then both build systems, then javadoc. References: the JDK tool specifications, dev.java "The Core JDK Tools", the Maven guides, and the Gradle user guide.

The Core JDK Tools

javac compiles .java to .class; java launches a class, a module, or — since Java 11 — a single source file directly; jar packages classes and can mark one as the entry point via the manifest’s Main-Class.

javac -d out $(find src -name '*.java')          # compile the tree into out/
java  -cp out com.example.Main                    # launch a class from the classpath
java  src/com/example/Scratch.java                # run one source file, no explicit compile

jar --create --file app.jar --main-class com.example.Main -C out .
java -jar app.jar                                 # executable JAR: Main-Class from the manifest

The remaining tools inspect and package:

jshell                                            # interactive REPL for snippets
jdeps --module-path libs app.jar                  # static class/module dependency analysis
jlink --add-modules com.example.app --output runtime          # minimal custom runtime image
jpackage --name MyApp --input target --main-jar app.jar       # native installer / app image

jcmd <pid> Thread.print                           # ad-hoc diagnostics on a live JVM
jstack <pid>                                      # thread dump
jmap -histo <pid>                                 # heap object histogram
java -XX:StartFlightRecording=duration=60s,filename=rec.jfr -jar app.jar   # JDK Flight Recorder

jlink and jpackage operate on modules — see Packages and Modules. JDK Flight Recorder writes a low-overhead .jfr profile that JDK Mission Control or jfr print can open.

Building with Maven

Maven expects a fixed layout — src/main/java for code, src/test/java for tests — and a single pom.xml describing the project. Every artifact has coordinates: groupId : artifactId : version. See Introduction to the POM.

myapp/
  pom.xml
  src/
    main/java/com/example/App.java
    test/java/com/example/AppTest.java
  target/                     (build output: classes, test reports, the packaged JAR)
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>myapp</artifactId>
  <version>1.0.0</version>

  <properties>
    <maven.compiler.release>25</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.11.4</version>
      <scope>test</scope>          <!-- on the test classpath only -->
    </dependency>
  </dependencies>
</project>

The build lifecycle is a fixed sequence of phases; running one runs every earlier phase. See Introduction to the Build Lifecycle.

mvn compile            # src/main/java -> target/classes
mvn test               # compile, then run tests via the Surefire plugin
mvn package            # assemble target/myapp-1.0.0.jar
mvn install            # copy that artifact into the local ~/.m2 repository
mvn -q clean package   # quiet, starting from a clean target/

Building with Gradle

Gradle uses a build script — build.gradle (Groovy DSL) or build.gradle.kts (Kotlin DSL) — that is executable code rather than declarative XML. The java plugin adds the same src/main/java layout and the compileJava, test, and jar tasks. See The Java Plugin.

plugins {
    id 'java'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(25)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}
./gradlew build        # compile, test, and assemble
./gradlew test         # run tests only
./gradlew --offline classes

In one line: Maven is declarative and convention-bound, so builds look alike and are easy to read; Gradle is a programmable build with fine-grained incremental execution, trading some uniformity for flexibility and speed.

Common Build Plugins

The bare Maven and Gradle setups above compile, test, and package — but real projects add plugins for testing, code coverage, static analysis, reporting, and publishing. Maven binds each plugin goal to a lifecycle phase so it runs automatically; Gradle applies each as a plugin that contributes tasks, most of which the check task then depends on. Almost every plugin below has a first-class counterpart on the other side, so a project can move between build systems without losing a capability. For a fuller, Spring-oriented treatment of the coverage and static-analysis trio — with multi-module report aggregation and generated-source exclusions — see Maven Quality Plugins.

Unit tests: Surefire / the test task

Maven Surefire runs unit tests in the test phase. By default it picks up **/Test*.java, **/*Test.java, **/*Tests.java, and **/*TestCase.java; <includes>/<excludes> override that. Gradle’s core java plugin already provides the test task — only useJUnitPlatform() is needed to select the JUnit 5 engine.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>3.6.0</version>
  <configuration>
    <includes>
      <include>**/*Test.java</include>
    </includes>
    <excludes>
      <exclude>**/*IT.java</exclude>   <!-- integration tests run in the Failsafe phase -->
    </excludes>
  </configuration>
</plugin>
tasks.named('test') {
    useJUnitPlatform()
}

Integration tests: Failsafe / a separate test suite

Integration tests — ones that start a container, hit a database, or bind a port — are slower and more fragile than unit tests, so they are kept out of the fast test phase and named *IT rather than *Test. Maven Failsafe is a sibling of Surefire that binds its integration-test and verify goals to the matching phases and matches **/IT*.java, **/*IT.java, and **/*ITCase.java; keeping the two goals separate lets verify fail the build only after post-integration cleanup has run. Gradle has no Failsafe: the current approach is a dedicated suite via the (incubating) JVM Test Suite plugin. check does not depend on a custom suite automatically — wire it in explicitly.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-failsafe-plugin</artifactId>
  <version>3.6.0</version>
  <executions>
    <execution>
      <goals>
        <goal>integration-test</goal>
        <goal>verify</goal>
      </goals>
    </execution>
  </executions>
</plugin>
plugins {
    id 'java'
    id 'jvm-test-suite'
}

testing {
    suites {
        integrationTest(JvmTestSuite) {
            dependencies {
                implementation project()
            }
            targets.configureEach {
                testTask.configure {
                    shouldRunAfter(test)
                }
            }
        }
    }
}

tasks.named('check') {
    dependsOn(testing.suites.integrationTest)
}

Writing the tests themselves — JUnit 5 and Mockito — is covered in Testing.

Code coverage: JaCoCo

JaCoCo instruments classes at test-run time and reports line and branch coverage as HTML/XML/CSV. In Maven, prepare-agent attaches the coverage agent to the JVM Surefire forks, report renders the collected data, and check compares it against configured rules — turning coverage into a build gate rather than a report nobody reads. Gradle’s core jacoco plugin provides jacocoTestReport and jacocoTestCoverageVerification for the same two jobs.

<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>
plugins {
    id 'java'
    id 'jacoco'
}

jacocoTestReport {
    reports {
        xml.required = true
    }
}

jacocoTestCoverageVerification {
    violationRules {
        rule {
            limit {
                counter = 'LINE'
                value = 'COVEREDRATIO'
                minimum = 0.80
            }
        }
    }
}

tasks.named('check') {
    dependsOn(jacocoTestCoverageVerification)
}

Static analysis: Checkstyle, PMD, SpotBugs

The three tools inspect different artifacts. Checkstyle reads the source text and enforces formatting and naming conventions against a rule set such as google_checks.xml. PMD analyses the source structure for questionable constructs (unused variables, over-complex methods) and bundles CPD, a copy-paste detector. SpotBugs works on compiled bytecode, matching known bug patterns such as null dereferences and resource leaks. Each has a Maven plugin whose check goal binds to verify, and a Gradle plugin — core checkstyle and pmd, plus the community com.github.spotbugs plugin — all wired into check.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-checkstyle-plugin</artifactId>
  <version>3.6.0</version>
  <configuration>
    <configLocation>google_checks.xml</configLocation>
  </configuration>
  <executions>
    <execution><phase>verify</phase><goals><goal>check</goal></goals></execution>
  </executions>
</plugin>

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-pmd-plugin</artifactId>
  <version>3.27.0</version>
  <executions>
    <execution><phase>verify</phase><goals><goal>check</goal><goal>cpd-check</goal></goals></execution>
  </executions>
</plugin>

<plugin>
  <groupId>com.github.spotbugs</groupId>
  <artifactId>spotbugs-maven-plugin</artifactId>
  <version>4.9.3.2</version>
  <executions>
    <execution><phase>verify</phase><goals><goal>check</goal></goals></execution>
  </executions>
</plugin>
plugins {
    id 'checkstyle'
    id 'pmd'
    id 'com.github.spotbugs' version '6.5.11'
}

checkstyle {
    toolVersion = '10.21.0'   // pins the analyzer itself; bump to the current release
    // rule set read from config/checkstyle/checkstyle.xml by default
}

pmd {
    toolVersion = '7.10.0'
    ruleSets = ['category/java/bestpractices.xml']
}

spotbugs {
    toolVersion = '4.9.3'
}

toolVersion pins the analyzer Gradle downloads, independent of the plugin version — keep it on the tool’s latest release (the Maven plugins pull a sensible default, so they omit the equivalent). By default the Gradle Checkstyle/PMD/SpotBugs tasks fail the build on any violation; set ignoreFailures = true (or, on the Maven side, <failOnViolation>false</failOnViolation> / <failOnError>false</failOnError>) to run a tool in report-only mode while a codebase catches up.

Continuous inspection: SonarQube

The plugins above run each tool in isolation; SonarQube (the self-hosted server or the hosted SonarQube Cloud) instead collects coverage, duplication, and its own rule engine’s findings into one dashboard with history and a pass/fail quality gate. A scanner run is a separate step after verify, not part of it, so it usually runs only in CI: mvn verify sonar:sonar via the SonarScanner for Maven, or ./gradlew build sonar via the SonarScanner for Gradle (org.sonarqube). The scanner reads the JaCoCo XML report, so run coverage first.

<plugin>
  <groupId>org.sonarsource.scanner.maven</groupId>
  <artifactId>sonar-maven-plugin</artifactId>
  <version>5.7.0.6970</version>
</plugin>
plugins {
    id 'org.sonarqube' version '7.4.0.8496'
}

sonar {
    properties {
        property 'sonar.projectKey', 'com.example:myapp'
        property 'sonar.host.url', 'https://sonarqube.example.com'
    }
}

Pass sonar.token (and, for SonarQube Cloud, sonar.organization) on the command line or via environment variables — never commit it.

API docs: the Javadoc plugin

The JDK javadoc tool (covered below) generates the HTML; the build plugin is a thin wrapper that runs it inside the build and, importantly, produces a -javadoc.jar for publishing to a repository alongside the main artifact. maven-javadoc-plugin's jar goal is bound to package for exactly that; Gradle’s core java plugin already defines a javadoc task, and java { withJavadocJar() } adds the publishable jar (with withSourcesJar() as its companion).

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-javadoc-plugin</artifactId>
  <version>3.12.0</version>
  <executions>
    <execution>
      <id>attach-javadocs</id>
      <phase>package</phase>
      <goals>
        <goal>jar</goal>
      </goals>
    </execution>
  </executions>
</plugin>
plugins {
    id 'java'
}

java {
    withJavadocJar()
}

tasks.named('javadoc') {
    options.addStringOption('Xdoclint:all,-missing', '-quiet')
}

Report aggregation: Maven Site

mvn site runs the Maven Site plugin, which renders a static project website gathering every <reporting> plugin’s output — Surefire test results, the JaCoCo coverage report, Checkstyle/PMD/SpotBugs findings, the Javadoc — into one browsable place. Gradle has no unified equivalent: each plugin writes its own HTML under build/reports/, and the closest aggregator is the legacy core build-dashboard plugin, whose buildDashboard task links together reports other tasks have already produced. It is not run by check or build, and applying it disables the Gradle configuration cache.

<reporting>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-report-plugin</artifactId>
      <version>3.6.0</version>
    </plugin>
    <plugin>
      <groupId>org.jacoco</groupId>
      <artifactId>jacoco-maven-plugin</artifactId>
      <version>0.8.12</version>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-javadoc-plugin</artifactId>
      <version>3.12.0</version>
    </plugin>
  </plugins>
</reporting>
plugins {
    id 'java'
    id 'build-dashboard'
}

tasks.named('test') {
    reports {
        html.required = true
    }
}
// ./gradlew build buildDashboard  ->  build/reports/buildDashboard/index.html

Publishing to Maven Central

Releasing a library to Maven Central means uploading the main JAR plus its -sources and -javadoc JARs and their PGP signatures to the Sonatype Central Portal. On the Maven side the central-publishing-maven-plugin replaces the deploy goal and handles the upload and (optionally) the release; pair it with maven-gpg-plugin for the signatures and the Javadoc/source plugins' jar goals. Gradle’s core maven-publish only builds the publication — for the Central Portal upload the common choice is the third-party com.vanniktech.maven.publish plugin, which also wires in signing and the source/Javadoc JARs.

The Central Portal validates every upload against required POM metadata (name, description, URL, licenses, developers, SCM), not just the plugin declaration — a pom.xml/build.gradle.kts that omits any of these fails the release with a metadata-validation error rather than an upload error. A minimal pom.xml covering the whole flow — coordinates, required metadata, the publishing plugin, the source/Javadoc jars Central Portal also requires, and GPG signing split into its own Maven profile so an ordinary local mvn verify doesn’t need a signing key — looks like:

<project>
  <groupId>com.example</groupId>
  <artifactId>myapp</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>

  <name>${project.groupId}:${project.artifactId}</name>
  <description>A short description of the library</description>
  <url>https://github.com/example/myapp</url>

  <licenses>
    <license>
      <name>The Apache License, Version 2.0</name>
      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
    </license>
  </licenses>
  <developers>
    <developer>
      <name>Jane Doe</name>
      <email>jane@example.com</email>
      <organizationUrl>https://github.com/example</organizationUrl>
    </developer>
  </developers>
  <scm>
    <connection>scm:git:git@github.com:example/myapp.git</connection>
    <developerConnection>scm:git:git@github.com:example/myapp.git</developerConnection>
    <url>git@github.com:example/myapp.git</url>
  </scm>

  <build>
    <plugins>
      <plugin>
        <groupId>org.sonatype.central</groupId>
        <artifactId>central-publishing-maven-plugin</artifactId>
        <version>0.11.0</version>
        <extensions>true</extensions>
        <configuration>
          <publishingServerId>central</publishingServerId>   <!-- credentials from settings.xml -->
          <autoPublish>true</autoPublish>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-source-plugin</artifactId>
        <version>3.3.1</version>
        <executions>
          <execution>
            <id>attach-sources</id>
            <goals><goal>jar-no-fork</goal></goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-javadoc-plugin</artifactId>
        <version>3.11.3</version>
        <executions>
          <execution>
            <id>attach-javadocs</id>
            <goals><goal>jar</goal></goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

  <!-- kept as its own profile: signing needs a GPG key, which a plain "mvn verify" shouldn't require -->
  <profiles>
    <profile>
      <id>sign</id>
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-gpg-plugin</artifactId>
            <version>3.2.8</version>
            <executions>
              <execution>
                <id>sign-artifacts</id>
                <phase>verify</phase>
                <goals><goal>sign</goal></goals>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>
</project>

Signing is opt-in via -P sign precisely so day-to-day builds don’t need a GPG key on hand — only a release build activates it: mvn -P sign deploy.

The Gradle equivalent uses com.vanniktech.maven.publish’s own DSL for both the source/Javadoc jars and the POM metadata, instead of separate plugins. Unlike every other Gradle example on this page, this one is shown in the Kotlin DSL (`build.gradle.kts) rather than Groovy — com.vanniktech.maven.publish’s own docs default to it, and it’s the form newly generated Gradle projects use; the same `mavenPublishing { } block works unmodified in a Groovy build.gradle, just with id 'java-library'-style plugin syntax instead of id("java-library").

import com.vanniktech.maven.publish.JavadocJar
import com.vanniktech.maven.publish.JavaLibrary

plugins {
    id("java-library")
    id("com.vanniktech.maven.publish") version "0.34.0"
}

group = "com.example"
version = "1.0.0"

mavenPublishing {
    configure(JavaLibrary(
        javadocJar = JavadocJar.Javadoc(),
        sourcesJar = true,
    ))

    publishToMavenCentral()
    signAllPublications()

    coordinates("com.example", "myapp", "1.0.0")

    pom {
        name.set("myapp")
        description.set("A short description of the library")
        inceptionYear.set("2024")
        url.set("https://github.com/example/myapp/")
        licenses {
            license {
                name.set("Apache License 2.0")
                url.set("https://github.com/example/myapp/blob/main/LICENSE")
                distribution.set("https://github.com/example/myapp/blob/main/LICENSE")
            }
        }
        developers {
            developer {
                id.set("jdoe")
                name.set("Jane Doe")
                email.set("jane@example.com")
                url.set("https://github.com/example/")
            }
        }
        scm {
            url.set("https://github.com/example/myapp/")
            connection.set("scm:git:github.com/example/myapp.git")
            developerConnection.set("scm:git:ssh://github.com/example/myapp.git")
        }
    }
}
JavaLibrary is the variant for a plain JVM library. An Android library instead passes configure(AndroidSingleVariantLibrary(variant = "release", sourcesJar = true, publishJavadocJar = true)) — see the Kotlin reference’s own pointer to this section for the JVM/Maven Central side of an Android/Kotlin library, which otherwise follows the exact same plugin, coordinates(), and pom { } shape shown here.

Creating a Sonatype Central Portal Account

Publishing requires an account at central.sonatype.com and a verified namespace — the groupId prefix a publisher is allowed to release under. A namespace based on a domain the publisher owns (com.example) is verified by adding a DNS TXT record with a token the portal provides; a namespace of the form io.github.<user> is verified automatically instead, by proving ownership of the matching GitHub account — the common choice for an individual maintainer with no domain of their own.

Generating a User Token

Once the account and namespace are set up, Account → Generate User Token on the Central Portal produces a token pair (a username and a password-like token) used for the actual upload — this is not the account’s own login password. On the Maven side it goes into ~/.m2/settings.xml, matching the publishingServerId the plugin configuration references:

<settings>
  <servers>
    <server>
      <id>central</id>
      <username>GENERATED_TOKEN_USERNAME</username>
      <password>GENERATED_TOKEN_PASSWORD</password>
    </server>
  </servers>
</settings>

On the Gradle side, com.vanniktech.maven.publish reads the same pair from gradle.properties (local development) or environment variables (CI):

# gradle.properties -- keep this file out of version control, or use env vars in CI instead
mavenCentralUsername=GENERATED_TOKEN_USERNAME
mavenCentralPassword=GENERATED_TOKEN_PASSWORD

Generating and Publishing a GPG Key

The Central Portal requires every artifact to carry a valid PGP signature, and that the public key be resolvable from a public keyserver so anyone downloading the artifact can verify it:

gpg --gen-key                                                     # create a new keypair, interactively
gpg --list-secret-keys --keyid-format long                        # find the key id to export/publish
gpg --export-secret-keys <KEY_ID> > private-key.gpg                # for use in CI, see below
gpg --keyserver keyserver.ubuntu.com --send-keys <KEY_ID>          # publish the PUBLIC key

Sonatype’s own validation queries keyserver.ubuntu.com (and mirrors such as keys.openpgp.org) when a release is uploaded, so the public key must be published before the first deploy.

A GitHub Actions Release Workflow

Following the same style as the C# reference workflow (csharp/build-and-tooling.adoc, == Continuous Integration): a build job that runs the ordinary build/test steps, and a publish step gated to run only on a version tag. actions/setup-java itself generates ~/.m2/settings.xml (matching the <server><id>central</id> block shown earlier) and imports the signing key into an isolated keyring when given the right inputs — there’s no need for a hand-written settings file or a separate gpg --batch --import step:

name: build

on:
  push:
    branches: [ main ]
    tags: [ 'v*' ]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '25'
          cache: 'maven'
          server-id: central                         # matches <publishingServerId>central</publishingServerId>
          server-username: CENTRAL_TOKEN_USERNAME     # env var name -- populated below, not the value itself
          server-password: CENTRAL_TOKEN_PASSWORD     # env var name -- populated below, not the value itself
          gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
          gpg-passphrase: GPG_PASSPHRASE               # env var name -- populated below, not the value itself

      - name: Build and test
        run: mvn -B verify

      - name: Publish to Maven Central
        if: startsWith(github.ref, 'refs/tags/v')
        run: mvn -B -P sign deploy -DskipTests
        env:
          CENTRAL_TOKEN_USERNAME: ${{ secrets.CENTRAL_TOKEN_USERNAME }}
          CENTRAL_TOKEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN_PASSWORD }}
          GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}

-P sign activates the signing profile shown earlier — deploying without it fails Central Portal’s signature check — and -DskipTests avoids re-running the suite mvn verify already executed a few steps earlier. server-username/server-password/gpg-passphrase name the environment variables the generated settings.xml reads from (via gpg.passphraseEnvName, which needs maven-gpg-plugin 3.2.0+ — already satisfied by the 3.2.8 used above); the actual secret values are only ever supplied through the deploy step’s own env: block, never inlined into a command-line flag.

The Gradle equivalent replaces the last two steps with ./gradlew publish, which com.vanniktech.maven.publish picks up automatically from ORG_GRADLE_PROJECT_mavenCentralUsername/ORG_GRADLE_PROJECT_mavenCentralPassword for the Central Portal credentials, and ORG_GRADLE_PROJECT_signingInMemoryKey/ ORG_GRADLE_PROJECT_signingInMemoryKeyPassword for an in-memory signing key — Gradle’s own CI-friendly mechanism, distinct from the keyring actions/setup-java manages for the Maven job above.

Also worth knowing

Dependency freshness. versions-maven-plugin's versions:display-dependency-updates and versions:display-plugin-updates goals report newer releases of the dependencies and plugins a build pins. The Gradle counterpart is io.github.ben-manes.versions and its dependencyUpdates task — the plugin’s namespace moved from com.github.ben-manes in v0.55.0, so the old id still resolves but logs a deprecation warning.

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>versions-maven-plugin</artifactId>
  <version>2.21.0</version>
</plugin>
plugins {
    id 'io.github.ben-manes.versions' version '0.61.0'
}

Fat / uber JARs. To bundle every dependency into one runnable JAR, maven-shade-plugin's shade goal (bound to package) merges and optionally relocates the dependency classes; Gradle’s com.gradleup.shadow (the maintained successor to com.github.johnrengelman.shadow) adds the shadowJar task.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-shade-plugin</artifactId>
  <version>3.6.2</version>
  <executions>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>shade</goal>
      </goals>
    </execution>
  </executions>
</plugin>
plugins {
    id 'java'
    id 'com.gradleup.shadow' version '9.6.1'
}

To hard-fail a build on banned dependencies, a wrong JDK, or unresolved version conflicts, Maven uses maven-enforcer-plugin's enforce goal; Gradle has no single-plugin analogue — the same guarantees come from dependency constraints and a resolution strategy such as failOnVersionConflict().

Groovy in a Maven build. Maven has no scripting of its own, so a build step that needs real logic (patching a generated file, a conditional check) reaches for gmavenplus-plugin — the maintained successor to the old groovy-maven-plugin/GMaven. Its execute goal runs an inline or external Groovy script bound to a phase; it also compiles src/main/groovy and can generate GroovyDoc. Gradle needs no equivalent: build scripts are already Groovy (or Kotlin), and the core groovy plugin compiles Groovy sources directly.

<plugin>
  <groupId>org.codehaus.gmavenplus</groupId>
  <artifactId>gmavenplus-plugin</artifactId>
  <version>5.1.0</version>
  <executions>
    <execution>
      <goals>
        <goal>execute</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <scripts>
      <script>file:///${project.basedir}/src/build/patch-version.groovy</script>
    </scripts>
  </configuration>
</plugin>
plugins {
    id 'groovy'   // compiles src/main/groovy; build-time logic goes straight in build.gradle
}

Wiring it together

With the plugins above bound to their phases, mvn verify runs Surefire, then Failsafe, then the JaCoCo check, then the Checkstyle/PMD/SpotBugs check goals — one command that passes only if tests, coverage, and static analysis all pass. The Gradle equivalent is gradle check, which the test task and each analysis plugin hook into (the custom integration-test suite only when wired as shown above). That is the command a CI pipeline should gate merges on. The SonarQube scan (sonar:sonar / sonar) and the Maven Central deploy run after that gate, as separate CI steps — typically only on the main branch and on tags respectively.

Generating API Docs with javadoc

javadoc turns doc comments (/** …​ */ immediately before a declaration) into linked HTML. Block tags document the contract; inline tags format text. See the Javadoc tutorial section.

For the doc-comment format itself — block and inline tags, package/module docs, documentation inheritance, and worked examples — see Javadoc. Run the tool directly, or let the build call it:

javadoc -d apidocs -sourcepath src -subpackages com.example \
        -link https://docs.oracle.com/en/java/javase/25/docs/api/

mvn javadoc:javadoc      # output under target/site/apidocs
./gradlew javadoc        # output under build/docs/javadoc

Both mvn test and ./gradlew test run the JUnit suite through the build’s test task; writing those tests is covered in Testing.

See Also

  • Getting Started — installing a JDK and the first compile-and-run cycle.

  • Packages and Modules — the classpath and module path that javac, jdeps, and jlink operate on.

  • Testing — the junit-jupiter dependency and the suites mvn test and gradle test execute.

  • Annotations and Reflection — annotation processors that run as part of javac.

  • Maven Quality Plugins — the same coverage and static-analysis plugins configured for a Spring Boot Maven build, with multi-module aggregation.