Java and Jenkins have a long shared history — Jenkins itself runs on the JVM, and it was practically built with Java projects in mind. If you’re working with a Maven or Gradle-based Java application, setting up a solid CI/CD pipeline in Jenkins is one of the most well-trodden paths in the DevOps world. But “well-trodden” doesn’t mean “obvious,” especially once you move past a toy example into something with real tests, static analysis, artifact publishing, and deployment.
This guide walks through building a complete Jenkins pipeline for a Java project, from basic setup to a production-grade Jenkinsfile.
Understanding Jenkins and Java CI/CD
At a conceptual level, a Java CI/CD pipeline typically moves through these phases: checkout source code, resolve dependencies, compile, run unit tests, run static analysis, package the artifact (JAR/WAR), optionally build a container image, and deploy. Jenkins orchestrates all of this, delegating the actual build logic to Maven or Gradle, which already understand how to compile and package Java code.
Jenkins Architecture Refresher
Jenkins controllers coordinate scheduling and store pipeline definitions and history; agents execute the actual stages. For Java projects, agents need the JDK and build tool (Maven or Gradle) installed, either directly on the host or via a Docker image referenced in the pipeline, which is increasingly the preferred approach since it guarantees a consistent, reproducible build environment.
Prerequisites
- Jenkins server installed with admin access
- JDK installed on the build agent (or available via Docker image)
- Maven or Gradle installed (or bundled in your Docker build image)
- A Java project in a Git repository with a
pom.xmlorbuild.gradle
Step 1: Install Required Plugins
In Manage Jenkins > Plugins, install:
- Pipeline (usually pre-installed)
- Git Plugin
- Maven Integration Plugin
- JUnit Plugin — for displaying test results
- JaCoCo Plugin — for code coverage reporting (optional but recommended)
Step 2: Configure JDK and Maven as Jenkins Tools
Go to Manage Jenkins > Tools:
- Under JDK installations, add a JDK (Jenkins can auto-install from Adoptium, or point to a manually installed JDK path)
- Under Maven installations, add Maven, again either auto-installed or pointed at a manual install
Naming these clearly (e.g., JDK-17, Maven-3.9) matters because you’ll reference these exact names in your Jenkinsfile.
Step 3: A Basic Declarative Jenkinsfile
pipeline {
agent any
tools {
jdk 'JDK-17'
maven 'Maven-3.9'
}
stages {
stage('Checkout') {
steps {
git branch: 'main', url: 'https://github.com/yourorg/java-app.git'
}
}
stage('Build') {
steps {
sh 'mvn clean compile'
}
}
stage('Unit Tests') {
steps {
sh 'mvn test'
}
post {
always {
junit '**/target/surefire-reports/*.xml'
}
}
}
stage('Package') {
steps {
sh 'mvn package -DskipTests'
}
}
stage('Archive Artifact') {
steps {
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
}
}
}
}
This covers the core loop. Now let’s expand it into something closer to a real production pipeline.
Step 4: Full Production-Grade Pipeline
pipeline {
agent any
tools {
jdk 'JDK-17'
maven 'Maven-3.9'
}
environment {
SONAR_SCANNER_HOME = tool 'SonarScanner'
ARTIFACT_VERSION = "1.0.${BUILD_NUMBER}"
DOCKER_IMAGE = "myorg/java-app:${ARTIFACT_VERSION}"
}
options {
timestamps()
buildDiscarder(logRotator(numToKeepStr: '20'))
timeout(time: 30, unit: 'MINUTES')
}
stages {
stage('Checkout') {
steps {
git branch: 'main', url: 'https://github.com/yourorg/java-app.git'
}
}
stage('Build') {
steps {
sh 'mvn clean compile'
}
}
stage('Unit Tests') {
steps {
sh 'mvn test'
}
post {
always {
junit '**/target/surefire-reports/*.xml'
jacoco execPattern: '**/target/jacoco.exec'
}
}
}
stage('Static Analysis') {
steps {
withSonarQubeEnv('SonarQube') {
sh 'mvn sonar:sonar -Dsonar.projectKey=java-app'
}
}
}
stage('Quality Gate') {
steps {
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
stage('Package') {
steps {
sh "mvn versions:set -DnewVersion=${ARTIFACT_VERSION}"
sh 'mvn package -DskipTests'
}
}
stage('Archive Artifact') {
steps {
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
}
}
stage('Publish to Artifactory') {
steps {
rtUpload(
serverId: 'artifactory-server',
spec: '''{
"files": [{
"pattern": "target/*.jar",
"target": "libs-release-local/com/myorg/java-app/"
}]
}'''
)
}
}
stage('Build Docker Image') {
steps {
sh "docker build -t ${DOCKER_IMAGE} ."
}
}
stage('Push Docker Image') {
steps {
withCredentials([usernamePassword(credentialsId: 'dockerhub-creds', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
sh "echo \$DOCKER_PASS | docker login -u \$DOCKER_USER --password-stdin"
sh "docker push ${DOCKER_IMAGE}"
}
}
}
stage('Deploy to Kubernetes') {
when {
branch 'main'
}
steps {
sh "kubectl set image deployment/java-app java-app=${DOCKER_IMAGE} --namespace=production"
}
}
}
post {
always {
cleanWs()
}
success {
echo "Build ${ARTIFACT_VERSION} completed successfully."
}
failure {
echo 'Pipeline failed - check the stage logs above.'
}
}
}
Step 5: Gradle-Based Alternative
If your project uses Gradle instead of Maven, the pipeline structure is nearly identical, just swapping the build tool commands:
pipeline {
agent any
tools {
jdk 'JDK-17'
}
stages {
stage('Checkout') {
steps {
git branch: 'main', url: 'https://github.com/yourorg/java-gradle-app.git'
}
}
stage('Build') {
steps {
sh './gradlew clean build -x test'
}
}
stage('Unit Tests') {
steps {
sh './gradlew test'
}
post {
always {
junit '**/build/test-results/test/*.xml'
}
}
}
stage('Package') {
steps {
sh './gradlew assemble'
}
}
stage('Archive Artifact') {
steps {
archiveArtifacts artifacts: 'build/libs/*.jar', fingerprint: true
}
}
}
}
Using the Gradle wrapper (gradlew) rather than a system-installed Gradle avoids version mismatches between what a developer’s machine has and what the CI agent has.
Step 6: Using Docker Agents Instead of Static Tool Installs
A cleaner and more reproducible approach is to skip installing JDK/Maven on the Jenkins agent entirely and instead run the build inside a container:
pipeline {
agent {
docker { image 'maven:3.9-eclipse-temurin-17' }
}
stages {
stage('Build and Test') {
steps {
sh 'mvn clean verify'
}
}
}
}
This guarantees every build uses the exact same JDK and Maven version, regardless of what’s installed on the underlying agent host.
Multi-Module Maven Projects
For larger Java projects split into multiple Maven modules, the same top-level mvn commands work since Maven handles the module build order via the reactor — no special Jenkins configuration is needed beyond making sure the parent pom.xml is at your repository root.
Parallel Testing for Faster Feedback
For large test suites, splitting tests across parallel stages cuts build time significantly:
stage('Tests') {
parallel {
stage('Unit Tests') {
steps {
sh 'mvn test -Dtest=**/*UnitTest.java'
}
}
stage('Integration Tests') {
steps {
sh 'mvn verify -Dtest=**/*IT.java'
}
}
}
}
Troubleshooting
Build fails with “JAVA_HOME not set”: Confirm the tools block references a JDK name that actually exists under Manage Jenkins > Tools, and that it matches exactly (case-sensitive).
Tests pass locally but fail in Jenkins: Usually an environment difference — check for hardcoded local paths, timezone assumptions, or a missing test database/service that exists locally but not on the agent.
mvn: command not found even after configuring the Maven tool: The tools block only injects Maven into PATH for sh/bat steps within stages that come after it’s declared at the pipeline level — verify it’s set at the top of the pipeline, not inside an individual stage.
Out of memory during build: Increase agent JVM heap via MAVEN_OPTS environment variable, e.g., MAVEN_OPTS='-Xmx2048m'.
Security Best Practices
- Never commit
.m2/settings.xmlwith plaintext repository credentials — use Jenkins Credentials and inject them viawithCredentialsor a Maven settings template - Scan dependencies for known vulnerabilities using the OWASP Dependency-Check Maven plugin as a pipeline stage
- Sign released artifacts (JAR signing / GPG) if publishing to public repositories
- Use a private artifact repository (Artifactory/Nexus) rather than exposing build artifacts publicly
FAQs
Should I use Maven or Gradle with Jenkins? Both work equally well; Jenkins doesn’t have a preference. Use whichever your team and project already standardize on — Gradle tends to be faster for large multi-module builds due to incremental compilation and build caching.
How do I speed up repeated builds? Cache the local Maven/Gradle dependency directory (~/.m2 or ~/.gradle/caches) across builds using a persistent volume if running in Docker, or a shared agent workspace.
Can Jenkins run my Spring Boot integration tests that need a database? Yes — either spin up a database container in the pipeline using docker run before the test stage, or use Testcontainers within your test suite, which Jenkins handles transparently as long as Docker is available on the agent.
How do I version my JAR automatically per build? Use mvn versions:set -DnewVersion=$BUILD_NUMBER (or a semantic versioning plugin) as shown in the pipeline above, before packaging.
What’s the best way to handle multi-branch Java projects? Set up a Multibranch Pipeline job pointing at your repo — Jenkins auto-discovers branches and PRs and runs the shared Jenkinsfile against each, letting you use when { branch 'x' } conditions for environment-specific stages.
Summary
A solid Jenkins pipeline for Java projects follows a predictable shape: checkout, build, test, analyze, package, and deploy — with Maven or Gradle doing the heavy lifting at each step and Jenkins orchestrating the sequence and reporting. Starting with a simple declarative pipeline and layering in SonarQube analysis, Docker builds, artifact publishing, and deployment stages gets you to a production-ready CI/CD setup without much friction, especially since Jenkins and the Java ecosystem have been refined together for well over a decade.