Java and Maven as code

Page content

An automaton climbs from a chest with a note (the Maven Wrapper and the version file), the cook serves coffee in a Java cup (the JDK), an engineer with a gauge rejects a wrench (the enforcer), a chest rides to an automated bench (the pipeline), and install notes lie in a bin

Most Java READMEs start by asking you to install a JDK and Maven, so every developer and the build server end up with different versions. The Maven Wrapper, a version file read by mise and the Maven Enforcer Plugin move both versions into the repository. A developer clones the project and builds it with one command, with the same JDK and Maven as the team and the pipeline.

Scoop covers the developer’s tools and the TomEE as code series covers the server. This post covers the build tools.

The Maven Wrapper

The Maven Wrapper adds these files to the repository:

mvnw                                # Linux and macOS
mvnw.cmd                            # Windows
.mvn/wrapper/maven-wrapper.properties

The properties file names the Maven version. Developers run the script instead of mvn:

./mvnw package

The first run downloads that Maven into a cache in the user’s home folder. Nobody installs Maven, everyone runs the same version, and an upgrade is one line in the properties file. Any installed Maven generates the files once:

mvn wrapper:wrapper -Dmaven=3.9.9

Recent wrapper versions use scripts only, with no JAR to commit. Behind a corporate proxy, set distributionUrl to the internal repository.

Maven also reads three other files from .mvn/, with or without the wrapper.

FileContent
.mvn/maven.configDefault command-line options, one per line, such as batch mode or a default profile
.mvn/jvm.configOptions for the JVM that runs Maven, such as more memory for a large build
.mvn/extensions.xmlMaven extensions, loaded before the build starts

Another project’s Maven

The wrapper protects only the builds that run through it. Spurious errors appear when a developer types mvn and runs a Maven installed for another project, or when the IDE builds with its embedded Maven or one global Maven.

  • Keep Maven off the PATH, so ./mvnw is the only way to build. A global Maven is needed only once, to generate the wrapper, and another project’s wrapper can do that too.
  • Point the IDE at the wrapper. IntelliJ IDEA has a “Use Maven wrapper” option for the Maven home path, and the Java tools for VS Code prefer the wrapper when it exists.
  • Require the exact version in the enforcer, as shown below.

A JDK version file read by mise

Maven runs on Java, so the wrapper needs a JDK it cannot install. Gradle toolchains download a missing JDK. Maven toolchains only select among installed JDKs, and their configuration lives in the user’s home folder. The JDK needs a tool outside Maven.

mise reads a mise.toml file in the project:

[tools]
java = "temurin-21"

Entering the folder activates that JDK, and mise install downloads it if it is missing. The file does not list Maven, because the wrapper already pins it. SDKMAN does the same with .sdkmanrc on Linux, macOS or WSL.

Other ways to provide the JDK

On Windows, a team that already describes its tools with Scoop can add the JDK to that script. scoop reset switches the JDK for the whole user, so a script in the repository activates the project’s JDK in the current terminal only:

# dev.ps1, at the root of the project
scoop install temurin21-jdk          # does nothing if it is already installed
$env:JAVA_HOME = scoop prefix temurin21-jdk
$env:PATH = "$env:JAVA_HOME\bin;$env:PATH"

A developer runs . .\dev.ps1 in a new terminal, then .\mvnw.cmd package.

A dev container keeps the JDK off the machine. .devcontainer/devcontainer.json names an image that includes it:

{
    "name": "app",
    "image": "mcr.microsoft.com/devcontainers/java:21"
}
OptionPer-project JDKExact versionSame declaration in a Linux pipelineNeeds
miseYes, on entering the folderYesYesmise
Scoop and dev.ps1Yes, per terminalLimited to what the bucket offersNoScoop, Windows only
Dev containerYesYesYesDocker, and WSL on Windows

Fail fast with the enforcer

A developer can still build with a system mvn or the wrong JDK. The Maven Enforcer Plugin stops the build at its start:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-enforcer-plugin</artifactId>
    <executions>
        <execution>
            <id>enforce-toolchain</id>
            <goals><goal>enforce</goal></goals>
            <configuration>
                <rules>
                    <requireJavaVersion>
                        <version>[21,22)</version>
                    </requireJavaVersion>
                    <requireMavenVersion>
                        <version>[3.9.9]</version>
                    </requireMavenVersion>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

A wrong version fails in the first second, and the message states the expected version. [3.9.9] accepts only that version, while [3.9.9,) would accept any later Maven, including another project’s. Keep it equal to the version in the wrapper’s properties file.

The pipeline reads the same files

With mise, the pipeline installs from mise.toml and runs the wrapper. On GitHub Actions:

- uses: jdx/mise-action@v2
- run: ./mvnw -B verify

A Java upgrade is one line in mise.toml, and the same pull request updates the pipeline and every developer.

The layers

LayerDescribed byReplaces
Developer toolsA Scoop scriptA setup guide for the laptop
JDKmise.toml, a Scoop script or a dev container“Install JDK 21” in the README
MavenThe Maven Wrapper and .mvn/“Install Maven 3.9” in the README
ServerThe TomEE or Cargo plugin and an overlay folderA server installed by hand