Maven Resolver
|
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 (Maven Resolver 2.0.24) on JDK 26. Every example was run on 2026-10-16. Maven 3.9.16 (Resolver 1.9.27)
and Maven 4.0.0-rc-7 (a release candidate, Resolver 2.0.23) 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] and [ERROR] lines, the Total time and Finished at lines, the Help 1 trailer of a failed build 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. Everything was resolved against the real Maven
Central, except the file-based repositories of the experiments that say so; nothing was published.
Every dependency, plugin, extension and parent POM that Maven uses comes through one component: Maven Resolver. Maven’s own code reads POMs and
decides what is needed; the Resolver decides which version wins, finds the files in the local repository or in the remote ones, checks them, and
installs and deploys the results. Most of the pages of this chapter describe something that the Resolver does: the
mediation of versions, the
repositories, mirrors and transports, the
snapshots and ranges. This page is about the component
itself: which release is in which Maven, how its options (the aether.* properties) are set, and the options that the other pages do not
cover — the groupId filter, trusted checksums, the split local repository, and the order in which a dependency graph is flattened.
What the Resolver is
Maven Resolver is a separate Apache project (an artifact resolution library, not a plugin and not part of the POM model). Maven calls it a
repository system and hands it a session: an object that holds the local repository, the mirrors, proxies and credentials from settings.xml, the
caches and, most importantly for this page, the configuration properties. Before 3.5.0 the library was the Eclipse Aether project; the project was
renamed when Maven took it over, but the Java packages are still org.eclipse.aether, and the key prefix of every option is still aether.. The
jars in the lib directory of each distribution show the modules (the listing is of the three distributions used for this page):
Maven 3.9.16 Maven 3.10.0 Maven 4.0.0-rc-7
maven-resolver-api-1.9.27.jar maven-resolver-api-2.0.24.jar maven-resolver-api-2.0.23.jar
maven-resolver-spi-1.9.27.jar maven-resolver-spi-2.0.24.jar maven-resolver-spi-2.0.23.jar
maven-resolver-util-1.9.27.jar maven-resolver-util-2.0.24.jar maven-resolver-util-2.0.23.jar
maven-resolver-impl-1.9.27.jar maven-resolver-impl-2.0.24.jar maven-resolver-impl-2.0.23.jar
maven-resolver-named-locks-1.9.27.jar maven-resolver-named-locks-2.0.24.jar maven-resolver-named-locks-2.0.23.jar
maven-resolver-connector-basic-1.9.27.jar maven-resolver-connector-basic-2.0.24.jar maven-resolver-connector-basic-2.0.23.jar
maven-resolver-transport-http-1.9.27.jar maven-resolver-transport-apache-2.0.24.jar maven-resolver-transport-apache-2.0.23.jar
maven-resolver-transport-file-1.9.27.jar maven-resolver-transport-file-2.0.24.jar maven-resolver-transport-file-2.0.23.jar
maven-resolver-transport-wagon-1.9.27.jar maven-resolver-transport-wagon-2.0.24.jar maven-resolver-transport-wagon-2.0.23.jar
maven-resolver-provider-3.9.16.jar maven-resolver-provider-3.10.0.jar maven-resolver-provider-4.0.0-rc-7.jar
The names say what the pieces are: an api and an spi (what Maven calls, what extensions can implement), util, the impl of the repository system,
the named-locks that serialise concurrent builds on the same local repository, a connector-basic that turns a request into transport calls according to
the repository layout, and the transports (HTTP, file:, and the bridge to Wagon). The last jar is not part of the Resolver project: maven-resolver-provider belongs to Maven and carries
the Maven-specific rules — how to read a POM for a dependency, relocation, version ranges, the default scope rules. Its version is the Maven version.
The Resolver releases follow the Maven ones: Maven 3.5.0 introduced the Apache Maven Resolver line, Maven 3.9 uses 1.9, and Maven 3.10 and Maven 4 use the 2.0 line. The release notes of 3.10.0 give the reason: the aim was to align Maven 3 and Maven 4 on the same Resolver version. Because of that, a Maven 3.10 build and a Maven 4 build behave alike in the Resolver, even where the rest of the two differs; and a number of option names changed between 1.x and 2.x, which the section on Upgrading from Resolver 1.x to 2.x collects.
Source: Maven 3.10.0 release notes and Maven Resolver
Configuring the Resolver from Maven
The Resolver is configured with configuration properties, named aether.<area>.<option>. The Maven page says it in one sentence: use system variables. The values are strings, converted to the type of the option:
| Option type | Conversion |
|---|---|
Boolean |
|
Integer, Long, Float |
|
Enumeration |
|
String |
as is |
Maven fills the session from several places and sets some defaults of its own, so a few options have a different default in Maven than in the Resolver
(the documentation sends the reader to the Maven class DefaultRepositorySystemSessionFactory for the details). Where can a user set an option? The
experiments used one option whose effect can be observed — aether.system.dependencyVisitor — and the order in which Maven loads a plugin’s
dependencies, which the section The dependency visitor and class path order explains. levelOrder is the default of Maven 3.10; preOrder is
the older behaviour. The question for each place is whether the order in the plugin realm became the old one (b d c e) or stayed b c d e:
| Where the property was set (Maven 3.10.0) | Plugin realm order | Taken into account? |
|---|---|---|
Nowhere |
|
(default) |
Command line, |
|
yes |
|
|
yes |
|
|
yes |
|
|
yes |
|
|
yes |
The same profile, activated with |
|
yes |
The same profile with |
|
yes |
The same profile, not activated |
|
no |
The same profile with a |
|
no |
A |
|
no |
Command line |
|
the command line wins |
The lessons: a user property counts wherever it comes from (command line, .mvn/maven.config, the environment variables MAVEN_OPTS and
MAVEN_ARGS); the command line wins over a settings profile; a settings profile counts when it is named in activeProfiles, with -P, or is
activeByDefault, but not when its activation is a jdk, os, property or file condition (the session is built before those are evaluated);
and a property in the POM is never seen, because the session exists before any POM is read. Maven 4.0.0-rc-7 gave the same results for the activeByDefault
and the conditions. The place to put a property that the whole team needs is .mvn/maven.config, which is committed with the project; the page on
Configuring Maven explains the file, and the
repositories page lists the Resolver options of the transport.
Repository id suffix. Options marked "Supports Repo ID Suffix" in the documentation can be set for one repository by appending a period and the
repository id: aether.transport.http.preemptiveAuth.tidepool-group=true, aether.remoteRepositoryFilter.prefixes.central=false. The next sections use it.
What the documentation page is. The page "Maven Resolver: Configuration Options" lists about 170 options of the latest Resolver release, including transports that
Maven does not ship (Jetty, MinIO, JDK HttpClient), with the type, the default, the release that introduced the option ("Since") and
whether it supports the suffix. An option that is not in the Resolver jars of your Maven is ignored without a message. Always check
the release column against the table of Resolver versions above.
Seeing the Resolver at work: -X
The debug output of Maven shows the Resolver’s choices. For a download from Central with an empty local repository:
$ mvn -X compile (Maven 3.10.0, empty local repository; the lines that mention the Resolver, the first of each kind)
[DEBUG] Created lock factory selector; available lock factories [file-lock, rwlock-local, semaphore-local, noop]
[DEBUG] Using manager EnhancedLocalRepositoryManager with priority 10.0 for ./sp/fresh
[DEBUG] Selecting Transporter for central (https://repo.maven.apache.org/maven2, default, releases)
[DEBUG] Using transporter ApacheTransporter with priority 5.0 for https://repo.maven.apache.org/maven2
[DEBUG] Using connector BasicRepositoryConnector with priority 0.0 for https://repo.maven.apache.org/maven2
[DEBUG] Loaded prefixes for remote repository central from auto-discovered file './sp/fresh/.meta/prefixes-central.txt'
Maven 3.9.16 printed the same kind of lines with HttpTransporter (the Resolver 1.9 transport class) and no lines about prefixes or lock factories. The
available lock factories are the Resolver 2 named-locks; the option aether.syncContext.named.factory (since 1.9.1) selects one of file-lock,
rwlock-local, semaphore-local and noop. file-lock is what lets two Maven processes that share a local repository wait for each other; noop
turns the locking off and is only for a repository nobody else writes to. In the -X header Maven also lists the user properties it received, among them
every aether.* option on the command line (for example aether.system.dependencyVisitor: levelOrder), which is the quickest way to see that a
property reached the build.
Remote repository filtering
The idea of the filter — ask a remote repository only for what it can have, so that builds are faster and private names do not leak to public repositories — and the prefixes filter with Central’s published .meta/prefixes.txt are described on
Repositories, Mirrors & Repository Managers, with the case of a
mirror. This section adds the rest: what the filter does to a request, what to do when it is wrong, and the groupId filter.
What a rejection looks like on each release
A private artifact (io.tidepool.order:b:1.0, which exists only in the local repository of the lab) is requested from an empty local repository with
dependency:get. The artifact is not on Central. Maven 3.9.16 asks Central and is told it does not have it; Maven 3.10.0 and 4.0.0-rc-7 do not even ask, because the
prefixes file of Central has no entry for /io/tidepool/order:
3.9.16:
[ERROR] ... The following artifacts could not be resolved: io.tidepool.order:b:jar:1.0 (absent): Could not find artifact io.tidepool.order:b:jar:1.0 in central (https://repo.maven.apache.org/maven2)
3.10.0:
[ERROR] ... The following artifacts could not be resolved: io.tidepool.order:b:jar:1.0 (absent): prefixes: Path io/tidepool/order/b/1.0/b-1.0.jar NOT allowed from central
4.0.0-rc-7:
[ERROR] ... The following artifacts could not be resolved: io.tidepool.order:b:jar:1.0 (absent): prefixes: Path io/tidepool/order/b/1.0/b-1.0.jar NOT allowed from central
[ERROR] This error indicates that a remote repository filter has rejected this artifact. This commonly happens with repository managers using virtual/group repositories that do not properly aggregate prefix files.
[ERROR] To disable remote repository filtering, add one or both of these to your command line or to .mvn/maven.config:
[ERROR] -Daether.remoteRepositoryFilter.prefixes=false
[ERROR] -Daether.remoteRepositoryFilter.groupId=false
[ERROR] See https://maven.apache.org/resolver/remote-repository-filtering.html
The difference is the point of the filter: the 3.9.16 request went to Central and told its operators the name of an internal library — the leak that turns into a dependency confusion attack when the name is registered there. The Resolver 2 message is a hint that the Maven 4 release candidate prints in full. With the suffix, the filter is switched off for Central alone and the request goes out again, as on 3.9.16:
$ mvn -Daether.remoteRepositoryFilter.prefixes.central=false dependency:get -Dartifact=io.tidepool.order:b:1.0 (Maven 3.10.0)
[ERROR] ... io.tidepool.order:b:jar:1.0 (absent): Could not find artifact io.tidepool.order:b:jar:1.0 in central (https://repo.maven.apache.org/maven2)
The documentation warns about the opposite mistake: Central is special, because plugins, plugin dependencies and extensions come from it, and limiting what can be fetched from Maven Central is usually a bad idea unless it is done very carefully, because the filter works on the pair of artifact and repository and does not know why the artifact is requested. The prefixes filter, which follows what Central itself publishes, is the safe form of filtering Central; a hand-written groupId list for Central is the risky form (see the experiment below).
When a repository has no list: noInputOutcome
A filter only acts on a repository for which it has input. For a repository without a prefixes file — a file: directory, a repository manager that does not
publish one — the filter abstains and the request goes out. aether.remoteRepositoryFilter.prefixes.noInputOutcome (since 2.0.14, default true) says what
to do instead: with false every request to such a repository is blocked. With a directory repository hosted (a file: repository holding the artifact), the default let the build
succeed, and the suffix form made it fail with a new message:
$ mvn dependency:get -Dartifact=io.tidepool.order:b:1.0 -DremoteRepositories=hosted::default::file://./cf/hosted (Maven 3.10.0)
[INFO] BUILD SUCCESS
$ mvn -Daether.remoteRepositoryFilter.prefixes.noInputOutcome.hosted=false dependency:get ...
[ERROR] ... io.tidepool.order:b:jar:1.0 (absent): prefixes: No input available
This is the setting for an organisation that wants a closed world: every repository must have a list, or no request goes out. The related
options of Resolver 2.0.14 and later are aether.remoteRepositoryFilter.prefixes.resolvePrefixFiles (default true; with false only user-provided files are
used, which is the documented answer for repository managers that serve a wrong prefixes file), .useMirroredRepositories (default false) and
.useRepositoryManagers. Since Resolver 2.0.21 the filter protects itself against a wrong auto-discovered file: the first path it denies for a repository
is checked against the repository itself, and if the repository does serve it, a warning is printed and (since 2.0.22) only that path is allowed while the file stays in force
for the others (the options .verifyDenied and .verifyDeniedDropsTree). A user-provided file is authoritative and is never verified or dropped.
The groupId filter
The second built-in filter reads a list of group ids per repository, from the file groupId-<repository id>.txt in the filter base directory (default
.remoteRepositoryFilters of the local repository; aether.remoteRepositoryFilter.groupId.basedir changes it, and can be a path in the project such as
${session.rootDirectory}/.mvn/rrf/, a file you commit). Unlike the prefixes filter, repositories do not publish these lists, so the filter has no effect until you provide
one. The file format, from the documentation:
-
blank lines and lines that start with
#are ignored; -
a group id allows that group id and everything below it:
org.apache.mavenallowsorg.apache.maven.plugins; -
the modifier
=limits the entry to exactly that group id;!denies it;!=denies exactly that group id; -
the entry
*makes the default accept (without it the default is reject), so that a list can be a list of exceptions; the last conflicting rule wins.
Recording. Setting aether.remoteRepositoryFilter.groupId.record=true makes the filter stop filtering and instead write down every group id it sees for each
repository. The first build of a small project with the compiler and resources plugins and commons-lang3, on an empty local repository, with the file directory
rrf:
$ mvn -Daether.remoteRepositoryFilter.groupId.record=true -Daether.remoteRepositoryFilter.groupId.basedir=./gid/rrf compile (Maven 3.10.0)
[INFO] BUILD SUCCESS
$ ls gid/rrf
groupId-central-8fac3ebd6edbaca3c794783fa38088af0aa128e7.txt
$ cat gid/rrf/groupId-central-8fac3ebd6edbaca3c794783fa38088af0aa128e7.txt
# Recorded entries
=com.github.javaparser
=commons-io
=javax.inject
=org.apache
=org.apache.commons
=org.apache.maven
=org.apache.maven.plugins
=org.apache.maven.shared
=org.codehaus.plexus
=org.eclipse.sisu
=org.junit
=org.ow2
=org.ow2.asm
=org.slf4j
=org.sonatype.forge
=org.sonatype.plexus
=org.sonatype.spice
Two details deserve attention. The recorded file name contains a hash of the repository URL after the id (the Resolver 2 repository key), and it uses
the exact-match modifier =, which makes it a precise list of what was needed; you edit it into the list you want to keep. And this list has seventeen group ids
for one commons-lang3 dependency: most of them belong to the plugins and their dependencies, and are the reason why a group id filter on Central is risky. A
file named groupId-central.txt, without the hash, was also honoured by Maven 3.10.0.
Using a list. A list that is too short breaks the build, and the message names the group id that was rejected, here after a file with the single line org.apache.maven:
$ mvn -Daether.remoteRepositoryFilter.groupId.basedir=./gid/rrf compile (Maven 3.10.0, groupId-central.txt = org.apache.maven)
[ERROR] Plugin org.apache.maven.plugins:maven-resources-plugin:3.5.0 or one of its dependencies could not be resolved:
[ERROR] The following artifacts could not be resolved: org.apache:apache:pom:37 (absent): groupId: G:org.apache NOT allowed from central
Notice that org.apache (the Apache parent POM of the plugin) is not below org.apache.maven. With the entry * the default becomes accept, and the file can
list exceptions; this denies one group:
$ cat gid/rrf/groupId-central.txt
# everything except commons
*
!org.apache.commons
$ mvn -Daether.remoteRepositoryFilter.groupId.basedir=./gid/rrf compile (Maven 3.10.0 and 4.0.0-rc-7)
[ERROR] Failed to execute goal on project gid-demo: Could not resolve dependencies for project io.tidepool:gid-demo:jar:1.0.0
[ERROR] dependency: org.apache.commons:commons-lang3:jar:3.20.0 (compile)
[ERROR] groupId: G:org.apache.commons NOT allowed from central
Maven 3.9.16 ignored this file: the filter is off by default in Resolver 1.9, and with the filter enabled explicitly
(-Daether.remoteRepositoryFilter.groupId=true) it rejected org.apache.maven.plugins instead, so it reads the same file differently from Resolver 2. So use these files with Maven 3.10 and 4, and keep the lists next to .mvn/maven.config. A list of group ids that are allowed (org.apache, org.codehaus.plexus,
org.eclipse.sisu, org.slf4j, org.sonatype, org.ow2, org.junit, commons-io, javax.inject and com.github.javaparser, enough for this project) built it without
errors.
The documentation recommends the arrangement in which the two filters complement each other: the prefixes of Central are discovered automatically, and groupId files are provided only for the other repositories (your own repository manager, a partner repository), where they say which of your groups come from where. The documentation’s table of that arrangement: for Central the prefixes filter is active and the groupId filter inactive; for another remote the prefixes filter is active or inactive (depending on whether it publishes a list) and the groupId filter is active.
Trusted checksums
A repository publishes a checksum next to each artifact, and Maven compares it with the file it downloaded. That detects a damaged download; it does not
detect an attacker who changes the file and its checksum, or a repository that has been replaced. Trusted checksums move the source of truth out of
the repository: Resolver checks every resolved artifact against a checksum that you keep, in your own files. The feature has two parts: a post-processor
(aether.artifactResolver.postProcessor.trustedChecksums, since 1.9.0, default false) that does the verification or the recording, and a source of the trusted values,
the layout of which is also chosen with options. The sparse directory source (aether.trustedChecksumsSource.sparseDirectory) stores one file per artifact file in a tree that
looks like a repository; the summary file source (aether.trustedChecksumsSource.summaryFile) is the alternative that keeps them in a summary file (not run here). For both, .basedir sets the directory
(default .checksums of the local repository) and .originAware (default true) separates the checksums by repository id.
The experiment recorded the checksums of the project of the previous section, then verified, then broke one value. The options (the same for each step, shown once) are
the first line, and record switches the recording on:
TC="-Daether.artifactResolver.postProcessor.trustedChecksums=true
-Daether.trustedChecksumsSource.sparseDirectory=true
-Daether.trustedChecksumsSource.sparseDirectory.basedir=./tc/checksums
-Daether.trustedChecksumsSource.sparseDirectory.originAware=false"
$ mvn $TC -Daether.artifactResolver.postProcessor.trustedChecksums.record=true compile (Maven 3.10.0, empty local repository)
[INFO] BUILD SUCCESS
$ find tc/checksums -type f | wc -l
87
$ find tc/checksums -name '*commons-lang3-3.20.0.jar.sha1'
tc/checksums/org/apache/commons/commons-lang3/3.20.0/commons-lang3-3.20.0.jar.sha1
$ cat tc/checksums/org/apache/commons/commons-lang3/3.20.0/commons-lang3-3.20.0.jar.sha1
65897b3e5731220962e659e001904af3c3cbeba9
$ curl -s https://repo.maven.apache.org/maven2/org/apache/commons/commons-lang3/3.20.0/commons-lang3-3.20.0.jar.sha1
65897b3e5731220962e659e001904af3c3cbeba9
Recording wrote 87 checksum files (POMs and JARs, the plugins' included), and the one for commons-lang3 is the value that Central publishes. A later
build with trustedChecksums=true and no record accepted a fresh local repository on all three Maven releases. Then the value was replaced by forty zeros:
$ mvn $TC compile (Maven 3.10.0, empty local repository)
[WARNING] Could not validate integrity of download from https://repo.maven.apache.org/maven2/org/apache/commons/commons-lang3/3.20.0/commons-lang3-3.20.0.jar
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal on project gid-demo: Could not resolve dependencies for project io.tidepool:gid-demo:jar:1.0.0
[ERROR] dependency: org.apache.commons:commons-lang3:jar:3.20.0 (compile)
[ERROR] Checksum validation failed, expected '0000000000000000000000000000000000000000' (PROVIDED) but is actually '65897b3e5731220962e659e001904af3c3cbeba9' (Trusted Checksums Source: sparseDirectory)
Maven 3.9.16 and 4.0.0-rc-7 failed the same way. The message says where the expected value came from (PROVIDED, by the sparseDirectory source). An artifact without a recorded
checksum is accepted by default; with aether.artifactResolver.postProcessor.trustedChecksums.failIfMissing=true (since 1.9.0) it is rejected, which turns the files into an
allow-list of every file the build may use:
$ mvn $TC -Daether.artifactResolver.postProcessor.trustedChecksums.failIfMissing=true compile (Maven 3.10.0, the file for commons-lang3 removed)
[ERROR] Missing from sparseDirectory trusted checksum(s) [SHA-1] for artifact org.apache.commons:commons-lang3:jar:3.20.0
Other options: .checksumAlgorithms (default SHA-1; list several, comma separated, to record and check them all), .snapshots (include snapshots, default false) and, since
Resolver 2.0.11, .scope (all by default, or project to check the project’s dependencies and leave the plugins out). Keep the recorded tree in version control, review its changes
like a lock file, and put the options in .mvn/maven.config. The file-level checks of the repository and the signatures of the artifacts are part of the page on
Security & Reproducible Builds; Gradle’s equivalent is dependency verification
(Dependency Locking, Verification & Caching).
The split local repository
The local repository is by default one tree for the artifacts you installed and the ones downloaded from any repository (the repositories page describes its
_remote.repositories records and the effects). Resolver 1.8.1 and later can split the tree. The flags are split (the main switch, default false), splitLocal,
splitRemote, splitRemoteRepository and splitRemoteRepositoryLast. The experiment ran mvn install of a small project that depends on
commons-lang3 (so one installed artifact and one downloaded artifact) on an empty repository, with each combination, and looked at where the two jars went:
-Daether.lrm.enhanced.split=true
installed/io/tidepool/gid-demo/1.0.0/gid-demo-1.0.0.jar
cached/org/apache/commons/commons-lang3/3.20.0/commons-lang3-3.20.0.jar
... + splitLocal=true
installed/releases/io/tidepool/gid-demo/1.0.0/gid-demo-1.0.0.jar
cached/org/apache/commons/commons-lang3/3.20.0/commons-lang3-3.20.0.jar
... + splitRemote=true
installed/io/tidepool/gid-demo/1.0.0/gid-demo-1.0.0.jar
cached/releases/org/apache/commons/commons-lang3/3.20.0/commons-lang3-3.20.0.jar
... + splitRemoteRepository=true
installed/io/tidepool/gid-demo/1.0.0/gid-demo-1.0.0.jar
cached/central-8fac3ebd6edbaca3c794783fa38088af0aa128e7/org/apache/commons/commons-lang3/3.20.0/commons-lang3-3.20.0.jar
... + splitRemote=true, splitRemoteRepository=true, splitRemoteRepositoryLast=true
installed/io/tidepool/gid-demo/1.0.0/gid-demo-1.0.0.jar
cached/releases/central-8fac3ebd6edbaca3c794783fa38088af0aa128e7/org/apache/commons/commons-lang3/3.20.0/commons-lang3-3.20.0.jar
The first level is installed and cached. splitLocal and splitRemote add a releases or snapshots level below it (the names come from the prefix options such as
aether.lrm.enhanced.snapshotsPrefix, default snapshots); splitRemoteRepository adds a directory per repository (the repository id and, in Resolver 2, a hash of
the URL), and splitRemoteRepositoryLast swaps the order of version and repository. The point is that what a build installed is kept apart from what it downloaded, and, with splitRemoteRepository, the downloads of one repository can be kept or dropped without
touching the others — a CI server can cache the cached part between runs without restoring installed. The flag names changed between the Resolver lines, and the experiment checked both:
| Flag | Resolver 1.9 (Maven 3.9.16) | Resolver 2 (Maven 3.10.0, 4.0.0-rc-7) |
|---|---|---|
|
no effect (the tree stayed plain) |
splits |
|
splits; remote repository directory is the plain id, |
splits, the old name is still understood |
(The Resolver 2 jar still contains the keys aether.enhancedLocalRepository.localPrefix, .releasesPrefix, .remotePrefix, .snapshotsPrefix, .split, .splitLocal, .splitRemote,
.splitRemoteRepository and .splitRemoteRepositoryLast next to the aether.lrm.enhanced.* ones.) Write the aether.enhancedLocalRepository.* form in a file that must work on
Maven 3.9 and 3.10 both, and the aether.lrm.enhanced.* form for Maven 3.10 and later.
The dependency visitor and class path order
The Resolver builds the dependency graph (a tree with the nearest-wins decisions applied; see
Dependencies, Scopes & Mediation) and then needs a list: the files to put in a
class path. The option aether.system.dependencyVisitor (since 2.0.0) chooses how the graph is flattened: preOrder (depth first, a library followed by its own dependencies),
levelOrder (breadth first, all direct dependencies, then their dependencies) or postOrder (dependencies before the library that needs them). The documentation says that
Maven 4 uses levelOrder by default, where Maven 3 used preOrder, and the release notes of 3.10.0 list "Classpath ordering change" under the potentially breaking changes:
-Daether.system.dependencyVisitor=preOrder restores the earlier order.
The order only matters when two libraries contain the same class or resource, and the first on the class path wins. The experiment has the smallest graph where the three orders differ:
app depends on b and c; b depends on d; c depends on e. All five were installed in a local repository. The order was read from the list of
libraries added to the class loader of a plugin that declares b and c as its own dependencies (the -X lines Included: ... of the plugin realm):
default preOrder levelOrder postOrder
Maven 3.9.16 b d c e b d c e b d c e (not tested)
Maven 3.10.0 b c d e b d c e b c d e d b e c
Maven 4.0.0-rc-7 b c d e b d c e b c d e d b e c
So the change is real: the default of Maven 3.10 and 4.0.0-rc-7 is levelOrder, preOrder gives the old order, and Maven 3.9.16 does not read the property at all. But the same
experiment gave an unexpected result for the project’s own class path. The compile class path of app (read from the compilePath the compiler plugin receives, and again from
dependency:build-classpath) was b d c e on all three releases and for all four values of the property:
$ mvn -X -Daether.system.dependencyVisitor=levelOrder compile (app; b, c, d and e from the local repository)
(f) compilePath = [.../app/target/classes, .../order/b/1.0/b-1.0.jar, .../order/d/1.0/d-1.0.jar, .../order/c/1.0/c-1.0.jar, .../order/e/1.0/e-1.0.jar]
In Maven 3.10.0 the source of LifecycleDependencyResolver builds the project’s artifacts by walking the dependency graph depth first (through RepositoryUtils.toArtifacts in maven-core), while the
plugin realms in the experiment followed the list that the Resolver flattens with the visitor. This matters for a migration: the visitor option changes the order of the libraries that plugins see (for example the
dependencies you add to a plugin’s <dependencies>), not, in this test, the order of the compile and test class paths of a project; the release notes state the change in more general terms.
On Maven 4.0.0-rc-7 the project’s class path was also b d c e for every value. If the class path order of a project is critical, do not rely on either: make the intent explicit with exclusions
and dependencyManagement (see Dependency Management & BOMs) and check with dependency:tree.
Upgrading from Resolver 1.x to 2.x
For a user of Maven the move from Maven 3.9 to 3.10 is the move from Resolver 1.9 to 2.0. The Resolver project’s own upgrade page is addressed to the developers of programs that embed the Resolver; for them it says the upgrade should be "smooth sailing" as long as the code does not use deprecated classes of the 1.x line. Its changes:
-
Session handling. Resolver 2 sessions are resources: they are created from a
SessionBuilderobtained fromRepositorySystem.createSessionBuilder, and eachCloseableSessionmust be closed, because some features (HTTP/2 transports) need anonSessionEndhook. The old way, the default constructor ofDefaultRepositorySystemSession, still works but can leak resources. Code that only uses a session, such as a Mojo, does not change. -
Delegated to the consumer. Most artifact properties, the
ArtifactTypeimplementation, class path generation and class path filtering are now Maven’s business (the classes are still in the Resolver jars, deprecated, with their replacements inmaven-resolver-provider). Dependency scopes were removed from the Resolver: they were always "just labels", and the Resolver is now unaware of them. -
Resolver 2 still supports the legacy (Maven 3 like) behaviour if the embedding code only adapts the session handling.
For a user the visible changes found in the experiments are:
| Observation | 3.9.16 (Resolver 1.9) | 3.10.0 (Resolver 2.0) | 4.0.0-rc-7 (Resolver 2.0) |
|---|---|---|---|
Native HTTP transport class (from |
|
|
|
Option names of the transport |
|
|
|
Option names of the split local repository |
|
|
|
Remote repository filter, prefixes |
no list of Central was discovered; the request went to Central |
on, with auto-discovered lists of Central |
on, with auto-discovered lists of Central, hint in the error |
Remote repository filter, groupId |
off unless |
on, dormant until a file exists |
on, dormant until a file exists |
|
ignored |
|
|
Named lock factories ( |
not printed |
|
|
Repository key in |
id only |
id plus hash of the URL |
id plus hash of the URL |
The first two rows are described with their consequences on the repositories page (the User-Agent option aether.connector.userAgent of Resolver 1.9 is aether.transport.http.userAgent in 2.0, and the old
name is ignored by Maven 3.10). A build that sets aether.* options in .mvn/maven.config for Maven 3.9 therefore needs a review when Maven is upgraded: some options silently stop working, as
the table shows, because an option the Resolver does not know is not an error.
The same idea in Ant and Gradle
The Resolver is not tied to Maven. The Maven Resolver Ant Tasks use the same library to resolve and deploy from an Ant build (see Maven Resolver Ant Tasks). Apache Ivy has its own resolver and cache model (Ivy: Settings & Resolvers). Gradle has its own dependency resolution engine, with its own cache, metadata formats and verification (Dependency Resolution, Conflicts & Capabilities); the Maven concepts of this page map to repository content filtering (Repositories & Content Filtering) for the remote repository filters, and to dependency verification for the trusted checksums. The shared overview is in Dependency Management Fundamentals.
Classic problems
-
prefixes: Path ... NOT allowed from ...: the filter has a list for that repository and the path is not in it. Fix the list, set the repository suffix form tofalsefor that repository, or switch the filter off with-Daether.remoteRepositoryFilter.prefixes=falsewhile you investigate. On Maven 4.0.0-rc-7 the message tells you. -
groupId: G:... NOT allowed from ...: agroupId-<id>.txtfile excludes a group that a plugin or a parent POM needs. Record a fresh list, or start the file with*. -
prefixes: No input available:noInputOutcomeisfalseand the repository has no prefixes file. -
An
aether.*option has no effect. It was set in the POM, in a settings profile with ajdk,os,propertyorfileactivation, with the wrong name for this Maven (aether.connector.*againstaether.transport.*), or the Resolver release of this Maven does not have it. Look for it in-Xunder the user properties, and compare with the "Since" column of the documentation. -
Checksum validation failed, expected '...' (PROVIDED) but is actually '...': the trusted checksum differs from the file. Do not edit the file; find out why the artifact changed (a re-release, a mirror that serves something else, an attack). -
The class path order changed after upgrading to Maven 3.10. The plugin and extension realms are flattened in level order now. Use
-Daether.system.dependencyVisitor=preOrderin.mvn/maven.configas a stop-gap, then fix the real conflict. -
The build is slow with several processes on one local repository. See the option
aether.syncContext.named.factory; Maven 3.9.12 added lock timeouts. -
Everything in the split repository is downloaded again. The first run after switching
aether.lrm.enhanced.spliton starts with an empty tree.
Where the books are outdated
The books of the bibliography predate the Resolver as an Apache project (Maven 3.5.0): they have no aether.* options, no remote repository filtering, no trusted checksums, no split local repository,
no named locks and no choice of the transport. Today’s documentation attributes all of that to the Resolver, which ships as a separate set of jars with each Maven.
Summary
-
Maven Resolver is the library that resolves, downloads, verifies, installs and deploys artifacts for Maven (and for the Ant tasks). Maven 3.9.16 has 1.9.27, Maven 3.10.0 has 2.0.24 and Maven 4.0.0-rc-7 has 2.0.23.
-
Options are
aether.*user properties: set them on the command line, in.mvn/maven.config, inMAVEN_ARGSorMAVEN_OPTS, or in an activesettings.xmlprofile; never in the POM. A repository id suffix targets one repository. -
The prefixes filter is on and discovers Central’s list in Maven 3.10 and 4; the groupId filter needs a file (
groupId-<id>.txt, with=,!and*), can record, and is not understood the same way by Maven 3.9. -
Trusted checksums (
sparseDirectoryorsummaryFilesource,record,failIfMissing) verify downloads against values that you keep. -
The local repository can be split into
installedandcached(and further by release, snapshot and repository); the key names differ between Resolver 1.9 and 2. -
aether.system.dependencyVisitorislevelOrderby default in 3.10 and 4; in the lab it changed the order of plugin realms, not the compile class path of the project. -
Unknown
aether.*options are ignored silently: check the Resolver release before relying on one.