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.