Wrappers: mvnw and gradlew
Pin the exact Maven or Gradle version in the repository so every laptop, CI agent and Docker build runs the same build tool.
./mvnw <phases or goals> # Maven, version from .mvn/wrapper/maven-wrapper.properties
./gradlew <tasks> # Gradle, version from gradle/wrapper/gradle-wrapper.propertiesA wrapper is a small script committed to the repository that downloads a fixed version of the build tool on first use and then runs it. ./mvnw verify and ./gradlew build behave like mvn and gradle, except that the version comes from the repository instead of from whatever happens to be installed.
Why it matters
Build tools change behaviour between releases: default plugin versions in Maven, deprecations that become errors in Gradle. Without a wrapper, a new developer, a CI image and a Docker build can each run a different version and produce different results, and the failure only shows up on one of them. With a wrapper, upgrading the build tool is a commit that goes through review like any other change, and nobody needs Maven or Gradle installed at all, only a JDK.
What to commit
- Maven:
mvnw,mvnw.cmdand.mvn/wrapper/maven-wrapper.properties. Recent versions of the Maven Wrapper default to a script-only mode with no JAR to commit. - Gradle:
gradlew,gradlew.bat,gradle/wrapper/gradle-wrapper.propertiesandgradle/wrapper/gradle-wrapper.jar. The JAR is required; do not let a broad*.jarrule in.gitignoredrop it.
The properties file holds distributionUrl, which is the version pin. Downloads are cached under ~/.m2/wrapper and ~/.gradle/wrapper/dists, so only the first run is slow.
Generating and upgrading
Maven: mvn wrapper:wrapper -Dmaven=<version>, or run the same goal through ./mvnw to upgrade. Gradle: gradle wrapper --gradle-version <version>, then ./gradlew wrapper --gradle-version ... for later upgrades. Gradle's documentation suggests running the upgrade command a second time so the scripts and JAR also come from the new version.
Mistakes that bite
A script committed from Windows can lose its executable bit, and CI fails with Permission denied. Fix it once with git update-index --chmod=+x mvnw (or gradlew). Running plain mvn or gradle out of habit skips the pin entirely, so use the wrapper in README files, CI and Dockerfiles alike.
A wrapper pins the build tool, not the JDK. Maven still uses JAVA_HOME or the java on the path; Gradle uses that to run itself and a toolchain, if configured, to compile. Check ./mvnw -v or ./gradlew -v on CI to see both versions.
Trusting the download
The wrapper downloads and runs code, so it is part of your supply chain. Both wrappers accept a distributionSha256Sum property that makes them refuse a distribution with the wrong checksum. For Gradle, also check that a pull request has not swapped gradle-wrapper.jar for a modified one; the Gradle project publishes a wrapper validation action for GitHub workflows that compares it with the official checksums.
Example
# Maven: add the wrapper to an existing project (needs Maven installed once)
# Versions shown are examples: use the current release or newer
mvn wrapper:wrapper -Dmaven=3.9.11
git add mvnw mvnw.cmd .mvn/wrapper/maven-wrapper.properties
# Gradle: add the wrapper (needs Gradle installed once)
gradle wrapper --gradle-version 9.1.0
git add gradlew gradlew.bat gradle/wrapper/
# Scripts committed from Windows may lose the executable bit
git update-index --chmod=+x mvnw gradlew
# From now on, everyone uses the wrapper
./mvnw -v
./gradlew -vWhen to use it
- Let a new developer clone an orders service and build it with only a JDK installed.
- Upgrade Gradle across a team in one reviewed commit instead of asking everyone to reinstall.
- Make a Docker build use the same Maven version as CI so images match the tested build.
More examples
The two properties files
Changing the version in distributionUrl is the whole upgrade, and it shows up in code review as a one-line diff. The escaped colon in the Gradle URL is normal properties-file syntax.
# Versions shown are examples: use the current releases or newer
# .mvn/wrapper/maven-wrapper.properties
wrapperVersion=3.3.2
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
# gradle/wrapper/gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/distsUpgrading Gradle with a checksum
With distributionSha256Sum set in gradle-wrapper.properties, the wrapper refuses a download whose checksum does not match. The second run regenerates gradlew and gradle-wrapper.jar with the new release's own copies.
# Take the SHA-256 from the checksums published with the Gradle release
./gradlew wrapper --gradle-version 9.1.0 \
--gradle-distribution-sha256-sum "$GRADLE_DIST_SHA256"
# Run once more so the scripts and JAR come from the new version;
# pass the checksum again so the pin stays in the properties file
./gradlew wrapper --gradle-version 9.1.0 \
--gradle-distribution-sha256-sum "$GRADLE_DIST_SHA256"
git diff --stat gradle/ gradlew gradlew.batUsing the wrapper in a Docker build
Copying the wrapper and POM before the source lets Docker reuse the downloaded Maven and dependencies when only code changes. dependency:go-offline does not catch every plugin a build resolves later, so the package step may still download a little.
FROM eclipse-temurin:25-jdk AS build
WORKDIR /src
# Wrapper and POM first: this layer is cached until they change
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
RUN ./mvnw -B -ntp dependency:go-offline
# Source changes only invalidate the layers from here on
COPY src src
RUN ./mvnw -B -ntp package -DskipTests
Discussion