Maven 4: What’s New
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is the overview of Apache Maven 4. Maven 4.0.0-rc-7 is a release candidate, not a final release: the release notes
call it a version "intended for testing and feedback", say it is expected to be the last candidate before 4.0.0, and no 4.0.0 had
been published when this page was written. Everything shown here can still change before the final release. The page is
written against the 3.10.0 line (the current Maven 3 release, with 3.9.16 as the other supported one: see
Maven Versions & Release History) and every
Maven 4 example was run on 4.0.0-rc-7 on 2026-10-16, mostly on JDK 26 and once on JDK 17. The examples use the tidepool project
(a parent POM with the tidepool-model, tidepool-engine and tidepool-cli subprojects) in a scratch directory, with a scratch
local repository so that nothing touched ~/.m2. Paths were shortened to /work/..., and the lines
Loaded N auto-discovered prefixes ... that rc-7 prints on the first request to each repository were filtered out; other trimming is
marked in the text.
The page covers every item of the official "What’s new in Maven 4?" article, one short section each, and sends you to the sibling pages for depth: Maven 4: POM Model & Consumer POM, Maven 4: Lifecycle and Migrating to Maven 4.
The big idea: separate the build from the consumers
For twenty years the POM has served two audiences. Maven itself reads it to build the project (plugins, properties, profiles,
modules). Everybody who depends on the artifact (other Maven projects, Gradle, IDEs, scanners, Maven Central) reads the very same
file from the repository to learn its dependencies. Because the whole ecosystem parses that file, its schema, model version 4.0.0,
could not change without breaking someone. Maven 4 breaks that deadlock by treating them as two documents: the build POM in
your source tree may use the new model version 4.1.0, and what is installed and deployed for consumers is a consumer POM that
still says 4.0.0. Most of what is new is either a consequence of that split or something the split now permits.
The section Build POM and consumer POM below shows the files this produces.
At a glance
| Area | Maven 3.10.0 | Maven 4.0.0-rc-7 | Depth |
|---|---|---|---|
Java to run Maven |
Java 8 (3.9 and 3.10) |
Java 17; the code you compile can still target an older release |
|
POM model |
|
|
|
Multi-module |
|
|
Subprojects instead of modules, Parent and version inference |
Root directory |
Convention ( |
|
|
Source directories |
|
|
|
CI-friendly versions |
|
|
|
Resuming a failed build |
|
|
|
Failing on warnings |
Not available |
|
|
Optional profiles |
Missing profile: warning |
Missing profile: error; |
|
Lifecycle |
Ordered list of phases, |
Tree of phases, |
|
Password encryption |
|
|
|
Upgrade help |
None |
|
Java 17 to run Maven
Maven 4 needs Java 17 or newer to run. Compiling for an older release is unchanged: you still choose it with the compiler plugin
(maven.compiler.release) or a toolchain, see Compiling & Toolchains.
rc-7 started on a JDK 17 (JetBrains Runtime) without problems:
$ JAVA_HOME=$HOME/Library/Java/JavaVirtualMachines/jbr-17.0.14/Contents/Home mvn -v
Apache Maven 4.0.0-rc-7 (6d8f301b38af7399b36b862969d520974944f0b9)
Maven home: /work/tools/apache-maven-4.0.0-rc-7
Java version: 17.0.14, vendor: JetBrains s.r.o., runtime: /Users/example/Library/Java/JavaVirtualMachines/jbr-17.0.14/Contents/Home
(The Default locale, OS name and later lines were left out.) Java 17 became the minimum with Maven 4.0.0-alpha-13, in March 2024.
CI images that still run Maven on Java 8 or 11 must move; the project being built does not have to.
Model version 4.1.0 and the 3.x error you will see
A POM whose root element is declared in the namespace http://maven.apache.org/POM/4.1.0 is a model 4.1.0 POM. Maven 4 still builds
every 4.0.0 POM unchanged, so nothing forces you to move. Maven 3 cannot read 4.1.0, and the failure is confusing, because it complains
about the first new attribute it meets rather than about the version. This is the tidepool parent POM used below on Maven 3.10.0
(3.9.16 prints the same):
$ mvn verify
[INFO] Scanning for projects...
[ERROR] [ERROR] Some problems were encountered while processing the POMs:
[ERROR] Malformed POM /work/tidepool/pom.xml: Unknown attribute 'root' for tag 'project' (position: START_TAG seen ...ttps://maven.apache.org/xsd/maven-4.1.0.xsd"\n root="true">... @5:22) @ /work/tidepool/pom.xml, line 5, column 22
[ERROR] 'modelVersion' is missing. @ io.tidepool:tidepool-parent:${revision}, /work/tidepool/pom.xml, line 5, column 22
[ERROR] 'modelVersion' is missing. @ io.tidepool:tidepool-parent:${revision}, /work/tidepool/pom.xml, line 5, column 22
@
[ERROR] The build could not read 1 project -> [Help 1]
(Only the first lines are shown.) The practical rule: the repository that holds a 4.1.0 build POM can only be built with Maven 4. What you publish does not have this problem, because consumers get the 4.0.0 consumer POM. The element-by-element list of what 4.1.0 adds is on Maven 4: POM Model & Consumer POM.
Subprojects instead of modules
Since Java 9 the word "module" also means a Java Platform Module System module, and a Maven "module" is merely a project in a
subdirectory. Maven 4 therefore renames the concept: model 4.1.0 has a <subprojects> element (with <subproject> children) analogous to
<modules>, which stays usable but is deprecated. A project with subprojects is a multi-project build, one without is a single-project
build. The reactor itself is unchanged, see Multi-Module Builds & the Reactor.
The tidepool parent POM below has no list at all:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd"
root="true">
<groupId>io.tidepool</groupId>
<artifactId>tidepool-parent</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>
<properties>
<revision>1.0.0-SNAPSHOT</revision>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- build/pluginManagement with every plugin version pinned: 3.16.0 compiler, 3.5.1 jar, ... -->
</project>
When a parent has pom packaging and neither <subprojects> nor <modules>, Maven 4 adds every direct subdirectory that contains a
pom.xml. Each subproject below inherits and depends without repeating coordinates (explained in Parent and version inference):
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd">
<parent/>
<artifactId>tidepool-engine</artifactId>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
</dependency>
</dependencies>
</project>
Running mvn clean verify on rc-7 found all three subprojects and ordered them from the dependencies:
$ mvn clean verify
[INFO] Scanning for projects...
[INFO] Reactor Build Order:
[INFO] tidepool-parent [pom]
[INFO] tidepool-model [jar]
[INFO] tidepool-engine [jar]
[INFO] tidepool-cli [jar]
[INFO] Building tidepool-parent 1.0.0-SNAPSHOT [1/4]
[INFO] from pom.xml
...
[INFO] tidepool-parent .................................... SUCCESS [ 0.645 s]
[INFO] tidepool-model ..................................... SUCCESS [ 0.694 s]
[INFO] tidepool-engine .................................... SUCCESS [ 0.096 s]
[INFO] tidepool-cli ....................................... SUCCESS [ 0.045 s]
(Only the reactor lines are shown; the ... marks the removed plugin output.) The same discovery is also what makes <parent/> find the
parent: with an empty <parent/> Maven looks for a pom.xml in ...
Parent and version inference
Maven 4 finally ships MNG-624, opened in 2005: with model 4.1.0 you need not repeat the parent version, nor the version of a
dependency on another project of the same reactor. You can write <parent><relativePath>..</relativePath></parent>, the shorthand
<parent/> (parent in ..), or just leave out <version>, <groupId> or <artifactId> of the parent. A dependency on a
sibling needs only groupId and artifactId. The tidepool-engine POM above does exactly this, and the dependency on
tidepool-model has no version. The result is that a version bump is a change in one file.
The inferred values are written back into the consumer POM, so readers never see the shorthand (see Build POM and consumer POM).
CI-friendly versions without the flatten plugin
Maven 3.5.0 added ${revision}, ${sha1} and ${changelist} as version placeholders, but publishing a POM that still contained the
placeholder broke consumers, so everybody added the Flatten Maven Plugin. In Maven 4 the consumer POM is generated with the placeholder
replaced, and any property name works, not just the three predefined ones. The tidepool parent above has <version>${revision}</version>
and no flatten plugin; mvn install on rc-7 produced a consumer POM with the real version (see the next section). Define the property on the
command line (-Drevision=4.0.1), in .mvn/maven.config, or in the root or parent POM. The full treatment, including the Maven 3 workflow it
replaces, is on Maven CI-Friendly Versions.
Build POM and consumer POM
mvn install on rc-7 put four files for each subproject into the scratch local repository (shown here for tidepool-engine):
tidepool-engine-1.0.0-SNAPSHOT-build.pom
tidepool-engine-1.0.0-SNAPSHOT.jar
tidepool-engine-1.0.0-SNAPSHOT.pom
maven-metadata-local.xml
(_remote.repositories was left out.) The -build.pom is the file from your source tree, installed with the classifier build, and it is
the only one that is model 4.1.0. The plain .pom, the one every consumer reads, is generated:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>tidepool-engine</artifactId>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
The model version is 4.0.0, the <parent/> shorthand has become a real parent with a version, and the dependency has its version.
By default the consumer POM keeps the parent reference, which is why the parent POM is installed too. Setting the user property
maven.consumer.pom.flatten=true goes further and produces a flattened POM. The same build with -Dmaven.consumer.pom.flatten=true:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-engine</artifactId>
<version>1.0.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>1.0.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
The parent is gone and the group and version are taken from it. Flattening is off by default because it changes which dependencies are listed (only compile and runtime ones, and managed versions only when used); the official comparison table says that the consumer POM keeps third-party dependency information, repository information, project information and the deployment information, while POM properties and plugin configuration exist only in the build POM. Where the consumer POM cannot be expressed in 4.0.0 (a condition-based profile, see Profiles: optional ids and condition activation), Maven warns and may publish a 4.1.0 consumer POM. Pitfalls and the Mermaid flow of how the model builder produces each file are on Maven 4: POM Model & Consumer POM.
The root directory and its properties
Maven always needed to know where the project root is (to find .mvn/maven.config, the parent, shared files). Until now the only way
to tell it was a .mvn directory, and there was no official property to refer to that directory from a POM (people used internal
properties instead, now removed or deprecated, see MNG-7038). Maven 4 adds a boolean root attribute on <project> (model 4.1.0
only; default false), and three properties:
| Property | Scope | Value | Always set |
|---|---|---|---|
|
Project |
The directory with |
No |
|
Session |
The directory Maven was started in, or the one holding the |
Yes |
|
Session |
The root directory of the top project |
No |
${basedir} is the base directory of each individual subproject, as before. When no root can be found rc-7 does not stay silent: on a
single-project build without .mvn and without the attribute, it prints this warning on the first line of output:
[WARNING] 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.
The new plugin generation depends on it. The 4.x compiler plugin refuses to run without a root (see Declaring source directories with sources), so a .mvn directory or
root="true" is part of the move to Maven 4 even for projects that do not need the properties.
Declaring source directories with sources
Maven 3 has <sourceDirectory> and <testSourceDirectory>, one directory each, so a second source root needed the Build Helper plugin.
Maven 4 adds <build><sources>, a repeatable <source> with a <scope> (main or test), a <directory> and, in the compiler
plugin’s documentation, include and exclude filters and per-source target releases for multi-release projects and the module source
hierarchy. A scratch project with two main source roots:
<build>
<sources>
<source>
<scope>main</scope>
<directory>src/main/java</directory>
</source>
<source>
<scope>main</scope>
<directory>src/extra/java</directory>
</source>
</sources>
</build>
With the 3.x compiler plugin line, 3.16.0, both directories were compiled (Compiling 2 source files). The 4.x line, 4.0.0-beta-5, which
is the first to read the new element natively, produced both classes (A.class and Extra.class) too, but only after the build had a root
directory. In the same project without root="true" (and without .mvn):
[INFO] --- compiler:4.0.0-beta-5:compile (default-compile) @ srcs ---
[ERROR] COMPILATION ERROR: 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.
[INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE
Note that the 4.x plugin lines (compiler 4.0.0-beta-5, install and deploy 4.0.0-beta-3, resources 4.0.0-beta-2) are the ones that
use the new Maven API, and they only run on Maven 4.
New artifact types and the bom packaging
A dependency’s <type> selects the artifact kind. jar stays the default, and Maven 4 adds types that say where the JAR goes:
| Type | Meaning |
|---|---|
|
Class path or module path, decided heuristically (unchanged) |
|
Always the class path |
|
Always the module path |
|
Annotation processor path, class path or module path decided heuristically |
|
Always the annotation processor class path |
|
Always the annotation processor module path |
The article warns that, in October 2025, only the compiler plugin (4.0.0-beta-3 and later) honours them; other plugins will follow.
Maven 4 also adds a dedicated bom packaging, so a bill of materials is no longer a parent POM in disguise. It exists only in the build POM
(model 4.1.0); Maven generates a Maven 3 compatible consumer POM. This POM, built with mvn install on rc-7 (the .mvn directory is present,
and the install plugin version was not pinned):
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd">
<groupId>io.tidepool</groupId>
<artifactId>tidepool-bom</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>bom</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
installed this consumer POM, in which bom became pom:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-bom</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Two related details from the article: in model 4.1.0 an import-scope BOM can be selected with <classifier> and its dependencies can be
excluded with <exclusions>, and a BOM imported from the same reactor draws a warning, because the team recommends importing only BOMs that
are published before the build. For the Maven 3 mechanics see Dependency Management & BOMs.
Alternate POM syntaxes
The 4.0.0 consumer POM must stay XML, but the build POM may not have to. Maven 4 has a ModelParser service provider interface
(MNG-7836) that a core extension can implement to read the build POM in another syntax. The article names the Apache Maven Hocon Extension
as one of the first users. Core extensions are loaded from .mvn/extensions.xml, see Maven Extensions.
Nothing in this page was run against a custom parser.
Reactor, resume and the end of half-deployed builds
Resume. In Maven 3, a failing subproject B that depends on A produced the advice to resume from B, which then failed at once because A was
no longer built. In Maven 4, -r or --resume resumes from the subproject that failed last time, and the reactor remembers the subprojects
that succeeded. The reactor summary of a failing build points at it. A Broken.java was added to tidepool-cli:
$ mvn clean verify
...
[INFO] tidepool-parent .................................... SUCCESS [ 1.634 s]
[INFO] tidepool-model ..................................... SUCCESS [ 6.406 s]
[INFO] tidepool-engine .................................... SUCCESS [ 0.101 s]
[ERROR] tidepool-cli ....................................... FAILURE [ 0.045 s]
[INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE
[ERROR] /work/tidepool/tidepool-cli/src/main/java/io/tidepool/cli/Broken.java:[3,24] incompatible types: java.lang.String cannot be converted to int
...
[ERROR] After correcting the problems, you can resume the build with the command
[ERROR] mvn [args] -r
After deleting the file, mvn -r verify (the output was filtered to the relevant lines):
$ mvn -r verify
[INFO] Resuming from io.tidepool:tidepool-cli due to the --resume / -r feature.
[INFO] BUILD SUCCESS
Maven 3.10.0 does not know the option: mvn -r verify ends with Unable to parse command line options: Unrecognized option: -r. The
long-standing bug that made -am ignore the resumed subprojects (MNG-6863) is fixed, and the reactor is aware of builds started from a
subdirectory (MNG-6118). The article’s advice follows from this: use mvn verify for everyday builds, not mvn clean install.
All or nothing deploy. In Maven 3, a failure late in the reactor left the early subprojects installed or deployed. The article says Maven 4
changes the deployAtEnd default to true. That default is a property of the plugin, not of the core, and the experiment showed it: with the
3.x install and deploy plugins (3.2.0) pinned, the failing build above still installed tidepool-parent, tidepool-model and
tidepool-engine into a fresh local repository, and the mvn deploy to a file:// repository (with -DaltDeploymentRepository) wrote 42 files before
tidepool-cli failed. With the 4.x plugin line (4.0.0-beta-3 for install and deploy) in the parent:
[INFO] tidepool-parent .................................... SUCCESS [ 1.411 s]
[INFO] tidepool-model ..................................... SUCCESS [ 4.788 s]
[INFO] tidepool-engine .................................... SUCCESS [ 0.104 s]
[ERROR] tidepool-cli ....................................... FAILURE [ 0.036 s]
[ERROR] BUILD FAILURE
and neither the scratch local repository nor the file:// repository received a single file. On success the same plugins installed all
four subprojects after the last jar goal (Installing ... lines come after install:4.0.0-beta-3:install of the last subproject). So
"deploy only if everything built" needs both Maven 4 and the 4.x install and deploy plugin versions: pin them in pluginManagement.
Section Where install, deploy and release are covered says where this is treated in depth.
Further, all subprojects now get the same timestamp in their SNAPSHOT file names (MNG-6754).
Where install, deploy and release are covered
The mechanics of install, deploy and distributionManagement are on
Install, Deploy & Distribution Management and the release
process on Releasing. The release-candidate status of Maven 4 matters there: the
release plugin line for Maven 4 was not part of this page’s runs, so use Maven 3.10 or 3.9 for releases until you have tested your own flow.
Failing the build on log severity
Warnings that nobody reads become errors with -fos WARN (long form --fail-on-severity WARN). The argument may be WARN, WARNING or
ERROR; any other value is rejected. A project whose POM does not pin the compiler and resources plugins gets a warning on rc-7:
$ mvn compile -fos WARN
...
[WARNING] Version not locked for default bindings plugins [maven-resources-plugin, maven-compiler-plugin], you should define versions in pluginManagement section of your pom.xml or parent
[INFO] --- resources:3.5.0:resources (default-resources) @ warny ---
[INFO] --- compiler:3.16.0:compile (default-compile) @ warny ---
[INFO] Nothing to compile - all classes are up to date.
[INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[ERROR] Build failed due to log statements with a higher severity than allowed. Fix the logged issues or remove flag --fail-on-severity (-fos).
The exit code was 1; with -fos ERROR the same build succeeded with exit code 0. A bad value:
[ERROR] Error executing Maven.
[ERROR] Invalid fail on severity threshold 'fatal'. Supported values are 'WARN', 'WARNING' and 'ERROR'.
The warning shown is the other point of the super POM upgrade: its default plugin versions are new in Maven 4, so a build that relied on them
may change behaviour, and rc-7 now warns that you do. Pin every plugin version. Maven 3.10.0 has no such option: --fail-on-severity is
Unrecognized option, and the short -fos WARN is silently read as -f os WARN, the file os, with the error
Non-readable POM /work/classic/os. This option is the quickest way to keep a Maven 4 migration clean in CI (see
Migrating to Maven 4).
Profiles: optional ids and condition activation
A missing profile is now an error. In Maven 3 -Pnonexistent printed a warning and carried on, which hid typos. Maven 4 stops. Run on the
same 4.0.0 project with the three releases (Maven 3.9.16 and 3.10.0 print the same):
$ mvn compile -Pnonexistent # Maven 3.9.16 and 3.10.0
[WARNING] The requested profile "nonexistent" could not be activated because it does not exist.
[INFO] BUILD SUCCESS
$ mvn compile -Pnonexistent # Maven 4.0.0-rc-7
[ERROR] The requested profiles [nonexistent] could not be activated or deactivated because they do not exist. -> [Help 1]
...
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MissingProfilesException
(The Maven 3 warning is printed twice, at the start and at the end of the build; one is shown.) Prefix the id with ? to get the tolerant
behaviour back. The command needs quoting in shells where ? is a wildcard:
$ mvn compile '-P?nonexistent'
[INFO] The requested optional profiles [nonexistent] could not be activated or deactivated because they do not exist.
...
[INFO] BUILD SUCCESS
...
[INFO] The requested optional profiles [nonexistent] could not be activated or deactivated because they do not exist.
Condition-based activation. In model 4.1.0 <activation> accepts a <condition> element holding an expression. The functions present in the
rc-7 implementation are length, upper, lower, substring, indexOf, contains, matches, not, if, exists, missing and inrange, with the
logical operators of the expression language. An unknown function is a model error:
[ERROR] Error parsing profile activation condition: Unknown function: lessThan @ /work/cond/pom.xml
(Only the first of two identical lines is shown.) A profile that is active when a data directory exists and the running Java is 17 or
newer:
<profiles>
<profile>
<id>on-jdk-17-plus-with-data</id>
<activation>
<condition>exists("${project.basedir}/data") && inrange(${java.specification.version}, "[17,)")</condition>
</activation>
<properties>
<flavour>with-data</flavour>
</properties>
</profile>
</profiles>
With and without the directory (flavour has the default plain in <properties>):
$ mvn help:active-profiles help:evaluate -Dexpression=flavour -DforceStdout # with a data/ directory
The following profiles are active:
- on-jdk-17-plus-with-data (source: io.tidepool:cond:1.0.0-SNAPSHOT)
with-data
$ rmdir data
$ mvn help:evaluate -Dexpression=flavour -q -DforceStdout
plain
(Both runs filtered to the relevant lines.) Maven 3.10.0 rejects the same POM with
Malformed POM ... Unrecognised tag: 'condition'. The profile reference is on Maven Profiles. A profile that
uses <condition> cannot be written in 4.0.0, so the published consumer POM may be 4.1.0 (Maven warns when that happens).
The lifecycle becomes a tree
The Maven 3 lifecycle is one ordered list of phases. Maven 4 defines it as a tree, which allows finer dependencies (a project can start
once a dependency has reached the ready phase instead of finishing verify) and skipping steps (deploy without installing). For
compatibility the default execution does not change; the tree is used by the opt-in builder: mvn -b concurrent .... Every phase has a
before: and an after: hook, executions in the same phase can be ordered with [n], and two new phases, all and each, wrap a whole
project and each phase of it. The deprecated pre-* and post-* names remain as aliases.
A scratch plugin binding to try the hooks, in tidepool-model (the antrun plugin with <echo>):
<execution>
<id>hook-before-compile</id>
<phase>before:compile</phase>
<goals><goal>run</goal></goals>
<configuration><target><echo message="before:compile"/></target></configuration>
</execution>
<execution>
<id>hook-after-compile-200</id>
<phase>after:compile[200]</phase>
<goals><goal>run</goal></goals>
<configuration><target><echo message="after:compile[200]"/></target></configuration>
</execution>
<!-- plus after:compile[100], before:all and after:all, written the same way -->
mvn compile -pl tidepool-model on rc-7 (plugin goals only):
[INFO] --- antrun:3.1.0:run (hook-before-all) @ tidepool-model ---
[WARNING] [echo] before:all
[INFO] --- resources:3.5.0:resources (default-resources) @ tidepool-model ---
[INFO] --- antrun:3.1.0:run (hook-before-compile) @ tidepool-model ---
[WARNING] [echo] before:compile
[INFO] --- compiler:3.16.0:compile (default-compile) @ tidepool-model ---
[INFO] --- antrun:3.1.0:run (hook-after-compile-100) @ tidepool-model ---
[WARNING] [echo] after:compile[100]
[INFO] --- antrun:3.1.0:run (hook-after-compile-200) @ tidepool-model ---
[WARNING] [echo] after:compile[200]
[INFO] --- antrun:3.1.0:run (hook-after-all) @ tidepool-model ---
[WARNING] [echo] after:all
(The [WARNING] prefix is how the antrun plugin prints its <echo>.) Two behaviour changes follow from the new hooks. First, Maven 4 always
runs the after: hook of a phase it reaches, while Maven 3 ran a post-* phase only when the next phase ran or when you named it. The
experiment bound the echo to post-clean in a 4.0.0 POM: mvn clean -pl tidepool-model printed nothing from it on Maven 3.10.0 and printed
post-clean ran on rc-7. Second, binding to a pre-* or post-* phase now works as an alias, but the article warns against it, notably
for plugins bound to a post-* phase whose pre-* counterpart does not exist.
An observation specific to rc-7 and the concurrent builder: with -b concurrent, the run of the same compile printed before:all,
before:compile, the compiler and after:all, but neither resources:resources nor the two after:compile[n] executions. The build still
reported success. Treat -b concurrent as experimental in the release candidate and compare the executed goals with those of the default
builder before relying on it. An unknown builder id fails with BuilderNotFoundException. The phases, ordering and builder are explained on
Maven 4: Lifecycle, and the Maven 3 lifecycle on
Maven Build Lifecycle.
Plugins: what stops working
Maven 4 removes long-deprecated pieces, and a plugin that uses them stops working:
-
Plexus dependency injection. "Plexus Containers" injection, deprecated since Maven 3.2 (2010), is gone. A plugin that still depends on it needs JSR-330 (
javax.inject). rc-7 adds validation for it. The distribution still carries the Sisu Plexus bridge (org.eclipse.sisu.plexus-1.1.0.jar), the same as 3.10.0, andplexus-component-annotations-2.2.0.jar. -
Maven 2-era APIs. Plugins that never moved to the Maven 3 APIs are the ones at risk. Maven 4 is expected to run any plugin that follows Maven 3 best practices. Run
-Dmaven.plugin.validation=verboseon 3.9.x or 4 to list what a plugin does wrong. -
The
${pom.*}expressions. The article says these were removed, and Maven 3.9.16 does warn about them (The expression ${pom.version} is deprecated. Please use ${project.version} instead.). In an experiment with<label>${pom.artifactId}-${pom.version}</label>in a 4.0.0 POM, rc-7 resolved the value (pomexp-1.0.0-SNAPSHOT) with no warning at all, so do not count on rc-7 to find them for you; search your POMs for${pom., and use${project.. -
The new plugin API. A plugin API on the immutable plugin model exists, but is still experimental in 4.0.0: only the 4.x compiler plugin is a reason to use it today.
Writing plugins is on Writing Maven Plugins.
Encryption, resolver, shell and upgrade tool
mvnenc and password encryption. Maven 3’s master-password mechanism was "obfuscation". Maven 4 replaces it with Maven Encryption, a
command line tool mvnenc, which has these goals in rc-7:
Goals:
diag - display encryption configuration diagnostic
init - wizard to configure encryption (interactive only)
encrypt - encrypts input
decrypt - decrypts encrypted input
It adds decrypt and supports external vaults. The command is also reachable as mvn --enc. The settings that use encrypted values are
on settings.xml & Password Encryption; mvnenc init is interactive and was not run here.
Maven Resolver 2. The core ships Maven Artifact Resolver 2.0.23 (maven-resolver-impl-2.0.23.jar); Maven 3.10.0 ships 2.0.24, so Resolver
2 is not new compared with 3.10. What is new is that in Maven 4 the resolver is behind the Maven API and plugins do not call it directly.
The rc-7 log line Loaded 24114 auto-discovered prefixes for remote repository central comes from it. See
Maven Resolver.
mvnsh, the Maven Shell. mvnsh keeps one Maven process open for a session, which saves the JVM start and configuration load of every
command. It is the single-process counterpart to the Maven Daemon (mvnd), see
Build Performance: Cache, Parallel Builds & mvnd. Run without a terminal
(printf — '-v\nexit\n' | mvnsh), it printed its banner and 4.0.0-rc-7 and then ended with a NullPointerException from the JLine terminal
capability lookup, so the shell needs a real interactive terminal. It was not run interactively here.
mvnup, the Upgrade Tool. mvnup check reports what needs to change, and mvnup apply rewrites the POMs. --all (-a) means
--model-version 4.1.0 --infer --model --plugins. On a copy of the 4.0.0 tidepool project (committed in a scratch git repository), mvnup -B check --all
listed 13 strategies (excerpt):
[INFO] Maven Upgrade Tool - Check
[INFO] Found 4 POM file(s)
[INFO] Executing 13 upgrade strategy(ies):
[INFO] - Upgrading POM model version
[INFO] - Applying Maven inference optimizations
[INFO] - Removing duplicate XML elements
[INFO] - Applying Maven 4 compatibility fixes
[INFO] - Migrating source configuration to <source> elements
[INFO] - Deduplicating dependency and plugin declarations
[INFO] - Injecting standalone Nashorn for antrun JavaScript compatibility
[INFO] - Widening RequireMavenVersion ranges to allow Maven 4
[INFO] - Adding binary file extension exclusions to resource filtering
[INFO] - Upgrading HTTP repository URLs to HTTPS
[INFO] - Adding maven-toolchains-plugin for JDK source level compatibility
[INFO] - Fixing invalid Automatic-Module-Name manifest entries
[INFO] - Upgrading Maven plugins to recommended versions
...
[INFO] • Converted <modules> to <subprojects>
[INFO] • Upgraded phase: post-clean → after:clean
mvnup -B apply --all then changed all four POMs (git diff --stat: 17 insertions, 32 deletions): namespace and schema to 4.1.0,
<modules> to subprojects (or auto-discovery), parents shortened to the <parent> shorthand, post-clean to after:clean, and
maven.compiler.release to a <sources><source><targetVersion>17</targetVersion></source></sources> block. That last change shows the one
trap worth knowing: tidepool pins the 3.x compiler plugin 3.16.0, which does not read <sources>, so mvn verify on the upgraded
project failed:
[ERROR] /work/up/tidepool-model/src/main/java/io/tidepool/model/Tide.java:[3,8] records are not supported in -source 8
[ERROR] (use -source 16 or higher to enable records)
Review the diff and re-run the build after mvnup apply; with the 3.x compiler plugin keep maven.compiler.release, or move to the 4.x compiler
plugin. The release notes of rc-7 list several mvnup fixes, so the tool is itself still moving. The step-by-step migration is on
Migrating to Maven 4.
Equivalents in Ant and Gradle
Maven 4’s separation of build and published metadata is the model Gradle already has: its Gradle Module Metadata is published next to a POM and the build script is never published (see Comparing Ant, Maven & Gradle). Subprojects are the Gradle word as well. Apache Ant has neither a project model nor a lifecycle, so there is nothing to compare for the lifecycle tree. The migration page between tools covers moving between them.
What to do while 4.0.0 is a release candidate
-
Try your builds on rc-7 in CI next to the production Maven, and treat failures as feedback to the project (GitHub issues).
-
Keep the build POM at 4.0.0 until you need a 4.1.0 feature: a 4.0.0 POM builds on Maven 3.9, 3.10 and 4, and the consumer POM is the same 4.0.0 either way. Moving to 4.1.0 is a one-way step for contributors who still run Maven 3.
-
Pin every plugin version, add a
.mvndirectory orroot="true", and use-fos WARNonce to see what the build is warning about. -
Use the 4.x plugin lines only on Maven 4 builds.
-
Do not depend on the
-b concurrentbuilder or onmvnshfor production yet.
The complete list of resolved issues is the issue overview linked from the article.