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.

flowchart LR cfg["Maven: settings, POMs, -D properties"] --> sess["Resolver session with aether.* configuration"] sess --> collect["Collect: read the POMs and build the graph"] collect --> transform["Transform: pick one version per library"] transform --> resolve["Resolve: find the files"] resolve --> lrm["Local repository manager"] resolve --> filter["Remote repository filters"] filter --> conn["Connector and transport"] conn --> remote["Remote repositories"] resolve --> flat["Visitor flattens the graph into a list"] flat --> cp["Class paths and plugin realms"]

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.

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

Boolean.parseBoolean: only true (in any case) is true; a typo is false

Integer, Long, Float

Integer.parseInt, Long.parseLong, Float.parseFloat

Enumeration

Enum.valueOf: the exact name, with the exact case

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

b c d e

(default)

Command line, -Daether.system.dependencyVisitor=preOrder

b d c e

yes

.mvn/maven.config, one line -Daether.system.dependencyVisitor=preOrder

b d c e

yes

MAVEN_OPTS=-Daether.system.dependencyVisitor=preOrder

b d c e

yes

MAVEN_ARGS=-Daether.system.dependencyVisitor=preOrder

b d c e

yes

settings.xml profile, property aether.system.dependencyVisitor, listed in activeProfiles

b d c e

yes

The same profile, activated with -Presolver-config

b d c e

yes

The same profile with <activeByDefault>true</activeByDefault>

b d c e

yes

The same profile, not activated

b c d e

no

The same profile with a jdk, os, property or file activation that is true on the machine

b c d e

no

A <properties> element of the POM

b c d e

no

Command line levelOrder and a settings profile with preOrder

b c d e

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.maven allows org.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)

aether.lrm.enhanced.split

no effect (the tree stayed plain)

splits

aether.enhancedLocalRepository.split

splits; remote repository directory is the plain id, cached/central/...

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 SessionBuilder obtained from RepositorySystem.createSessionBuilder, and each CloseableSession must be closed, because some features (HTTP/2 transports) need an onSessionEnd hook. The old way, the default constructor of DefaultRepositorySystemSession, 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 ArtifactType implementation, class path generation and class path filtering are now Maven’s business (the classes are still in the Resolver jars, deprecated, with their replacements in maven-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 -X)

HttpTransporter

ApacheTransporter

ApacheTransporter

Option names of the transport

aether.connector.*

aether.transport.*

aether.transport.*

Option names of the split local repository

aether.enhancedLocalRepository.*

aether.lrm.enhanced.* (old names accepted)

aether.lrm.enhanced.*

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 groupId=true

on, dormant until a file exists

on, dormant until a file exists

aether.system.dependencyVisitor

ignored

levelOrder default, affects plugin realms

levelOrder default, affects plugin realms

Named lock factories (-X)

not printed

file-lock, rwlock-local, semaphore-local, noop

file-lock, rwlock-local, semaphore-local, noop

Repository key in _remote.repositories and in split directories

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 to false for that repository, or switch the filter off with -Daether.remoteRepositoryFilter.prefixes=false while you investigate. On Maven 4.0.0-rc-7 the message tells you.

  • groupId: G:... NOT allowed from ...: a groupId-<id>.txt file excludes a group that a plugin or a parent POM needs. Record a fresh list, or start the file with *.

  • prefixes: No input available: noInputOutcome is false and the repository has no prefixes file.

  • An aether.* option has no effect. It was set in the POM, in a settings profile with a jdk, os, property or file activation, with the wrong name for this Maven (aether.connector.* against aether.transport.*), or the Resolver release of this Maven does not have it. Look for it in -X under 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=preOrder in .mvn/maven.config as 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.split on 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, in MAVEN_ARGS or MAVEN_OPTS, or in an active settings.xml profile; 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 (sparseDirectory or summaryFile source, record, failIfMissing) verify downloads against values that you keep.

  • The local repository can be split into installed and cached (and further by release, snapshot and repository); the key names differ between Resolver 1.9 and 2.

  • aether.system.dependencyVisitor is levelOrder by 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.