Maven Dependency Analysis & Convergence
|
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 with the Apache Maven Dependency Plugin 3.11.0 and the Apache Maven Enforcer
Plugin 3.6.3; the third-party MojoHaus Versions Maven Plugin 2.23.0 is covered at the end. Every example was run on JDK 26 on
2026-10-16. Where the output is identical on Maven 3.9.16 and Maven 4.0.0-rc-7 (a release candidate) the text says so; where it
differs the difference is named. Paths in the console output were shortened to /work/mvn-dep-analysis/..., and the one line
Loaded N auto-discovered prefixes for remote repository ... that Maven 3.10 prints while resolving was left out.
A Maven dependency graph is computed, not written: the build you read in the POM and the build that actually runs differ by
every transitive dependency, every mediation decision and every management override. Declaring a dependency is easy; knowing
what you really got, whether you still use it, and whether two libraries agree about a third one needs tools. This page
collects them. How the graph is built (scopes, nearest-wins mediation, exclusions) is explained on
Dependencies, Scopes & Mediation;
dependencyManagement and BOMs are on
Dependency Management & BOMs; version
syntax and snapshots are on
Versions, Snapshots & Ranges; the
enforcer plugin as a whole is on The Enforcer Plugin. The
cross-tool vocabulary is in
Dependency Management Fundamentals.
Two example projects
Two scratch projects are used. The first is the multi-module tidepool build (tidepool-model, tidepool-engine,
tidepool-cli, a JUnit Jupiter 6.1.3 test in the engine, Commons CSV 1.14.1 and picocli 4.7.7), with the dependency plugin
pinned to 3.11.0 in pluginManagement. The second is a small module, tidepool-report, that is deliberately inconsistent:
<dependencies>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.14.1</version>
</dependency>
</dependencies>
Both libraries need Commons IO, but Commons FileUpload 1.5 asks for 2.11.0 and Commons CSV 1.14.1 asks for 2.20.0. The rest of the page is largely about finding, explaining and ending that disagreement. Both projects are this page’s own lab setup, not examples from the Maven documentation.
Seeing the graph: dependency:tree
dependency:tree prints the resolved dependency hierarchy of the project that is being built, the one Maven actually uses.
For the tidepool CLI module (mvn -pl tidepool-cli dependency:tree):
[INFO] --- dependency:3.11.0:tree (default-cli) @ tidepool-cli ---
[INFO] io.tidepool:tidepool-cli:jar:1.0.0
[INFO] +- io.tidepool:tidepool-engine:jar:1.0.0:compile
[INFO] | +- io.tidepool:tidepool-model:jar:1.0.0:compile
[INFO] | \- org.apache.commons:commons-csv:jar:1.14.1:compile
[INFO] | +- commons-io:commons-io:jar:2.20.0:compile
[INFO] | \- commons-codec:commons-codec:jar:1.19.0:compile
[INFO] +- info.picocli:picocli:jar:4.7.7:compile
[INFO] \- org.junit.jupiter:junit-jupiter:jar:6.1.3:test
[INFO] +- org.junit.jupiter:junit-jupiter-api:jar:6.1.3:test
[INFO] | +- org.opentest4j:opentest4j:jar:1.3.0:test
[INFO] | +- org.junit.platform:junit-platform-commons:jar:6.1.3:test
[INFO] | +- org.apiguardian:apiguardian-api:jar:1.1.2:test
[INFO] | \- org.jspecify:jspecify:jar:1.0.0:test
[INFO] +- org.junit.jupiter:junit-jupiter-params:jar:6.1.3:test
[INFO] \- org.junit.jupiter:junit-jupiter-engine:jar:6.1.3:test
[INFO] \- org.junit.platform:junit-platform-engine:jar:6.1.3:test
Each line is groupId:artifactId:type[:classifier]:version:scope; the indentation is the dependency path. The module’s own
dependencies are the first level; tidepool-model appears only below tidepool-engine, which matters in the next section.
Parameters
| Parameter | User property | Default / since | Meaning |
|---|---|---|---|
|
|
|
Include the nodes that mediation omitted, with the reason (see below). |
|
|
since 2.0-alpha-6 |
Comma-separated filter: only the matching artifacts, and the paths that lead to them, are kept. Syntax
|
|
|
since 2.0-alpha-6 |
The same syntax, but the matches are removed from the tree. |
|
|
since 2.0-alpha-5 |
Resolve the tree for one scope (for example |
|
|
|
|
|
|
since 2.0-alpha-5 |
Write the tree to a file instead of the console. |
|
|
|
Append to |
|
|
|
Line-drawing characters: |
|
|
|
Character set of |
|
|
|
Do nothing. |
Source: dependency:tree parameters
Why is this version here? -Dverbose
By default the tree shows only the winners. With -Dverbose the losers of mediation appear in parentheses, with the reason. On
tidepool-report:
[INFO] --- dependency:3.11.0:tree (default-cli) @ tidepool-report ---
[INFO] io.tidepool:tidepool-report:jar:1.0.0
[INFO] +- commons-fileupload:commons-fileupload:jar:1.5:compile
[INFO] | \- commons-io:commons-io:jar:2.11.0:compile
[INFO] \- org.apache.commons:commons-csv:jar:1.14.1:compile
[INFO] +- (commons-io:commons-io:jar:2.20.0:compile - omitted for conflict with 2.11.0)
[INFO] \- commons-codec:commons-codec:jar:1.19.0:compile
The plain tree shows only commons-io:2.11.0 below Commons FileUpload and nothing below Commons CSV except Commons Codec;
the verbose tree shows that Commons CSV wanted 2.20.0 and lost. Both versions sit at depth 2, so the tie is broken by
declaration order: Commons FileUpload is declared first, so its Commons IO wins. Commons CSV 1.14.1 will run against a Commons
IO two minor lines older than the one it was built and tested with. The three Maven releases print exactly the same verbose
tree: the output above was identical on 3.10.0, 3.9.16 and 4.0.0-rc-7.
Finding one artifact: -Dincludes
On a real project the tree has hundreds of lines. -Dincludes keeps only the paths that lead to the artifact you ask about:
$ mvn -pl tidepool-cli dependency:tree -Dincludes=org.apache.commons,commons-io
[INFO] --- dependency:3.11.0:tree (default-cli) @ tidepool-cli ---
[INFO] io.tidepool:tidepool-cli:jar:1.0.0
[INFO] \- io.tidepool:tidepool-engine:jar:1.0.0:compile
[INFO] \- org.apache.commons:commons-csv:jar:1.14.1:compile
[INFO] \- commons-io:commons-io:jar:2.20.0:compile
-Dincludes=... takes a comma-separated list, and each entry is a full filter pattern, so commons-io matches by group and
org.apache.commons matches the Commons CSV group. The complementary -Dexcludes removes noise, such as the whole JUnit
subtree, when it is the test scope you do not care about; -Dscope=runtime does it more simply by not resolving test
dependencies at all.
Output formats
-DoutputType renders the same tree for other tools. dot is a Graphviz digraph; tgf is Trivial Graph Format (a node list, a
# line, then the edges); graphml and json (3.7.0) are for graph viewers and scripts. For the runtime scope of the CLI:
$ mvn -pl tidepool-cli dependency:tree -Dscope=runtime -DoutputType=dot
[INFO] digraph "io.tidepool:tidepool-cli:jar:1.0.0" {
[INFO] "io.tidepool:tidepool-cli:jar:1.0.0" -> "io.tidepool:tidepool-engine:jar:1.0.0:compile" ;
[INFO] "io.tidepool:tidepool-cli:jar:1.0.0" -> "info.picocli:picocli:jar:4.7.7:compile" ;
[INFO] "io.tidepool:tidepool-engine:jar:1.0.0:compile" -> "io.tidepool:tidepool-model:jar:1.0.0:compile" ;
[INFO] "io.tidepool:tidepool-engine:jar:1.0.0:compile" -> "org.apache.commons:commons-csv:jar:1.14.1:compile" ;
[INFO] "org.apache.commons:commons-csv:jar:1.14.1:compile" -> "commons-io:commons-io:jar:2.20.0:compile" ;
[INFO] "org.apache.commons:commons-csv:jar:1.14.1:compile" -> "commons-codec:commons-codec:jar:1.19.0:compile" ;
[INFO] }
(Trailing spaces on the console lines were trimmed.) Add -DoutputFile=graph.dot to write the graph without the [INFO]
prefix. With -DoutputType=tgf the same graph is printed as seven numbered nodes followed by # and six
from to scope edges, each node number being a hash of the coordinates.
Auditing the declarations: dependency:analyze
The tree shows what you got. dependency:analyze asks whether the POM declares what the code uses. It reads the compiled
bytecode of your classes and tests with the Maven Dependency Analyzer, computes which dependency JARs the bytecode refers to, and
compares that with the declared dependencies. It reports three groups:
-
Used undeclared: the code uses a class from a JAR that arrives only transitively. If the library that brought it drops it, your build breaks.
-
Unused declared: a declared dependency that no class refers to. Dead weight, or a runtime-only dependency the analysis cannot see.
-
Non-test scoped, test only: a compile-scope dependency that only test classes use (should be
testscope).
analyze, analyze-only, analyze-report and analyze-duplicate
| Goal | Purpose |
|---|---|
|
For the command line. It always forks the lifecycle up to |
|
The same analysis for binding in the POM; it assumes |
|
The same results as a report for |
|
Compares resolved versions with |
|
Finds exclusions that no longer exclude anything (since 3.7.0). |
|
Finds dependencies declared twice in |
Run over the whole tidepool reactor (output reduced to the plugin lines and warnings; the compiler and resources lines of the
forked test-compile were left out):
$ mvn dependency:analyze
[INFO] --- dependency:3.11.0:analyze (default-cli) @ tidepool ---
[INFO] Skipping pom project
[INFO] --- dependency:3.11.0:analyze (default-cli) @ tidepool-model ---
[WARNING] Unused declared dependencies found:
[WARNING] org.junit.jupiter:junit-jupiter:jar:6.1.3:test
[INFO] --- dependency:3.11.0:analyze (default-cli) @ tidepool-engine ---
[WARNING] Used undeclared dependencies found:
[WARNING] org.junit.jupiter:junit-jupiter-api:jar:6.1.3:test
[WARNING] Unused declared dependencies found:
[WARNING] org.junit.jupiter:junit-jupiter:jar:6.1.3:test
[INFO] --- dependency:3.11.0:analyze (default-cli) @ tidepool-cli ---
[WARNING] Used undeclared dependencies found:
[WARNING] io.tidepool:tidepool-model:jar:1.0.0:compile
[WARNING] Unused declared dependencies found:
[WARNING] org.junit.jupiter:junit-jupiter:jar:6.1.3:test
[INFO] BUILD SUCCESS
Three real findings, and one that is not a finding:
-
tidepool-cliusesio.tidepool.model.Stationbut declares onlytidepool-engine;tidepool-modelarrives through the engine. This is the textbook used undeclared case and it is a true positive. -
tidepool-modelandtidepool-clihave no tests, sojunit-jupiteris truly unused there. The parent POM declares it for every module, which is the cause. -
tidepool-enginehas a test that importsorg.junit.jupiter.api.Test. The declared artifact is the aggregatorjunit-jupiter, which contains no classes of its own; the classes live injunit-jupiter-api, so the analysis reports the aggregator as unused and the API artifact as used but undeclared. This is a false positive produced by how the JUnit aggregator is packaged, and the usual reason for the ignore settings below.
The same warnings, in the same order, were produced by Maven 3.9.16 and by 4.0.0-rc-7. (4.0.0-rc-7 additionally prints
Unable to find the root directory. Create a .mvn directory... when the project has no .mvn directory.)
Seeing what is used: -Dverbose, -DoutputXML, -DscriptableOutput
-Dverbose also prints the used declared dependencies, and under each used undeclared dependency the classes that were found:
$ mvn -pl tidepool-cli dependency:analyze -Dverbose
[INFO] --- dependency:3.11.0:analyze (default-cli) @ tidepool-cli ---
[INFO] Used declared dependencies found:
[INFO] io.tidepool:tidepool-engine:jar:1.0.0:compile
[INFO] info.picocli:picocli:jar:4.7.7:compile
[WARNING] Used undeclared dependencies found:
[WARNING] io.tidepool:tidepool-model:jar:1.0.0:compile
[WARNING] class io.tidepool.model.Height
[WARNING] class io.tidepool.model.Station
[WARNING] Unused declared dependencies found:
[WARNING] org.junit.jupiter:junit-jupiter:jar:6.1.3:test
(The two trailing Ignored ... groups that -Dverbose adds were omitted; with no ignore settings they repeat the warnings.)
-DoutputXML prints the <dependency> element to paste for the missing dependencies, and -DscriptableOutput prints
machine-readable lines starting with a marker (-DscriptableFlag, default $$$%%%) for scripts:
$ mvn -pl tidepool-cli dependency:analyze -DoutputXML
...
[INFO] Add the following to your pom to correct the missing dependencies:
[INFO]
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>1.0.0</version>
</dependency>
Failing the build
By default the report is only advice. -DfailOnWarning=true (or <failOnWarning> in the POM) turns it into an error:
$ mvn -pl tidepool-cli dependency:analyze -DfailOnWarning
[ERROR] Used undeclared dependencies found:
[ERROR] io.tidepool:tidepool-model:jar:1.0.0:compile
[ERROR] Unused declared dependencies found:
[ERROR] org.junit.jupiter:junit-jupiter:jar:6.1.3:test
[ERROR] BUILD FAILURE
...
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-dependency-plugin:3.11.0:analyze (default-cli) on project tidepool-cli: Dependency problems found -> [Help 1]
For a permanent gate bind analyze-only to verify, and ignore the known false positives. The configuration below was added to
the parent POM; the two ignore lists name the aggregator and its API (the pattern is groupId:artifactId, with the same
optional type:version tail and * wildcards as the other filters):
<build>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>analyze</id>
<goals>
<goal>analyze-only</goal>
</goals>
<configuration>
<failOnWarning>true</failOnWarning>
<ignoredUnusedDeclaredDependencies>
<ignoredUnusedDeclaredDependency>org.junit.jupiter:junit-jupiter</ignoredUnusedDeclaredDependency>
</ignoredUnusedDeclaredDependencies>
<ignoredUsedUndeclaredDependencies>
<ignoredUsedUndeclaredDependency>org.junit.jupiter:junit-jupiter-api</ignoredUsedUndeclaredDependency>
</ignoredUsedUndeclaredDependencies>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
With the fault in tidepool-cli still there, mvn verify stops exactly at the real problem and the false positives are
silent:
[INFO] --- dependency:3.11.0:analyze-only (analyze) @ tidepool-cli ---
[ERROR] Used undeclared dependencies found:
[ERROR] io.tidepool:tidepool-model:jar:1.0.0:compile
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary for tidepool 1.0.0:
[INFO]
[INFO] tidepool ........................................... SUCCESS [ 1.186 s]
[INFO] tidepool-model ..................................... SUCCESS [ 1.026 s]
[INFO] tidepool-engine .................................... SUCCESS [ 0.919 s]
[ERROR] tidepool-cli ....................................... FAILURE [ 0.085 s]
[INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE
...
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-dependency-plugin:3.11.0:analyze-only (analyze) on project tidepool-cli: Dependency problems found -> [Help 1]
The fix is one command, described in Editing the POM from the command line: add and remove; afterwards the same
mvn verify prints BUILD SUCCESS and the analyze-only (analyze) execution is silent in all four modules.
All analyze parameters
| Parameter | User property | Default / since | Meaning |
|---|---|---|---|
|
|
|
Fail the build if any warning is found. |
|
|
|
Also list the used dependencies, and the classes that make a dependency used. |
|
|
|
Print the XML for the used-undeclared dependencies. |
|
|
|
Print machine-readable lines for the used-undeclared dependencies, starting with the flag. |
|
- |
since 2.10 |
Dependencies excluded from all three lists (unused declared, used undeclared, non-test scoped). |
|
- |
since 2.10 |
Dependencies not reported when declared but unused. |
|
- |
since 2.10 |
Dependencies not reported when used but undeclared. |
|
- |
since 3.3.0; default |
Dependencies not reported when used only by tests but not test-scoped. |
|
|
|
Ignore the whole non-test-scoped group. |
|
|
|
Never report |
|
|
|
Do not report |
|
- |
since 2.6 |
Force |
|
|
since 3.7.0 |
Regular expressions on full class names; matching classes are not analysed (for generated code). |
|
- |
|
Packagings that are skipped. |
|
|
|
Plexus role hint of another analyzer implementation, supplied as a plugin dependency. |
|
|
|
Do nothing. |
The plugin does not warn about a few dependencies where bytecode analysis is known to be unreliable, SLF4J being the example the
documentation gives. Two causes of false positives are named in the documentation: annotations with source retention, which do
not appear in bytecode, and classes loaded by reflection at runtime. For those use usedDependencies or the ignore lists.
Because the analysis is on bytecode, it cannot see a dependency that is only needed at runtime (a JDBC driver, a logging
backend); declare those with runtime scope and add ignoreUnusedRuntime.
Source: dependency:analyze parameters
Management overrides: dependency:analyze-dep-mgt
Maven applies dependencyManagement to transitive dependencies, so a managed version always wins there. A mismatch can only
remain where a direct declaration overrides the managed version, which is why the goal ignores direct dependencies by default
(ignoreDirect=true, user property mdep.analyze.ignore.direct). In a copy of tidepool-report the management section pins
Commons IO 2.20.0 while a direct declaration asks for 2.11.0:
$ mvn -f pom-mgt.xml dependency:analyze-dep-mgt
[INFO] --- dependency:3.11.0:analyze-dep-mgt (default-cli) @ tidepool-report ---
[INFO] Found Resolved Dependency/DependencyManagement mismatches:
[INFO] Ignoring Direct Dependencies.
[INFO] None
$ mvn -f pom-mgt.xml dependency:analyze-dep-mgt -Dmdep.analyze.ignore.direct=false
[INFO] --- dependency:3.11.0:analyze-dep-mgt (default-cli) @ tidepool-report ---
[INFO] Found Resolved Dependency/DependencyManagement mismatches:
[INFO] Dependency: commons-io:commons-io:jar
[INFO] DepMgt : 2.20.0
[INFO] Resolved: 2.11.0
[WARNING] Potential problems found in Dependency Management
-Dmdep.analyze.failBuild=true turns the warning into
Failed to execute goal ...:analyze-dep-mgt (default-cli) on project tidepool-report: Found Dependency errors. Use the goal
to keep a parent’s or BOM’s management section honest: a child that silently declares its own version defeats the point of
managing versions.
Stale exclusions: dependency:analyze-exclusions
An <exclusion> records a decision ("do not bring me X through this dependency"). When the dependency is later upgraded and
stops bringing X, the exclusion silently becomes noise. Since 3.7.0 analyze-exclusions finds those. Here Commons FileUpload
excludes Commons IO (effective) and Commons Logging (which Commons FileUpload 1.5 does not depend on):
$ mvn -f pom-excl.xml dependency:analyze-exclusions
[INFO] --- dependency:3.11.0:analyze-exclusions (default-cli) @ tidepool-report ---
[WARNING] tidepool-report defines following unnecessary excludes
[WARNING] commons-fileupload:commons-fileupload:1.5
[WARNING] - commons-logging:commons-logging @ line: 26
The report carries the POM line number. With -Dmdep.exclusion.fail=true (exclusionFail, since 3.7.0) the same lines are
logged at ERROR level and the build fails with Invalid exclusions found. Note what the tree looks like after the effective
exclusion: Commons IO 2.20.0 is now taken from Commons CSV, so Commons FileUpload 1.5 runs with a newer Commons IO than it was
built against. Exclusions change the runtime of the excluded-from library; run its tests.
Duplicate declarations: dependency:analyze-duplicate
analyze-duplicate lists a dependency declared twice in the same POM. On Maven 3.9.16, which only warns about it:
$ mvn -f pom-dup.xml dependency:analyze-duplicate
[WARNING] Some problems were encountered while building the effective model for io.tidepool:tidepool-report:jar:1.0.0
[WARNING] 'dependencies.dependency.(groupId:artifactId:type:classifier)' must be unique: org.apache.commons:commons-csv:jar -> duplicate declaration of version 1.14.1 @ line 27, column 17
...
[INFO] --- dependency:3.11.0:analyze-duplicate (default-cli) @ tidepool-report ---
[INFO] List of duplicate dependencies defined in <dependencies/> in your pom.xml:
o org.apache.commons:commons-csv:jar
On Maven 3.10.0 the goal can no longer be reached for such a POM: the stricter validation introduced in 3.10.0 refuses to build it at all.
[ERROR] Some problems were encountered while processing the POMs:
[ERROR] 'dependencies.dependency.(groupId:artifactId:type:classifier)' must be unique: org.apache.commons:commons-csv:jar -> duplicate declaration of version 1.14.1 @ line 27, column 17
[ERROR] The build could not read 1 project -> [Help 1]
Maven 4.0.0-rc-7 also stops with Encountered 1 problem(s) while processing the POMs. The 3.10.0 release notes describe the
change ("duplicated dependency entries or duplicated plugin entries will not be tolerated anymore") and give
-Dmaven.resolver.validation=off as a temporary escape hatch (on 3.10.0 the lower-case value fails with No enum constant; the property governs coordinate validation, see Maven: Versions & Release History). The goal is still useful on 3.9.x and as a migration check
before upgrading. The enforcer rule banDuplicatePomDependencyVersions covers the sibling problem of the same artifact being
declared with different versions through properties.
Source: dependency:analyze-duplicate
Acting on the graph
copy-dependencies and build-classpath
dependency:copy-dependencies copies the project’s dependency JARs into a directory (default target/dependency, set with
-DoutputDirectory); it binds by default to process-sources and is the usual way to assemble a lib/ directory for a
distribution (the alternative is an assembly or shade, see
Uber JARs with Shade & Assembly).
Runtime-scope dependencies of the CLI:
$ mvn -pl tidepool-cli -am dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/libs
$ ls tidepool-cli/target/libs
commons-codec-1.19.0.jar
commons-csv-1.14.1.jar
commons-io-2.20.0.jar
picocli-4.7.7.jar
tidepool-engine-1.0.0.jar
tidepool-model-1.0.0.jar
$ mvn -pl tidepool-cli -am dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/libs2 -Dmdep.stripVersion=true -DexcludeGroupIds=io.tidepool
$ ls tidepool-cli/target/libs2
commons-codec.jar
commons-csv.jar
commons-io.jar
picocli.jar
The parameters (the same filter family is shared with unpack-dependencies, go-offline and build-classpath):
| Parameter | User property | Meaning |
|---|---|---|
|
|
Destination; default |
|
|
Scope threshold, interpreted as Maven filters for creating a classpath ( |
|
same names |
Comma-separated lists. |
|
same names |
Filter by packaging type or classifier; |
|
|
Only direct dependencies. |
|
|
Drop the version, classifier or type from the file name. |
|
|
Write a |
|
|
One subdirectory per artifact, per type or per scope. |
|
|
Avoid name clashes by prefixing the group id. |
|
|
Copy POMs and |
|
same names |
Overwrite rules; |
|
- |
since 3.10.0: start from selected |
|
|
With |
|
|
Skip; the second only for IDE incremental builds (3.4.0). |
Because all files go to one directory, two dependencies with the same file name overwrite each other. graphRoots is the
newest addition. Configured in the plugin as a list of <graphRoot> elements with groupId and artifactId, it restricted the
copy of tidepool-report to the Commons CSV subtree, and -DincludeScope=compile then produced:
commons-codec-1.19.0.jar
commons-csv-1.14.1.jar
commons-io-2.20.0.jar
jsr305-3.0.2.jar
spotbugs-annotations-4.9.3.jar
Without graphRoots the same command copies the project’s own four JARs (commons-codec-1.19.0.jar, commons-csv-1.14.1.jar,
commons-fileupload-1.5.jar, commons-io-2.11.0.jar). Note that the subtree resolution includes the compile-scope
dependencies that the root artifact itself declares and that are not part of the project’s resolved classpath (here two
annotation JARs); the output was not analysed further.
dependency:build-classpath prints, or writes with -Dmdep.outputFile=target/cp.txt, the classpath in java -cp form, using
the local repository paths. For tidepool-report the file holds four entries separated by the platform separator:
commons-fileupload/commons-fileupload/1.5/commons-fileupload-1.5.jar, commons-io/commons-io/2.11.0/commons-io-2.11.0.jar,
org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar and commons-codec/commons-codec/1.19.0/commons-codec-1.19.0.jar
(listed relative to the local repository). dependency:resolve lists the resolved files with their Java module names:
[INFO] --- dependency:3.11.0:resolve (default-cli) @ tidepool-report ---
[INFO] The following files have been resolved:
[INFO] commons-fileupload:commons-fileupload:jar:1.5:compile -- module commons.fileupload (auto)
[INFO] commons-io:commons-io:jar:2.11.0:compile -- module org.apache.commons.io [auto]
[INFO] org.apache.commons:commons-csv:jar:1.14.1:compile -- module org.apache.commons.csv
[INFO] commons-codec:commons-codec:jar:1.19.0:compile -- module org.apache.commons.codec
list, resolve-sources and resolve-plugins are close relatives. dependency:sources is deprecated in favour of
resolve-sources and will be removed.
list-repositories
dependency:list-repositories collects the dependencies and prints the repositories the build and its transitive dependencies'
POMs use. The documentation says it shows repositories "defined in your settings, poms, and declared in transitive dependency poms":
[INFO] --- dependency:3.11.0:list-repositories (default-cli) @ tidepool-report ---
[INFO] Project remote repositories used by this build:
* apache.snapshots (https://repository.apache.org/snapshots, default, snapshots)
* central (https://repo.maven.apache.org/maven2, default, releases)
* central (https://repo.maven.apache.org/maven2, default, releases)
apache.snapshots is not in the example POM or in settings.xml. It comes from a transitive dependency’s parent chain: the
Commons CSV POM inherits from commons-parent, which inherits from the Apache parent POM, and the versions of that parent in
the local repository (30 to 32, checked) declare https://repository.apache.org/snapshots as a snapshots-only repository. It is
harmless here (snapshots only, never asked for a release) but it is the kind of silently inherited repository that the
repository chapter explains how to block; see
Repositories, Mirrors & Repository Managers.
(central is listed twice; that is how 3.10.0 reports the default and the effective entry.)
go-offline
dependency:go-offline resolves "everything this project is dependent on (dependencies, plugins, reports)" so that
mvn -o can run later. The usage page says it is exactly dependency:resolve dependency:resolve-plugins. To show the effect
without touching ~/.m2, a private local repository was used (-Dmaven.repo.local=...). With an empty one, an offline build
fails at the first thing it needs, the JUnit BOM:
$ mvn -o -Dmaven.repo.local=/work/mvn-dep-analysis/repo-off verify
[ERROR] Some problems were encountered while processing the POMs:
[ERROR] Non-resolvable import POM: The following artifacts could not be resolved: org.junit:junit-bom:pom:6.1.3 (absent): Cannot access central (https://repo.maven.apache.org/maven2) in offline mode and the artifact org.junit:junit-bom:pom:6.1.3 has not been downloaded from it before. @ line 26, column 19
[ERROR] 'dependencies.dependency.version' for org.junit.jupiter:junit-jupiter:jar is missing. @ line 37, column 17
After mvn -Dmaven.repo.local=... dependency:go-offline (BUILD SUCCESS, 160 JARs and 51 MB downloaded in the fresh repository,
including the compiler, resources, surefire, jar, clean, install, deploy, enforcer, dependency and even the site plugin) the
build gets further, but it still fails offline, at the tests:
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[WARNING] The POM for org.apache.maven.surefire:surefire-junit-platform:jar:3.6.0 is missing, no dependency information available
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.6.0:test (default-test) on project tidepool-engine: The following artifacts could not be resolved: org.apache.maven.surefire:surefire-junit-platform:jar:3.6.0 (absent): Cannot access central (https://repo.maven.apache.org/maven2) in offline mode and the artifact org.apache.maven.surefire:surefire-junit-platform:jar:3.6.0 has not been downloaded from it before.
go-offline cannot know which Surefire provider a test run will pick, because the provider is chosen at run time from the test
classpath and downloaded then. This is the classic reason why "go-offline" builds still reach out to the network. The remedy is
to make the runtime download explicit: declare the provider as a dependency of the plugin, and (JUnit 6 on Surefire 3.6.0) the
launcher as a test dependency:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit-platform</artifactId>
<version>3.6.0</version>
</dependency>
</dependencies>
</plugin>
Adding only the provider moved the failure to org.junit.platform:junit-platform-launcher:jar:6.1.3 (absent) (165 JARs). With
the provider plugin dependency and org.junit.platform:junit-platform-launcher as a test dependency (its version comes from
the junit-bom import), a fresh go-offline (166 JARs, 52 MB) was followed by a fully offline build:
$ mvn -o -Dmaven.repo.local=/work/mvn-dep-analysis/repo-B verify
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.021 s -- in io.tidepool.engine.StationReaderTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] Reactor Summary for tidepool 1.0.0:
[INFO] tidepool ........................................... SUCCESS [ 0.001 s]
[INFO] tidepool-model ..................................... SUCCESS [ 0.558 s]
[INFO] tidepool-engine .................................... SUCCESS [ 0.428 s]
[INFO] tidepool-cli ....................................... SUCCESS [ 0.055 s]
[INFO] BUILD SUCCESS
Other limits: excludeReactor (default true) skips modules of the current reactor, so the first multi-module build still has
to build them; and the site or release plugins resolve their own tools only when they run. The pragmatic alternative, used
by many CI pipelines, is to run one full online mvn verify into a cached local repository and reuse that. Gradle’s
--offline works from the dependency cache in the same way (see
Dependency Locking, Verification & Caching).
purge-local-repository
dependency:purge-local-repository removes the project’s dependencies from the local repository and, by default, resolves them
again (reResolve=true), which is the way to check that a corrupted or half-downloaded artifact is not the problem. It is the
one goal here that can destroy data, so the example again uses a private repository. After
dependency:resolve into it, the four dependencies of tidepool-report were present (commons-fileupload/1.5,
commons-io/2.11.0, commons-csv/1.14.1, commons-codec/1.19.0, plus other Commons IO versions required by the plugins):
$ mvn -Dmaven.repo.local=/work/mvn-dep-analysis/repo-q dependency:purge-local-repository -DreResolve=false -Dverbose
[INFO] --- dependency:3.11.0:purge-local-repository (default-cli) @ tidepool-report ---
[INFO] Deleting 4 transitive dependencies for project tidepool-report from <repo> with artifact version resolution fuzziness
[INFO] Purging artifact: commons-fileupload:commons-fileupload:jar:1.5
[INFO] Deleting: <repo>/commons-fileupload/commons-fileupload/1.5
[INFO] Purging artifact: org.apache.commons:commons-csv:jar:1.14.1
[INFO] Deleting: <repo>/org/apache/commons/commons-csv/1.14.1
[INFO] Purging artifact: commons-io:commons-io:jar:2.11.0
[INFO] Deleting: <repo>/commons-io/commons-io/2.11.0
[INFO] Purging artifact: commons-codec:commons-codec:jar:1.19.0
[INFO] Deleting: <repo>/commons-codec/commons-codec/1.19.0
(<repo> stands for the private repository path.) The version directories of those four artifacts were gone afterwards; the
other Commons IO versions (2.17.0, 2.20.0, 2.21.0) that the plugins use stayed. The same goal with
-DresolutionFuzziness=file -Dexclude=commons-io:commons-io deleted only the three JAR files of the other artifacts and left
the .pom, checksum and _remote.repositories files, and Commons IO untouched.
| Parameter | User property | Meaning |
|---|---|---|
|
|
Default |
|
|
|
|
|
Default |
|
|
|
|
|
Since 2.6: delete these given artifacts instead of the project’s; it also works outside a project. |
|
|
Since 2.4: purge only snapshot artifacts. |
|
|
Print each deletion. |
Outside a project, -DmanualInclude=commons-io:commons-io:2.11.0 printed Deleting 1 manual dependency from <repo> and removed the
2.11.0 directory. Never run these goals with -Dmaven.repo.local pointing at your shared ~/.m2 unless you mean it.
Editing the POM from the command line: add and remove
dependency:add and dependency:remove are new in dependency plugin 3.11.0. They edit pom.xml in place while preserving
comments, indentation and encoding. Detecting the missing declaration reported above, the fix is:
$ mvn -pl tidepool-cli dependency:add -Dgav=io.tidepool:tidepool-model:1.0.0
[INFO] --- dependency:3.11.0:add (default-cli) @ tidepool-cli ---
[INFO] Added dependency io.tidepool:tidepool-model:1.0.0 to pom.xml
[INFO] BUILD SUCCESS
The change to tidepool-cli/pom.xml was exactly one new <dependency> element after the existing ones, and the following
mvn verify with the analyze-only gate passed in all four modules. A second add of the same coordinates fails with:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-dependency-plugin:3.11.0:add (default-cli) on project tidepool-cli: Dependency io.tidepool:tidepool-model already exists in pom.xml. Remove it first with dependency:remove, then re-add. -> [Help 1]
| Parameter | User property | Meaning (all since 3.11.0) |
|---|---|---|
|
|
|
|
same names |
Override the attributes. Scopes are validated against |
|
|
Insert into |
|
|
Target an existing profile by id. |
|
same names |
|
Adding a managed version in the parent and a version-less test dependency in a child, then removing the managed entry again:
$ mvn -pl . dependency:add -Dmanaged=true -Dgav=org.assertj:assertj-core:3.27.3
[INFO] Added dependency org.assertj:assertj-core:3.27.3 to pom.xml
$ mvn -pl tidepool-engine dependency:add -Dgav=org.assertj:assertj-core -Dscope=test
[INFO] Added dependency org.assertj:assertj-core [scope=test] to pom.xml
$ mvn -pl . dependency:remove -Dmanaged=true -Dgav=org.assertj:assertj-core
[WARNING] The following child modules depend on org.assertj:assertj-core without an explicit version and will break: [tidepool-engine]. Proceeding with removal.
[INFO] Removed dependency org.assertj:assertj-core from pom.xml
The warning is accurate. The next build of the project fails at model-building time with
'dependencies.dependency.version' for org.assertj:assertj-core:jar is missing. @ line 26, column 17, and because add and
remove themselves need a loadable project, dependency:remove can no longer repair it: the POM must be edited by hand. Keep the
goals for small, deliberate edits. remove matches groupId, artifactId, type (default jar) and classifier, refuses to
edit a dependency whose version comes from a property reference in the raw POM, and takes the same managed and profile
options.
Source: dependency:add parameters
Source: dependency:remove parameters
Enforcing convergence: the enforcer rules
The analysis goals tell you; the enforcer plugin makes the build refuse. Two built-in rules police the shape of the graph:
-
dependencyConvergencerequires that every artifact resolves to one version in all paths of the tree. If A needs C 1.0 and B needs C 2.0 the rule fails, even if mediation would pick one of them quietly. -
requireUpperBoundDepsrequires that the version Maven selected is equal to or higher than the highest version requested anywhere in the tree. It targets the dangerous half of the problem: a library running against an older version than it asked for.
Both are bound to the enforcer’s enforce goal. This was added to the tidepool-report POM (the enforcer plugin is pinned to
3.6.3 in pluginManagement):
<build>
<plugins>
<plugin>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>enforce-dependencies</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<dependencyConvergence/>
<requireUpperBoundDeps/>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
mvn validate (the enforce goal’s default phase) fails, with both rules reported and the paths to both versions:
[INFO] --- enforcer:3.6.3:enforce (enforce-dependencies) @ tidepool-report ---
[ERROR] Rule 0: org.apache.maven.enforcer.rules.dependency.DependencyConvergence failed with message:
[ERROR] Failed while enforcing releasability.
[ERROR] Dependency convergence error for commons-io:commons-io:jar:2.11.0. Paths to dependency are:
[ERROR] +-io.tidepool:tidepool-report:jar:1.0.0
[ERROR] +-commons-fileupload:commons-fileupload:jar:1.5:compile
[ERROR] +-commons-io:commons-io:jar:2.11.0:compile
[ERROR] and
[ERROR] +-io.tidepool:tidepool-report:jar:1.0.0
[ERROR] +-org.apache.commons:commons-csv:jar:1.14.1:compile
[ERROR] +-commons-io:commons-io:jar:2.20.0:compile
[ERROR] Rule 1: org.apache.maven.enforcer.rules.dependency.RequireUpperBoundDeps failed with message:
[ERROR] Failed while enforcing RequireUpperBoundDeps. The error(s) are [
[ERROR] Require upper bound dependencies error for commons-io:commons-io:2.11.0. Paths to dependency are:
[ERROR] +-io.tidepool:tidepool-report:1.0.0
[ERROR] +-commons-fileupload:commons-fileupload:1.5
[ERROR] +-commons-io:commons-io:2.11.0
[ERROR] and
[ERROR] +-io.tidepool:tidepool-report:1.0.0
[ERROR] +-org.apache.commons:commons-csv:1.14.1
[ERROR] +-commons-io:commons-io:2.20.0
[ERROR] ]
(The BUILD FAILURE banner, the Failed to execute goal ...:enforce (enforce-dependencies) line and the standard help lines
were left out.) The output was identical on Maven 3.9.16 and 4.0.0-rc-7. The standalone invocation
mvn enforcer:enforce -Denforcer.rules=requireUpperBoundDeps works from the command line; the older -Drules still works but
prints Parameter 'commandLineRules' (user property 'rules') is deprecated: Use enforcer.rules property instead.
Source: Enforcer: dependencyConvergence
Source: Enforcer: requireUpperBoundDeps
Source: Enforcer: built-in rules
Four ways to make it converge, and what each does
The same failing POM was changed in four ways, each time re-running both rules.
| Change | dependencyConvergence | requireUpperBoundDeps | Comment |
|---|---|---|---|
|
passed |
passed |
The recommended fix. Management applies to every path, so the tree has one version. In a multi-module build put it in the parent or a BOM (Dependency Management & BOMs). |
|
passed |
passed |
Only Commons CSV’s 2.20.0 remains. Commons FileUpload 1.5 now runs against Commons IO 2.20.0, which it was never built with. |
A direct dependency on |
failed |
passed |
The direct, highest version satisfies the upper bound, but there are still three different paths with two versions (2.20.0 directly, 2.11.0 through FileUpload, 2.20.0 through CSV). |
Swap the declaration order (CSV first) |
failed |
passed |
Mediation now picks 2.20.0, so the upper bound is satisfied, but the divergence is still reported. This shows that the declaration-order tie-break was hiding the problem. |
The direct-dependency result is the one that surprises people: declaring the version you want does not make the tree converge.
Only dependency management or an exclusion removes the competing path. A fifth option, upgrading the stale library, helps only
if the newer release has caught up: commons-fileupload 1.6.0 depends on Commons IO 2.19.0, which is still below Commons CSV’s
2.20.0, and both rules still failed.
Tuning the rules
| Parameter | Meaning |
|---|---|
|
Only enforce for matching artifacts (format |
|
Exceptions to the includes. Wildcards are allowed in |
|
|
|
Compare snapshots by their timestamped versions (default |
With <excludes><exclude>commons-io:commons-io</exclude></excludes> inside each rule, both rules passed on the failing POM:
that is the "known and accepted" escape hatch; keep such lists short and commented. The rules also run on a test scope only if
you remove test from excludedScopes. Other rules that guard the graph (bannedDependencies, banDuplicatePomDependencyVersions,
requireReleaseDeps, requirePluginVersions) are covered on
The Enforcer Plugin.
Keeping up: the MojoHaus Versions plugin (third party)
The Versions Maven Plugin, org.codehaus.mojo:versions-maven-plugin, is not an Apache Maven plugin. It belongs to MojoHaus,
which took over the former Codehaus Mojo project (the org.codehaus.mojo coordinates were kept). Version 2.23.0 was used.
Pin it like any plugin; mvn versions:... resolves the prefix through the default org.codehaus.mojo plugin group, but an
unpinned plugin is a moving target. The examples use the full coordinates.
$ mvn org.codehaus.mojo:versions-maven-plugin:2.23.0:display-dependency-updates
[INFO] --- versions:2.23.0:display-dependency-updates (default-cli) @ tidepool-report ---
[INFO] The following dependencies in Dependencies have newer versions:
[INFO] commons-fileupload:commons-fileupload ................... 1.5 -> 1.6.0
Only Commons FileUpload is listed: Commons CSV 1.14.1 is already the newest, and the transitive Commons IO is not reported
because the goal looks at the project’s own declarations (and management); -Dverbose would also list the dependencies without
updates. Useful parameters: allowMajorUpdates, allowMinorUpdates, allowIncrementalUpdates (all default true),
allowSnapshots, dependencyIncludes/dependencyExcludes, processDependencyManagement, ignoredVersions (regex list, to
skip milestones) and showNewestPerSegment (since 2.23.0).
display-plugin-updates does the same for plugins and also checks the minimum Maven version. In the test project it reported
that all plugins use the latest versions, then printed the warning Project does not define minimum Maven version required for
build and a long list of old plugin versions that "require Maven 2.0" and so on; read only the first lines.
use-latest-releases rewrites the POM:
$ mvn org.codehaus.mojo:versions-maven-plugin:2.23.0:use-latest-releases -DgenerateBackupPoms=false
[INFO] --- versions:2.23.0:use-latest-releases (default-cli) @ tidepool-report ---
[INFO] Updated commons-fileupload:commons-fileupload:jar:1.5 to version 1.6.0 in io.tidepool:tidepool-report:1.0.0
20c20
< <version>1.5</version>
---
> <version>1.6.0</version>
By default the plugin leaves a pom.xml.versionsBackup; the run without -DgenerateBackupPoms=false produced a backup byte
identical to the original, and versions:revert restored it (Restoring ...pom.xml from ...pom.xml.versionsBackup). In a Git
repository the backup file is unnecessary; use the flag. use-latest-releases honours the same allow*Updates switches, plus
includes/excludes, scope, processParent (default false) and excludeReactor (default true). Other goals are
use-latest-versions, use-next-releases, update-properties, set and commit/revert.
This is the update workflow the convergence rules want: the rewrite takes the stale library to 1.6.0, whose Commons IO is
2.19.0, and, as measured above, the enforcer still fails until management pins 2.20.0. Updating blindly can also take a
dependency across a major version; start with -DallowMajorUpdates=false.
Source: Versions Maven Plugin
Source: versions:use-latest-releases
Source: versions:revert
When each tool appeared
| Feature | Since | Note |
|---|---|---|
|
plugin 2.0-alpha-6 |
|
|
2.0-alpha-3 |
|
|
3.7.0 |
Reports unnecessary exclusions. |
|
2.2 |
|
|
3.10.0 |
|
|
3.11.0 |
Current release. |
|
deprecated |
Use |
Stricter POM validation (duplicate dependencies and plugins are errors) |
Maven 3.10.0 |
Turns the |
Comparable tools in Ant, Ivy and Gradle
None of these rows was run for this page; they are named from the official documentation.
| Task | Maven | Gradle | Ant with Ivy |
|---|---|---|---|
Show the resolved graph |
|
|
|
Fail on conflicting versions |
enforcer |
|
the |
Unused or undeclared dependencies |
|
not a core Gradle task |
no standard task |
Copy dependencies to a directory |
|
a |
|
Newer versions available |
MojoHaus |
version catalogs plus external tools |
no standard task |
Source: Gradle: listing dependencies
Source: Ivy: conflict managers
Where the books are outdated
The Maven 2 era books (the issue’s AM2 table) show dependency:analyze and dependency:tree from the plugin’s 2.x line.
Today the plugin is 3.11.0 and a chapter written then is missing a lot:
-
dependency:analyze-exclusions(3.7.0), thejsontree format (3.7.0),graphRoots(3.10.0) anddependency:add/remove(3.11.0) did not exist. -
A POM with duplicate dependencies, once a warning, is a build error on Maven 3.10.0.
-
dependency:sourcesand thesilentparameter are deprecated;-qreplacessilent. -
The Codehaus Mojo plugins were renamed to MojoHaus; the Versions plugin lives at mojohaus.org, and the old dependency plugin at Mojo (
dependency-maven-plugin) is the same project, renamed and moved to Apache (the plugin FAQ says so). -
Convergence enforcement was usually described with the old enforcer rule names and
-Drules; the rule parameters and theenforcer.rulesproperty above are current.
Summary of pitfalls
-
dependency:treewithout-Dverbosehides the loser of every conflict; use-Dverbosebefore blaming a library. -
On a tie in depth, the first declaration wins; reordering dependencies can change the classpath silently.
-
dependency:analyzeforkstest-compile; on a large build preferanalyze-onlybound toverifyand ignore known false positives instead of turningfailOnWarningoff. -
Aggregator artifacts (
junit-jupiter, many BOM-like starters) always look unused; the real classes belong to the artifacts below them. -
A direct declaration of a managed dependency overrides the management;
analyze-dep-mgtignores that unless-Dmdep.analyze.ignore.direct=false. -
go-offlinedoes not fetch the Surefire provider or the JUnit launcher; pre-declare them, or warm the cache with a real build. -
purge-local-repositorywith the defaultreResolve=truegoes online; with a shared~/.m2it deletes for every project. -
dependency:addandremovechange only what you ask; they do not check that child modules still build. -
Direct declaration is not convergence. Use
dependencyManagement, a BOM or an exclusion.