Maven 4 - Part I - Easier Versions

Overview

This is the first article in this series about Apache Maven 4. Currently Apache Maven 4 is in alpha state (alpha-13). You can already download it and of course use it (I recommend to test things to see, if something strange happens. If you find problems, please report them) but I would not recommend to use it in production in the current stage.

Let us start with a basic example of a Maven POM file which looks similar like this (For the sake of clarity no dependencies defined):

 1<project
 2    xmlns="http://maven.apache.org/POM/4.0.0"
 3    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
 5
 6  <modelVersion>4.0.0</modelVersion>
 7
 8  <parent>
 9    <groupId>com.soebes.smpp</groupId>
10    <artifactId>smpp</artifactId>
11    <version>6.0.5</version>
12    <relativePath/>
13  </parent>
14
15  <groupId>com.soebes.examples.maven4</groupId>
16  <artifactId>basic</artifactId>
17  <version>1.0.0-SNAPSHOT</version>
18  <name>Basic Example</name>
19  ..
20</project>

Let us take a deeper look into this. So we see a parent, which is used to inherit configuration setup for plugins etc. to prevent repetition for each project. The next parts are the usual definition of the groupId, artifactId and the version (this combination is often abbreviated with GAV). So we would like to build that setup with Maven 4. So first let us check, if we have installed the correct Maven version. The output should look similar to this:

1$> mvn --version
2Apache Maven 4.0.0-alpha-13 (0a6a5617fe5ef65c44f05903491e170d92cf37fc)
3Maven home: /projects/tools/maven
4Java version: 22, vendor: Oracle Corporation, runtime: /projects/.sdkman/candidates/java/22-open
5Default locale: en_DE, platform encoding: UTF-8
6OS name: "mac os x", version: "14.0", arch: "aarch64", family: "mac"

Ok, let us try to build that example project via:

1mvn clean verify

and that will produce the following (a bit lengthy) output:

 1[INFO] Unable to find the root directory. Create a .mvn directory in the root directory or add the root="true" attribute on the root project's model to identify it.
 2[INFO] Scanning for projects...
 3[INFO] 
 4[INFO] -------------------------------------------< com.soebes.examples.maven4:basic >-------------------------------------------
 5[INFO] Building Basic Example 1.0.0-SNAPSHOT
 6[INFO]   from pom.xml
 7[INFO] ---------------------------------------------------------[ jar ]----------------------------------------------------------
 8[INFO] 
 9[INFO] --- clean:3.3.2:clean (default-clean) @ basic ---
10[INFO] Deleting /projects/basic/target
11[INFO] 
12[INFO] --- enforcer:3.4.1:enforce (enforce-maven) @ basic ---
13[INFO] Rule 0: org.apache.maven.enforcer.rules.RequireSameVersions passed
14[INFO] Rule 1: org.apache.maven.enforcer.rules.version.RequireMavenVersion passed
15[INFO] Rule 2: org.apache.maven.enforcer.rules.dependency.BannedDependencies passed
16[INFO] Rule 3: org.apache.maven.enforcer.rules.RequireNoRepositories passed
17[INFO] Rule 4: org.apache.maven.enforcer.rules.RequirePluginVersions passed
18[INFO] Rule 5: org.apache.maven.enforcer.rules.property.RequireProperty passed
19[INFO] Rule 6: org.apache.maven.enforcer.rules.property.RequireProperty passed
20[INFO] Rule 7: org.apache.maven.enforcer.rules.property.RequireProperty passed
21[INFO] 
22[INFO] --- jacoco:0.8.11:prepare-agent (default) @ basic ---
23[INFO] argLine set to -javaagent:/.m2/repository/org/jacoco/org.jacoco.agent/0.8.11/org.jacoco.agent-0.8.11-runtime.jar=destfile=/projects/basic/target/jacoco.exec
24[INFO] 
25[INFO] --- resources:3.3.1:resources (default-resources) @ basic ---
26[INFO] skip non existing resourceDirectory /projects/basic/src/main/resources
27[INFO] skip non existing resourceDirectory /projects/basic/src/main/resources-filtered
28[INFO] 
29[INFO] --- compiler:3.12.1:compile (default-compile) @ basic ---
30[INFO] Recompiling the module because of changed source code.
31[INFO] Compiling 1 source file with javac [debug release 11] to target/classes
32[INFO] 
33[INFO] --- resources:3.3.1:testResources (default-testResources) @ basic ---
34[INFO] skip non existing resourceDirectory /projects/basic/src/test/resources
35[INFO] skip non existing resourceDirectory /projects/basic/src/test/resources-filtered
36[INFO] 
37[INFO] --- compiler:3.12.1:testCompile (default-testCompile) @ basic ---
38[INFO] No sources to compile
39[INFO] 
40[INFO] --- surefire:3.2.3:test (default-test) @ basic ---
41[INFO] No tests to run.
42[INFO] 
43[INFO] --- jar:3.3.0:jar (default-jar) @ basic ---
44[INFO] Building jar: /projects/basic/target/basic-1.0-SNAPSHOT.jar
45[INFO] 
46[INFO] --- site:3.12.1:attach-descriptor (attach-descriptor) @ basic ---
47[INFO] Skipping because packaging 'jar' is not pom.
48[INFO] 
49[INFO] --- jacoco:0.8.11:report (default) @ basic ---
50[INFO] Skipping JaCoCo execution due to missing execution data file.
51[INFO] Copying com.soebes.examples.maven4:basic:pom:1.0-SNAPSHOT to project local repository
52[INFO] Copying com.soebes.examples.maven4:basic:jar:1.0-SNAPSHOT to project local repository
53[INFO] Copying com.soebes.examples.maven4:basic:pom:consumer:1.0-SNAPSHOT to project local repository
54[INFO] --------------------------------------------------------------------------------------------------------------------------
55[INFO] BUILD SUCCESS
56[INFO] --------------------------------------------------------------------------------------------------------------------------
57[INFO] Total time:  1.199 s
58[INFO] Finished at: 2024-03-26T23:20:12+01:00
59[INFO] --------------------------------------------------------------------------------------------------------------------------

If you have already have used Maven 3.X before, you might have spotted some differences. The first line shows:

1[INFO] Unable to find the root directory. Create a .mvn directory in the root directory or add the root="true" attribute on the root project's model to identify it.
2.

We will ignore that for now, because we will discuss that later in detail. Let's talk about versions.

SNAPSHOT vs. Release Version

In the initial example, you have seen, that we have used a literal version 1.0.0-SNAPSHOT, which is very common in Maven projects. The usual process is to start with a SNAPSHOT-version which is implied by the postfix -SNAPSHOT. This is the indicator, that it is not final yet or in other words, it will change. But, of course, there is a time to make that version final (immutable). Being more accurate, finalize the state of the software (artifact), which is indicated by the version. So we change the version to 1.0.0 (without the postfix -SNAPSHOT), which is now called a release version. This release version will be published in whatever manner (often in central repository, or in an internal repository). The next development cycle starts by changing the version from 1.0.0 to something like 1.1.0-SNAPSHOT. Now the circle starts from the beginning. So changing the version in the pom.xml everytime is something, which some people don't like and yes it's a bit cumbersome. That version change can also be handled by using the Maven Release Plugin, which is also not really liked by some people.

Easier Revisions

Starting with Maven 4, you can define a version property simply in the pom.xml like the following:

1...
2  <groupId>com.soebes.examples.maven4</groupId>
3  <artifactId>basic</artifactId>
4  <version>${revision}</version>
5  <name>Basic Example</name>
6...

For brevity reasons, only the relevant parts are being shown. If you build that via: mvn clean you will see the following:

 1..
 2[INFO] 
 3[INFO] -------------------------------------------< com.soebes.examples.maven4:basic >-------------------------------------------
 4[INFO] Building Basic Example ${revision}
 5[INFO]   from pom.xml
 6[INFO] ---------------------------------------------------------[ jar ]----------------------------------------------------------
 7[INFO] 
 8[INFO] --- clean:3.3.2:clean (default-clean) @ basic ---
 9[INFO] Deleting /projects/basic-revision/target
10[INFO] --------------------------------------------------------------------------------------------------------------------------
11[INFO] BUILD SUCCESS
12[INFO] --------------------------------------------------------------------------------------------------------------------------
13[INFO] Total time:  0.242 s
14[INFO] Finished at: 2024-03-26T23:22:12+01:00
15[INFO] --------------------------------------------------------------------------------------------------------------------------

Command Line Parameter

So it's a bit weird that you see Building Basic Example ${revision}. That means, this project does not have a version at all. The version is ${revision} which I assume is not, what you wanted nor expected. How can we define a particular version for our build (or for the resulting artifact)? This can be achieved by using a property via the command line like this:

1$ mvn clean -Drevision=1.2.0-SNAPSHOT

The output during building now changes into this:

 1...
 2[INFO] 
 3[INFO] -------------------------------------------< com.soebes.examples.maven4:basic >-------------------------------------------
 4[INFO] Building Basic Example 1.2.0-SNAPSHOT
 5[INFO]   from pom.xml
 6[INFO] ---------------------------------------------------------[ jar ]----------------------------------------------------------
 7[INFO] 
 8[INFO] --- clean:3.3.2:clean (default-clean) @ basic ---
 9[INFO] Deleting /projects/basic-revision/target
10[INFO] --------------------------------------------------------------------------------------------------------------------------
11[INFO] BUILD SUCCESS
12[INFO] --------------------------------------------------------------------------------------------------------------------------
13[INFO] Total time:  0.243 s
14[INFO] Finished at: 2024-03-26T23:21:12+01:00
15[INFO] --------------------------------------------------------------------------------------------------------------------------

The output Building Basic Example 1.2.0-SNAPSHOT now indicates, that the given version via property is being used as the version for the Maven project or more accurate for the resulting artifacts.

POM Property

This means, we can define any version we like, by just adding that option on command line every time we execute Maven. Hold on a second. Everytime? Yes everytime hm. Isn't there a way to avoid that? Yes, there is one. We can define the revision property in the pom.xml. That looks like this:

 1  ..
 2  <groupId>com.soebes.examples.maven4</groupId>
 3  <artifactId>basic</artifactId>
 4  <version>${revision}</version>
 5  <name>Basic Example</name>
 6
 7  <properties>
 8    <revision>1.0.0-SNAPSHOT</revision>
 9  </properties>
10..

Ok, now you can simply build like before without giving the revision everytime on command as an option:

 1$> mvn clean
 2..
 3[INFO] Scanning for projects...
 4[INFO] 
 5[INFO] -------------------------------------------< com.soebes.examples.maven4:basic >-------------------------------------------
 6[INFO] Building Basic Example 1.0.0-SNAPSHOT
 7[INFO]   from pom.xml
 8[INFO] ---------------------------------------------------------[ jar ]----------------------------------------------------------
 9[INFO] 
10[INFO] --- clean:3.3.2:clean (default-clean) @ basic ---
11[INFO] Deleting /projects/basic-revision-pom/target
12[INFO] --------------------------------------------------------------------------------------------------------------------------
13[INFO] BUILD SUCCESS
14[INFO] --------------------------------------------------------------------------------------------------------------------------
15[INFO] Total time:  0.238 s
16[INFO] Finished at: 2024-03-26T12:15:42+01:00
17[INFO] --------------------------------------------------------------------------------------------------------------------------

One could argue: Haven't we reached the same level as in the beginning, while defining the version literally in the pom? On the first glance it might look like this, but we have reached a different level of flexibility. Ok, let's try to build with a different version? Do we have to change the pom.xml file? No, there is no need for that anymore. You simply give a different version via command line mvn clean -Drevision=1.2.0-SNAPSHOT:

 1..
 2[INFO] 
 3[INFO] -------------------------------------------< com.soebes.examples.maven4:basic >-------------------------------------------
 4[INFO] Building Basic Example 1.2.0-SNAPSHOT
 5[INFO]   from pom.xml
 6[INFO] ---------------------------------------------------------[ jar ]----------------------------------------------------------
 7[INFO] 
 8[INFO] --- clean:3.3.2:clean (default-clean) @ basic ---
 9[INFO] Deleting /projects/basic-revision-pom/target
10[INFO] --------------------------------------------------------------------------------------------------------------------------
11[INFO] BUILD SUCCESS
12[INFO] --------------------------------------------------------------------------------------------------------------------------
13[INFO] Total time:  0.239 s
14[INFO] Finished at: 2024-03-26T12:21:16+01:00
15[INFO] --------------------------------------------------------------------------------------------------------------------------

That means, we can now overwrite the version within the pom.xml easily by giving any version we like, via the property on the command line. This is very helpful within an CI/CD tools (for example Jenkins, Circle CI, Github Actions or alike). That also prevents the manual change of the pom.xml everytime, you would like to change version.

Creating a Release

Also creating a release is very easy. Just use mvn deploy -Drevision=1.2.0. Ok, you have to have done the required setup to publish a release, but that's a different story.

The output looks similar to the following (removed the beginning of the output, because it's already shown in previous examples):

 1...
 2[INFO] 
 3[INFO] --- install:3.1.1:install (default-install) @ basic ---
 4[INFO] Deferring install for com.soebes.examples.maven4:basic:1.2.0 at end
 5[INFO] Installing /projects/basic-distro/target/basic-1.2.0.jar to /.m2/repository/com/soebes/examples/maven4/basic/1.2.0/basic-1.2.0.jar
 6[INFO] Installing /projects/basic-distro/pom.xml to /.m2/repository/com/soebes/examples/maven4/basic/1.2.0/basic-1.2.0-build.pom
 7[INFO] Installing /projects/basic-distro/target/consumer-4964754963249724515.pom to /.m2/repository/com/soebes/examples/maven4/basic/1.2.0/basic-1.2.0.pom
 8[INFO] 
 9[INFO] --- deploy:3.1.1:deploy (default-deploy) @ basic ---
10Uploading to releases: http://localhost:8081/nexus/content/repositories/releases/com/soebes/examples/maven4/basic/1.2.0/basic-1.2.0.jar
11Uploaded to releases: http://localhost:8081/nexus/content/repositories/releases/com/soebes/examples/maven4/basic/1.2.0/basic-1.2.0.jar (3.1 kB at 33 kB/s)
12Uploading to releases: http://localhost:8081/nexus/content/repositories/releases/com/soebes/examples/maven4/basic/1.2.0/basic-1.2.0-build.pom
13Uploading to releases: http://localhost:8081/nexus/content/repositories/releases/com/soebes/examples/maven4/basic/1.2.0/basic-1.2.0.pom
14Uploaded to releases: http://localhost:8081/nexus/content/repositories/releases/com/soebes/examples/maven4/basic/1.2.0/basic-1.2.0-build.pom (969 B at 88 kB/s)
15Uploaded to releases: http://localhost:8081/nexus/content/repositories/releases/com/soebes/examples/maven4/basic/1.2.0/basic-1.2.0.pom (2.2 kB at 199 kB/s)
16Downloading from releases: http://localhost:8081/nexus/content/repositories/releases/com/soebes/examples/maven4/basic/maven-metadata.xml
17Uploading to releases: http://localhost:8081/nexus/content/repositories/releases/com/soebes/examples/maven4/basic/maven-metadata.xml
18Uploaded to releases: http://localhost:8081/nexus/content/repositories/releases/com/soebes/examples/maven4/basic/maven-metadata.xml (530 B at 48 kB/s)
19[INFO] Copying com.soebes.examples.maven4:basic:pom:1.2.0 to project local repository
20[INFO] Copying com.soebes.examples.maven4:basic:jar:1.2.0 to project local repository
21[INFO] Copying com.soebes.examples.maven4:basic:pom:consumer:1.2.0 to project local repository
22[INFO] --------------------------------------------------------------------------------------------------------------------------
23[INFO] BUILD SUCCESS
24[INFO] --------------------------------------------------------------------------------------------------------------------------
25[INFO] Total time:  1.367 s
26[INFO] Finished at: 2024-03-26T23:50:23+01:00
27[INFO] --------------------------------------------------------------------------------------------------------------------------

Multi Module Builds

Ok, now lets make a step further to a more complex setup, a multi-module-build. This kind of build comprises of a structure like this:

 1.
 2|-- pom.xml (1)
 3|-- domain
 4|   |-- pom.xml
 5|   `-- src
 6|-- service
 7|   |-- pom.xml
 8|   `-- src
 9|-- service-client
10|   |-- pom.xml
11|   `-- src
12`-- webgui
13|-- pom.xml
14`-- src

A multi-module build is characterized by the fact, that the child modules (for example domain or service) always have a relation to the parent module (parent marked with (1)). The parent pom looks like this:

 1<project...>
 2
 3  <modelVersion>4.0.0</modelVersion>
 4
 5  <parent>
 6    <groupId>com.soebes.smpp</groupId>
 7    <artifactId>smpp</artifactId>
 8    <version>6.0.5</version>
 9    <relativePath/>
10  </parent>
11
12  <groupId>com.soebes.examples.j2ee</groupId>
13  <artifactId>jee-parent</artifactId>
14  <version>3.1.4-SNAPSHOT</version>
15  <packaging>pom</packaging>
16  ...
17  <modules>
18    ..
19    <module>webgui</module>
20    <module>domain</module>
21    <module>service</module>
22    <module>service-client</module>
23    ..
24  </modules>
25
26</project>

It is important to mention, that the list of modules referenced via <modules>...</modules> will create the relationship to the child modules. A child pom looks like this:

 1<project....>
 2
 3  <modelVersion>4.0.0</modelVersion>
 4
 5  <parent>
 6    <groupId>com.soebes.examples.j2ee</groupId>
 7    <artifactId>jee-parent</artifactId>
 8    <version>3.1.4-SNAPSHOT</version>
 9  </parent>
10
11  <artifactId>domain</artifactId>
12  ...
13</project>

It is crucial, that the specification parent refers exactly to the parent (referenced with (1)) in the multi-module build. Such a multi-module build may consist of several hundred or even thousands of child modules. This is then often divided into several sub-levels (similar to a directory tree). Theoretically, there is no limit here (only memory etc.). If you now imagine, that you have to change the version number, this leads as described at the beginning, that all pom.xml files have to be changed. However, this can be avoided by using a ${revision} property approach and simplified dramatically.

This results, that the parent POM receives a corresponding property and the child modules as well. Here the parent pom:

 1<project ...>
 2
 3  <modelVersion>4.0.0</modelVersion>
 4
 5  <parent>
 6    <groupId>com.soebes.smpp</groupId>
 7    <artifactId>smpp</artifactId>
 8    <version>6.0.5</version>
 9    <relativePath/>
10  </parent>
11
12  <groupId>com.soebes.examples.j2ee</groupId>
13  <artifactId>jee-parent</artifactId>
14  <version>${revision}</version>
15  <packaging>pom</packaging>
16
17  <properties>
18    <revision>1.0.0-SNAPSHOT</revision>
19  </properties>
20
21  ...
22  <modules>
23    ..
24    <module>webgui</module>
25    <module>domain</module>
26    <module>service</module>
27    <module>service-client</module>
28    ..
29  </modules>
30</project>

And exemplified by a child module (domain):

 1<project ...>
 2
 3  <modelVersion>4.0.0</modelVersion>
 4
 5  <parent>
 6    <groupId>com.soebes.examples.j2ee</groupId>
 7    <artifactId>jee-parent</artifactId>
 8    <version>${revision}</version>
 9  </parent>
10  ..
11  <artifactId>domain</artifactId>
12  ...
13</project>

Different Properties

Some will ask themself, could we use other properties than the used revision in the previous examples? Technically there are three properties available revision (already mentioned), sha1 and changelist. The usage and rules are the same as for revision. So you can create a combination like this <version>${revision}${sha1}${changelist}</version>. My recommendation is, use only a single one and that is the revision property. In the end it's your decision, which way you go with that. But to make things clear, you can NOT use other properties except the mentioned revision, sha1 and changelist.

Maven Configuration File

In contradiction to the previous approach, you can define the ${revision} in a different manner. You have to create a directory .mvn (in the root of your project) and put a file named maven.config into that directory, which contains the following: -Drevision=1.0.0-SNAPSHOT.

 1.
 2|-- .mvn
 3|-- pom.xml
 4|-- domain
 5|   |-- pom.xml
 6|   `-- src
 7|-- service
 8|   |-- pom.xml
 9|   `-- src
10|-- service-client
11|   |-- pom.xml
12|   `-- src
13`-- webgui
14|-- pom.xml
15`-- src

This is the equivalent of the previously mentioned command line approach. All things in maven.config will be added to the command line of Maven, as it would be given directly. The option for external configuration files exists since Maven 3.3.1 (ca. 9 years). The maven.config can contain more options for example -T 3.

Conclusion

The usage of the revision property (also the other two), is simplifying the versioning of artifacts. In consequence the release creation is simplified even without changing the pom.xml file(s). The most important part in relationship with Maven 4 is, that this works out-of-the-box. It not necessary to configure or use the flatten-maven-plugin anymore, as it was necessary with CI Friendly in Maven 3. This also means, that the use of e.g. version-maven-plugin to update the version in the pom.xml file, is not needed anymore. One of the things missing here, is the support of the maven-release-plugin creating tags in Git during the release creation, but that can be accomplished by adding some steps in your CI/CD pipeline.