Maven Extensions
|
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 written against Apache Maven 3.10.0 on JDK 26. Every example was run on 2026-10-16. Maven 3.9.16 and Maven 4.0.0-rc-7 (a release
candidate) were run wherever the page says so. All runs used MAVEN_ARGS="-B -ntp" (batch mode, no transfer progress) and a private local
repository, and from the console output the Scanning for projects line, the banner and separator lines, blank [INFO] lines, the
Total time and Finished at lines and the Loaded N auto-discovered prefixes line of Maven 3.10 were left out. The directory of the
experiments is shown as . in paths. The extension that the page builds, tidepool-build-notes, is a few lines of Java written for this
page; the other extensions are real ones from Maven Central.
A Maven plugin adds goals: it does something when a lifecycle phase or the command line asks for it. A Maven extension changes Maven itself. It is code that is loaded into the Maven process, can replace or add components of the Maven container, and can hook into the whole session — before the POMs are read, between projects, after the last mojo. Most of the time a build does not need one; the build, the plugins and the plugin configuration do the work. Extensions exist for the things plugins cannot do: teach Maven a new transport protocol, read the POM from another file format, schedule modules differently, cache build results, check the environment before anything starts, or record what the build did.
The extension mechanism is also the reason the section on Writing Plugins is a separate page: a plugin is loaded late, in an isolated class loader, and an extension is loaded early, and sometimes into the same class loader as Maven.
Core extensions and build extensions
The official guide distinguishes two kinds by when they are loaded:
| Core extension | Build extension | |
|---|---|---|
Registered in |
|
|
Loaded |
At startup, before the POMs are read |
While the project is being built, after its POM is read |
Class loader |
The Maven core class loader (a realm of its own for each extension in |
A realm of its own for each extension, isolated from the others |
Can |
Change how Maven reads POMs, schedules and builds, replace core components, hook the whole session |
Add packagings, lifecycle mappings, Wagon providers and components that the plugins of that project use |
Applies to |
The whole session (every project) |
The project that declares it (and, by inheritance, its children) |
The guide explains that some extensions change Maven so deeply that they must be loaded early, and that an extension’s documentation should say which kind it is. The sections below build one extension and load it each way, so that the difference is visible.
The class loaders behind the difference
Maven uses Plexus Classworlds to build a graph of class loaders (realms). The guide to Maven classloading describes them; the diagram shows the parts that matter for extensions.
-
The core realm holds the libraries of
${maven.home}/lib. Core extensions are added to it, so Maven itself and, through the API realm, every plugin can see them (only what they export; see Exported packages and artifacts). -
The API realm is a filtered view of the core: it exposes only the exported packages of Maven and of the core extensions. It exists since Maven 3.3.1, the release that also introduced
.mvn/extensions.xml. -
A build extension gets its own realm that imports the API realm. One build extension cannot see another. Every Sisu (JSR-330) or Plexus component in its JAR is registered in the global container when the realm is created, and component references in a plugin descriptor are wired from that container.
-
Each project has a project realm that imports the API realm and exposes the classes of the build extensions bound to that project. Each plugin that is not a build extension has a plugin realm that imports the project realm. A plugin marked
<extensions>true</extensions>uses the build extension realm instead of a plugin realm. -
The guide warns that a build extension is loaded late and therefore has limited effect: anything that Maven has already done by the time the POM is read cannot be changed by it.
Writing an extension: lifecycle participants and event spies
The Maven guide for lifecycle extensions lists the three classes to extend, depending on what the extension must see:
-
org.apache.maven.AbstractMavenLifecycleParticipant: callbacksafterSessionStart,afterProjectsReadandafterSessionEnd. This is the place to read or modify the projects after the POMs are read (afterProjectsRead), for example to change a plugin configuration in all projects. -
org.apache.maven.execution.AbstractExecutionListener: callbacks for the project and mojo events of the execution. -
org.apache.maven.eventspy.AbstractEventSpy: a spy that receives every event object that Maven dispatches (execution events and also, for example, the dependency resolution request and result of each project).
Everything is a Sisu component. The extension class is marked with the JSR-330 annotations @Named and @Singleton, and the JAR carries a
Sisu index, META-INF/sisu/javax.inject.Named, which lists the components so that the container can find them without scanning. The extension
of this page has two components:
package io.tidepool.ext;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.maven.AbstractMavenLifecycleParticipant;
import org.apache.maven.MavenExecutionException;
import org.apache.maven.execution.MavenSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Core/build extension: runs around the whole session, before any plugin. */
@Named("tidepool-build-notes")
@Singleton
public class BuildNotesParticipant extends AbstractMavenLifecycleParticipant {
private static final Logger LOG = LoggerFactory.getLogger(BuildNotesParticipant.class);
@Override
public void afterSessionStart(MavenSession session) throws MavenExecutionException {
LOG.info("[tidepool-build-notes] session started, goals {}", session.getGoals());
}
@Override
public void afterProjectsRead(MavenSession session) throws MavenExecutionException {
LOG.info("[tidepool-build-notes] {} project(s) read, class loader {}",
session.getProjects().size(), getClass().getClassLoader());
session.getProjects().forEach(p ->
LOG.info("[tidepool-build-notes] {}", p.getId()));
}
@Override
public void afterSessionEnd(MavenSession session) throws MavenExecutionException {
LOG.info("[tidepool-build-notes] session ended");
}
}
package io.tidepool.ext;
import java.util.concurrent.atomic.AtomicInteger;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.maven.eventspy.AbstractEventSpy;
import org.apache.maven.execution.ExecutionEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** An EventSpy sees every event of the build, including those before the POMs are read. */
@Named
@Singleton
public class MojoCounterSpy extends AbstractEventSpy {
private static final Logger LOG = LoggerFactory.getLogger(MojoCounterSpy.class);
private final AtomicInteger mojos = new AtomicInteger();
@Override
public void onEvent(Object event) {
if (event instanceof ExecutionEvent e) {
if (e.getType() == ExecutionEvent.Type.MojoStarted) {
mojos.incrementAndGet();
} else if (e.getType() == ExecutionEvent.Type.SessionEnded) {
LOG.info("[tidepool-build-notes] {} mojo execution(s) started", mojos.get());
}
}
}
}
The POM compiles against Maven’s own API with provided scope: Maven supplies maven-core, JSR-330 and SLF4J at runtime from its core
realm, and an extension must not bundle them. The release is 17 because the extension is meant to run on every Maven in this page, and the
sisu-maven-plugin generates the index:
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>3.9.16</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.eclipse.sisu</groupId>
<artifactId>sisu-maven-plugin</artifactId>
<version>1.1.0</version>
<executions>
<execution>
<id>main-index</id>
<phase>process-classes</phase>
<goals><goal>main-index</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
The version matters. The official guide still shows 0.3.5 of the plugin. Built with it on JDK 26 (release 17) the plugin ran without a message
and wrote no index, and the JAR had no META-INF/sisu entry at all; with 0.9.0.M4 and with 1.1.0 the same build produced the file. An extension
without an index loads, but Maven never finds its components, so it silently does nothing. Check the JAR after building it:
$ mvn install (Maven 3.10.0, extension project)
[INFO] --- sisu:1.1.0:main-index (main-index) @ tidepool-build-notes ---
[INFO] BUILD SUCCESS
$ unzip -l target/tidepool-build-notes-1.0.0.jar | grep -E 'class|sisu/javax'
69 10-16-2026 00:00 META-INF/sisu/javax.inject.Named
2817 10-16-2026 00:00 io/tidepool/ext/BuildNotesParticipant.class
1628 10-16-2026 00:00 io/tidepool/ext/MojoCounterSpy.class
$ unzip -p target/tidepool-build-notes-1.0.0.jar META-INF/sisu/javax.inject.Named
io.tidepool.ext.BuildNotesParticipant
io.tidepool.ext.MojoCounterSpy
The JAR was then installed into the private local repository as io.tidepool.ext:tidepool-build-notes:1.0.0. The tidepool build used for the
experiments is a parent POM with two modules, tidepool-model and tidepool-engine, each with one class. The extension, its POM and that build are this page’s own lab setup, not examples from the Maven documentation.
Core extensions
.mvn/extensions.xml
The recommended way since Maven 3.3.1 is the file .mvn/extensions.xml in the root of the project. It is part of the project and is committed
with it, so everybody who builds the project gets the same extension without changing their installation. Before it existed, an extension had to be
a (shaded) JAR copied by hand into the lib/ext directory of the Maven installation, or named on the command line. The file lists
the extension by Maven coordinates, and Maven resolves it with its transitive dependencies from a repository like any other artifact (see
Configuring Maven for the other files of the .mvn directory):
<?xml version="1.0" encoding="UTF-8"?>
<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.1.0">
<extension>
<groupId>io.tidepool.ext</groupId>
<artifactId>tidepool-build-notes</artifactId>
<version>1.0.0</version>
</extension>
</extensions>
The namespace is the one of the guide, http://maven.apache.org/EXTENSIONS/1.1.0 (with the schema core-extensions-1.1.0.xsd). Running any
goal now prints the lines of the extension before the first project is built:
$ mvn compile (Maven 3.10.0)
[INFO] [tidepool-build-notes] session started, goals [compile]
[INFO] [tidepool-build-notes] 3 project(s) read, class loader ClassRealm[coreExtension>io.tidepool.ext:tidepool-build-notes:1.0.0, parent: ClassRealm[plexus.core, parent: null]]
[INFO] [tidepool-build-notes] io.tidepool:tidepool:pom:1.0.0
[INFO] [tidepool-build-notes] io.tidepool:tidepool-model:jar:1.0.0
[INFO] [tidepool-build-notes] io.tidepool:tidepool-engine:jar:1.0.0
[INFO] --- resources:3.5.0:resources (default-resources) @ tidepool-model ---
[INFO] --- compiler:3.16.0:compile (default-compile) @ tidepool-model ---
[INFO] --- resources:3.5.0:resources (default-resources) @ tidepool-engine ---
[INFO] --- compiler:3.16.0:compile (default-compile) @ tidepool-engine ---
[INFO] [tidepool-build-notes] 4 mojo execution(s) started
[INFO] BUILD SUCCESS
[INFO] [tidepool-build-notes] session ended
The order shows what the extension can do: session started came before Maven had read any POM, the three projects were visible to
afterProjectsRead before the first plugin ran, the spy counted the four mojo executions, and session ended was printed after BUILD SUCCESS.
The class loader in the message is the extension’s own realm, coreExtension>io.tidepool.ext:tidepool-build-notes:1.0.0, whose parent is
plexus.core, the realm of the Maven core. The same project and the same extension produced the same lines on Maven 3.9.16 and on Maven
4.0.0-rc-7. The extension was written against the Maven 3 API with javax.inject, and it worked unchanged on 4.0.0-rc-7; an extension that is
written only for Maven 4 uses the Maven 4 API and jakarta.inject.
The command line, lib/ext and the extension classpath
Two older ways are still there. -Dmaven.ext.class.path= takes the path of a JAR (a path list), and puts it in an extension realm named
maven.ext; there is no dependency resolution, so the JAR must be complete:
$ mvn -Dmaven.ext.class.path=ext/target/tidepool-build-notes-1.0.0.jar compile (Maven 3.10.0, project without .mvn/extensions.xml)
[INFO] [tidepool-build-notes] session started, goals [compile]
[INFO] [tidepool-build-notes] 3 project(s) read, class loader ClassRealm[maven.ext, parent: ClassRealm[plexus.core, parent: null]]
...
[INFO] [tidepool-build-notes] 4 mojo execution(s) started
[INFO] BUILD SUCCESS
[INFO] [tidepool-build-notes] session ended
A JAR copied to ${maven.home}/lib/ext goes straight into the core realm (ClassRealm[plexus.core, parent: null] in the output, with no
realm of its own). The lab used a copy of the Maven 3.10.0 distribution for this, because the installation must not be modified: the directory
lib/ext of the distribution contains a README.txt, and the two directories hazelcast and redisson, and the copied JAR was found:
$ cp tidepool-build-notes-1.0.0.jar mvn-copy/apache-maven-3.10.0/lib/ext/
$ mvn compile (the copy of Maven 3.10.0)
[INFO] [tidepool-build-notes] session started, goals [compile]
[INFO] [tidepool-build-notes] 3 project(s) read, class loader ClassRealm[plexus.core, parent: null]
...
The two methods change Maven’s installation or the command line and are the reason .mvn/extensions.xml was added: the guide calls the
first one an inconvenient on-boarding step, and says the second forces everybody to repeat the option on every call.
The environment variable MAVEN_OPTS or the file .mvn/maven.config can hold the -D option, but the extension’s coordinates in the project file
are what the next developer, and the CI server, will find.
User-wide and installation-wide extensions (Maven 3.10)
The release notes of Maven 3.10.0 list support for user-wide extensions (in ~/.m2/extensions.xml) and installation-wide extensions (in
$MAVEN_HOME/conf/extensions.xml), in addition to the project-specific .mvn/extensions.xml. They use the same format. The lab pointed
Maven at a fake home directory with MAVEN_OPTS=-Duser.home=..., so that the real ~/.m2 was not touched, and put the file of the previous section
there. Maven 3.9.16 ignored it, Maven 3.10.0 loaded it (Maven 4.0.0-rc-7 loaded it too, and also loaded the installation-wide conf/extensions.xml of a copy of its distribution):
$ MAVEN_OPTS=-Duser.home=./home mvn compile (project without .mvn/extensions.xml)
3.10.0: [INFO] [tidepool-build-notes] session started, goals [compile]
[INFO] [tidepool-build-notes] 3 project(s) read, class loader ClassRealm[coreExtension>io.tidepool.ext:tidepool-build-notes:1.0.0, ...
3.9.16: [INFO] BUILD SUCCESS (no extension line at all)
With the same extension in more than one place Maven loads it once, from the place of highest precedence, and says so. -X lists the
configured extensions in precedence order and the ones that were selected. In the lab the project file named version 1.0.0, the user file 0.9.0
and the installation file 0.8.0:
[WARNING] Found 2 extension conflict(s):
[WARNING] * Conflicting extension io.tidepool.ext:tidepool-build-notes: 0.9.0 vs 1.0.0 in ./home2/.m2/extensions.xml
[WARNING] * Conflicting extension io.tidepool.ext:tidepool-build-notes: 0.8.0 vs 1.0.0 in ./mvn-copy/apache-maven-3.10.0/conf/extensions.xml
[WARNING]
[WARNING] Order of core extensions precedence is project > user > installation. Selected extensions are:
[WARNING] * io.tidepool.ext:tidepool-build-notes:1.0.0
The same extension, with the same version, in the project and in the user file gave no warning: the build ran with one copy, and -X printed the two source
files under Configured core extensions (in precedence order) and the single io.tidepool.ext:tidepool-build-notes:1.0.0 under
Selected core extensions (in loading order). The user-wide and installation-wide files are for what a team or a machine wants on every
build, for example an extension that records timing for all projects; the project file is for what one project needs, and, since it is committed,
is the only one that makes the build repeatable on other machines.
Source: Maven 3.10.0 release notes
Build extensions
Declared in the POM
A POM declares an extension in build/extensions. It is resolved while the project is read, and it is in effect for the project (and for
the children that inherit the declaration), but only from that point on. The same JAR declared this way, in the parent POM, behaves differently
from a core extension:
<build>
<extensions>
<extension>
<groupId>io.tidepool.ext</groupId>
<artifactId>tidepool-build-notes</artifactId>
<version>1.0.0</version>
</extension>
</extensions>
</build>
$ mvn compile (Maven 3.10.0; the output of Maven 3.9.16 and 4.0.0-rc-7 had the same lines)
[INFO] [tidepool-build-notes] 3 project(s) read, class loader ClassRealm[extension>io.tidepool.ext:tidepool-build-notes:1.0.0, parent: jdk.internal.loader.ClassLoaders$AppClassLoader@5bc2b487]
[INFO] [tidepool-build-notes] io.tidepool:tidepool:pom:1.0.0
[INFO] [tidepool-build-notes] io.tidepool:tidepool-model:jar:1.0.0
[INFO] [tidepool-build-notes] io.tidepool:tidepool-engine:jar:1.0.0
[INFO] BUILD SUCCESS
[INFO] [tidepool-build-notes] session ended
Three things are different from the core extension. The class loader is extension>io.tidepool.ext:tidepool-build-notes:1.0.0, a build extension
realm (Maven 4.0.0-rc-7 appended a suffix of digits to the realm id). The line session started is missing: the session had started long before the
POM was read. And the spy printed no mojo execution(s) started line either: the spy is registered when Maven starts, so only core extensions can be
spies. The participant’s afterProjectsRead and afterSessionEnd did run, because lifecycle participants of build extensions are
called once they are known. A build extension is therefore the right place for things that belong to one project (a packaging, a Wagon provider), and the
wrong one for anything that must act on Maven’s own start.
(On Maven 4.0.0-rc-7 this experiment also printed 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 — the lab project had no .mvn directory for that run. See
What’s New in Maven 4.)
Source: Maven: POM Reference, Extensions
Plugins with <extensions>true</extensions>
A plugin can also carry an extension. When the plugin is declared with <extensions>true</extensions>, Maven loads the plugin in a build
extension realm instead of a plugin realm, and registers the Plexus/Sisu components of its JAR — packaging definitions, lifecycle mappings and
artifact handlers, for example. This is how a plugin adds a packaging type: a POM with <packaging>bundle</packaging> is only understood
once the plugin that defines bundle is loaded as an extension. The official guide uses the Apache Felix bundle plugin as its example. In the lab
tidepool-model became an OSGi bundle, first without the flag:
<packaging>bundle</packaging>
...
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>6.2.0</version>
</plugin>
$ mvn package (Maven 3.10.0 and 3.9.16)
[ERROR] Some problems were encountered while processing the POMs:
[ERROR] Unknown packaging: bundle @ line 6, column 14
Maven knows the project’s packaging when it reads the POM, which is before it loads any plugin. Adding <extensions>true</extensions> to the
plugin makes Maven load the plugin first and register its packaging:
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>6.2.0</version>
<extensions>true</extensions>
</plugin>
$ mvn package (Maven 3.10.0; Maven 3.9.16 and 4.0.0-rc-7 ended with BUILD SUCCESS as well)
[INFO] -------------------------------[ bundle ]-------------------------------
[INFO] --- bundle:6.2.0:bundle (default-bundle) @ tidepool-model ---
[INFO] BUILD SUCCESS
$ unzip -p target/tidepool-model-1.0.0.jar META-INF/MANIFEST.MF | head -9
Manifest-Version: 1.0
Build-Jdk-Spec: 26
Bundle-ManifestVersion: 2
Bundle-SymbolicName: io.tidepool.model
Bundle-Version: 1.0.0
Created-By: Apache Maven Bundle Plugin 6.2.0
Export-Package: io.tidepool.model;version="1.0.0"
Import-Package: java.lang,java.lang.invoke,java.lang.runtime
Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=21))"
The packaging is defined by a LifecycleMapping component in the plugin JAR, in the file META-INF/plexus/components.xml. Its beginning
shows the binding the packaging bundle gives: the same plugins as jar up to test, and the bundle plugin’s bundle goal in package
(the build-lifecycle page explains the standard bindings, Build Lifecycle):
<component>
<role>org.apache.maven.lifecycle.mapping.LifecycleMapping</role>
<role-hint>bundle</role-hint>
<implementation>org.apache.maven.lifecycle.mapping.DefaultLifecycleMapping</implementation>
...
<package>org.apache.felix:maven-bundle-plugin:bundle</package>
<install>
org.apache.maven.plugins:maven-install-plugin:install,
org.apache.felix:maven-bundle-plugin:install
</install>
(The experiment pinned the versions of the standard plugins in pluginManagement. Without that, a lifecycle mapping that names a plugin
without a version makes Maven look for the latest release that its own version can run, and print one has unmet prerequisites block for each
newer plugin it has to reject; it works, but the output is long.) A plugin that needs the flag says so in its documentation, as the Felix bundle plugin does.
Wagon providers
The oldest use of build extensions is adding a Wagon provider: the library of transport protocols that Maven used before Resolver’s own
HTTP transport (the Repositories, Mirrors & Repository Managers
page has the transport table). Maven 3.10.0 ships the Resolver file and apache (HTTP) transports and the Wagon transport as a bridge, but the
Wagon bridge needs a provider for each other protocol. A project that deploys to an ftp: URL, declared in distributionManagement, fails
on all three releases because no transport knows the protocol:
$ mvn deploy (Maven 3.10.0, repository ftp://localhost:2121/releases)
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-deploy-plugin:3.2.0:deploy (default-deploy) on project tidepool-model: Failed to deploy artifacts/metadata: Cannot access ftp://localhost:2121/releases with type default using the available connector factories: BasicRepositoryConnectorFactory: Cannot access ftp://localhost:2121/releases using the registered transporter factories: ApacheTransporterFactory, FileTransporterFactory, WagonTransporterFactory -> [Help 1]
Adding the provider as a build extension, as the guide does, makes the protocol known:
<build>
<extensions>
<extension>
<groupId>org.apache.maven.wagon</groupId>
<artifactId>wagon-ftp</artifactId>
<version>3.5.3</version>
</extension>
</extensions>
</build>
$ mvn deploy (Maven 3.10.0, 3.9.16 and 4.0.0-rc-7 gave the same message, the artifact name differs only on 4.0.0-rc-7: the jar before the pom)
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-deploy-plugin:3.2.0:deploy (default-deploy) on project tidepool-model: Failed to deploy artifacts: Could not transfer artifact io.tidepool:tidepool-model:pom:1.0.0 from/to tidepool-ftp (ftp://localhost:2121/releases): Password not specified for repository tidepool-ftp -> [Help 1]
The build still fails — the lab has no FTP server and no credentials, and nothing was sent — but it fails for a different reason:
the ftp: protocol is now known and Maven asks for the credentials of the server tidepool-ftp (see
settings.xml & Password Encryption). Deploying
to a real FTP, SCP or WebDAV server is not run here. The same mechanism provides wagon-ssh, wagon-webdav-jackrabbit and others, and
also the Wagon providers that a site deployment needs (see
Site & Reporting).
Exported packages and artifacts
By default what a core extension contains is hidden from plugins: the API realm exposes only the packages that the extension exports.
An extension JAR can contain the descriptor META-INF/maven/extension.xml that names them. The Takari Polyglot YAML extension (used further below)
is a real example:
<extension>
<exportedPackages>
<exportedPackage>org.sonatype.maven.polyglot</exportedPackage>
<exportedPackage>org.sonatype.maven.polyglot.execute</exportedPackage>
</exportedPackages>
<exportedArtifacts>
<exportedArtifact>io.takari.polyglot:polyglot-common</exportedArtifact>
</exportedArtifacts>
</extension>
exportedPackages makes classes visible to the plugin realms; exportedArtifacts does the same for every package of an artifact (Maven
uses this itself, in its own extension.xml of Maven core, to export the Resolver packages). An extension that provides an API for plugins,
for example a Wagon provider that a plugin uses directly, needs the descriptor; an extension that only plugs components into the container, like the
lifecycle participant above, does not. The Maven Core Extensions Reference lists what Maven exports from its own core.
Finding an extension: resolution and repositories
A core extension is resolved before Maven has read the project’s POM, so the repositories that the POM declares are not available to resolve
it. What Maven can use at that point is Maven Central and the repositories of settings.xml, with the same mirrors and credentials as for any
other download (Repositories, Mirrors & Repository Managers).
The failure for a version that does not exist — a typo, or an extension that has not been installed in the local repository — is an early one,
and the message differs by release because Maven 3.10 and 4 consult the remote repository filter (the page on
Maven Resolver explains it):
$ mvn compile (.mvn/extensions.xml asks for io.tidepool.ext:tidepool-build-notes:9.9.9)
3.10.0:
[WARNING] The POM for io.tidepool.ext:tidepool-build-notes:jar:9.9.9 is missing, no dependency information available
[ERROR] Error executing Maven.
[ERROR] Extension io.tidepool.ext:tidepool-build-notes:9.9.9 or one of its dependencies could not be resolved: Plugin io.tidepool.ext:tidepool-build-notes:9.9.9 or one of its dependencies could not be resolved:
prefixes: Path io/tidepool/ext/tidepool-build-notes/9.9.9/tidepool-build-notes-9.9.9.jar NOT allowed from central
3.9.16:
[WARNING] The POM for io.tidepool.ext:tidepool-build-notes:jar:9.9.9 is missing, no dependency information available
[ERROR] Error executing Maven.
[ERROR] Extension io.tidepool.ext:tidepool-build-notes:9.9.9 or one of its dependencies could not be resolved: Plugin io.tidepool.ext:tidepool-build-notes:9.9.9 or one of its dependencies could not be resolved:
Could not find artifact io.tidepool.ext:tidepool-build-notes:jar:9.9.9 in central (https://repo.maven.apache.org/maven2)
Notice that the message says Error executing Maven, not BUILD FAILURE: no project had been built yet, and there is no reactor summary. In
both messages Plugin ... could not be resolved is the resolver’s wording for any artifact it resolves with the plugin machinery; the
extension is resolved with the same code as a plugin. The 3.9.16 message is the classic one; on 3.10.0 and 4.0.0-rc-7 the Central prefixes
file already says that the path io/tidepool/ext/... does not exist there, so Maven does not even ask. An extension published to your
own repository manager is resolved through the settings mirror in the same way, which is also why a build behind a proxy that blocks Central needs the
extension in the manager.
The extension catalogue
The Maven Extensions page lists the extensions that the Maven project maintains and the ones that other projects provide.
Maintained by the Maven project
Two extensions, at the time of the page’s last publication (2026-10-16):
-
Maven Build Cache Extension 1.3.0 (released 2026-08-19): incremental builds and a local and remote cache of build results. It is a core extension; its configuration, the cache key and the remote cache are on Build Performance: Cache, Parallel Builds & mvnd. Its JAR contains the Sisu index
META-INF/sisu/javax.inject.Namedand noextension.xml. -
Maven Enforcer Extension: defines rules globally, without POM inheritance. The catalogue table lists 3.5.0; Central has 3.6.3, the version of the Enforcer plugin, and 3.6.3 was used here.
The Enforcer extension runs the Enforcer plugin’s enforce goal at the start of the build, for every project, with rules read from the file
.mvn/enforcer-extension.xml (the name is the constant of the extension class; the Maven page on the extension only has a usage link). The core
extension is declared like any other:
<?xml version="1.0" encoding="UTF-8"?>
<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.1.0">
<extension>
<groupId>org.apache.maven.extensions</groupId>
<artifactId>maven-enforcer-extension</artifactId>
<version>3.6.3</version>
</extension>
</extensions>
<?xml version="1.0" encoding="UTF-8"?>
<extension>
<executions>
<execution>
<id>tidepool-baseline</id>
<configuration>
<rules>
<requireMavenVersion>
<version>[3.9.0,)</version>
</requireMavenVersion>
<requireJavaVersion>
<version>[21,)</version>
</requireJavaVersion>
</rules>
</configuration>
</execution>
</executions>
</extension>
$ mvn compile (Maven 3.10.0; Maven 3.9.16 and 4.0.0-rc-7 printed the same enforcer lines)
[INFO] --- enforcer:3.6.3:enforce (tidepool-baseline) @ tidepool ---
[INFO] Rule 0: org.apache.maven.enforcer.rules.version.RequireMavenVersion passed
[INFO] Rule 1: org.apache.maven.enforcer.rules.version.RequireJavaVersion passed
[INFO] --- enforcer:3.6.3:enforce (tidepool-baseline) @ tidepool-model ---
...
The enforce execution ran for each of the three projects; the rule results were printed for the first one only. Raising the Maven range to [3.11,) shows what a failing rule looks like:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-enforcer-plugin:3.6.3:enforce (tidepool-baseline) on project tidepool:
[ERROR] Rule 0: org.apache.maven.enforcer.rules.version.RequireMavenVersion failed with message:
[ERROR] Detected Maven Version: 3.10.0 is not in the allowed range [3.11,).
...
[INFO] tidepool-model ..................................... SKIPPED
[INFO] tidepool-engine .................................... SKIPPED
[ERROR] tidepool ........................................... FAILURE [ 0.208 s]
The rules and the plugin are described on Enforcer; the extension is
the way to apply them to a project whose POMs one cannot change, or to every project of an organisation through the user-wide or
installation-wide extensions.xml and the .mvn directory that a template provides.
Outside the Maven land
The catalogue’s second table lists extensions from other maintainers. The ones below are named there; apart from Polyglot, they were not run here: they need accounts, servers or services that the lab does not have, and their documentation is where their configuration lives.
| Extension | Maintainer | What it is (from the catalogue) and how it is loaded |
|---|---|---|
polyglot |
Takari |
A set of extensions that allow the POM model to be written in other dialects than XML. Run here with YAML, below. |
smart-builder |
Takari |
A replacement for the scheduling of the projects of a multi-module build (see Multi-Module Builds & the Reactor). Not run here. |
profiler, maven-timeline |
Takari, Jean-Christophe Gay |
Event spies that record the time taken by each mojo of the build, or produce a time line of the phases and goals. Not run here. The spy in this page is the same idea in a few lines. |
notifier |
Jean-Christophe Gay |
Sends a status notification at the end of the build. Not run here. |
opentelemetry-maven-extension |
The OpenTelemetry project |
Instruments the build and emits traces that observability tools (Jaeger and commercial products) can show. Not run here. |
jfr-maven-extension |
Philippe Marschall |
Generates Java Flight Recorder events for a Maven build. Not run here. |
Develocity (commercial) |
Gradle Inc. |
Captures build insights (build scans), and provides local and remote build caching, distributed test execution and predictive test selection for builds connected to a Develocity installation. Not run here: it needs a server (the free build scans service is described in its own documentation). |
Polyglot with YAML. The Takari Polyglot YAML extension io.takari.polyglot:polyglot-yaml:0.8.2 (the latest on Central) is declared in
.mvn/extensions.xml exactly like the extension above, and then a file pom.yaml replaces pom.xml:
modelVersion: 4.0.0
groupId: io.tidepool
artifactId: tidepool-model
version: 1.0.0
properties:
maven.compiler.release: '21'
project.build.sourceEncoding: UTF-8
project.build.outputTimestamp: '2026-10-16T00:00:00Z'
$ mvn compile (Maven 3.10.0; no pom.xml in the directory)
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 1 source file with javac [debug release 21] to target/classes
[INFO] BUILD SUCCESS
Maven 3.10.0 and 3.9.16 built the project from pom.yaml, and help:effective-pom reported Effective POM for project
'io.tidepool:tidepool-model:jar:1.0.0'. Maven 4.0.0-rc-7 did not: with the same extension file it answered The goal you specified requires a project
to execute but there is no POM in this directory, so this Takari release does not work there. What Maven 4 changed about the POM is on
Maven 4: POM Model & the Consumer POM.
The same idea in Ant and Gradle
Ant’s extension points are its antlibs and the class path of Ant itself: JARs in ANT_HOME/lib, in ~/.ant/lib or named with -lib are
loaded into Ant’s own class loader, and a task library that is on the build’s class path is used through an XML namespace; see
Antlibs & Namespaces and
Listeners, Loggers & Input Handlers for the
equivalent of an event spy. Gradle has no distinction between plugins and extensions in this sense: a plugin applied in settings.gradle.kts
or in an init script can hook the whole build (the Gradle and Settings objects take lifecycle listeners and build services), and a
plugin in buildSrc or an included build is loaded before the build script; see
Init Scripts & the Tooling API and
Binary Plugins. In Maven the line between the two is drawn by when the code
is loaded and where its class loader hangs in the graph.
Maven 3.9, 3.10 and 4: what differed
| Observation | 3.9.16 | 3.10.0 | 4.0.0-rc-7 |
|---|---|---|---|
|
loaded |
loaded |
loaded |
|
ignored (user-wide file) |
loaded; precedence project, user, installation |
loaded (both files) |
|
loaded, realm |
loaded, realm |
loaded, realm |
JAR in |
not tested |
loaded into |
not tested |
Build extension realm id |
|
|
|
Takari Polyglot YAML 0.8.2 |
built the project |
built the project |
|
Failure of a missing extension |
|
|
|
Classic problems
-
The extension seems to do nothing. The JAR has no Sisu index (
META-INF/sisu/javax.inject.Named), or the class is not annotated. Unzip the JAR and look; the oldsisu-maven-plugin0.3.5 from the official guide produced no index in the lab. -
NoClassDefFoundErrororClassCastExceptionbetween the extension and Maven. The extension bundled a copy of a class that Maven’s core realm already has (maven-core, SLF4J, Resolver). Declare those withprovidedscope and compile against the Maven release you target. -
The extension runs, but its first lines are missing. It was declared in the POM (a build extension), and the part of Maven that it wants to change had already run. Move it to
.mvn/extensions.xml. -
"Unknown packaging" or "Unsupported Protocol". A plugin that defines a packaging needs
<extensions>true</extensions>, and a transport needs the Wagon provider as a build extension (or anapacheorfilerepository URL). -
The extension cannot be found. Core extensions are resolved before the POM is read; use Central or a repository from
settings.xml, and check the local repository’s_remote.repositoriesrecords when the artifact was installed by hand (the repositories page explains them). -
A teammate’s build does not use the extension. It was in
~/.m2/extensions.xml, a file that is not part of the project. -
A plugin cannot see the extension’s classes. The extension did not export the package in
META-INF/maven/extension.xml.
Where the books are outdated
The Maven 2 book of the bibliography (Maven 2.2) predates .mvn/extensions.xml and the API class loader, which Maven 3.3.1 added; before
that the only choices were lib/ext and the command line, as the official guide recalls. Maven 3.10 adds user-wide and installation-wide files.
Maven 2 itself has been end of life since 2014.
Summary
-
An extension changes Maven itself and is loaded into the Maven process; a plugin adds goals and is loaded in an isolated realm.
-
Core extensions (
.mvn/extensions.xml, and in 3.10~/.m2/extensions.xmland$MAVEN_HOME/conf/extensions.xml,lib/ext,-Dmaven.ext.class.path) load before the POMs are read and can hook the whole session. Build extensions (build/extensions, or a plugin with<extensions>true</extensions>) load with the project and are isolated from each other. -
An extension is a Sisu component: JSR-330 annotations and a
META-INF/sisu/javax.inject.Namedindex (check the JAR). ExtendAbstractMavenLifecycleParticipant,AbstractExecutionListenerorAbstractEventSpy; event spies need to be core extensions. -
Precedence of core extensions in 3.10 is project, user, installation; the same extension is loaded once.
-
Core extensions are resolved without the POM’s repositories; Maven 3.10 and 4 may reject a path that Central’s prefixes file does not list.
-
The catalogue: Build Cache 1.3.0, the Enforcer extension 3.6.3, Polyglot (not for Maven 4.0.0-rc-7), Wagon providers, and third-party telemetry and cache extensions.