Transitive conflicts and exclusions

Maven · Advanced · 12 min

Classpath conflicts cause NoSuchMethodError at run time; diagnosing them is a core Maven skill.

When the same library is reachable through several paths, Maven's nearest-wins (dependency mediation) rule picks the version whose path from your POM is shortest; for equal depth, the first declaration wins. This can silently select an older version than another path needs, leading to NoSuchMethodError or ClassNotFoundException at run time.

Diagnosing:

  • mvn dependency:tree -Dverbose shows omitted versions and why (omitted for conflict with 2.18.3).
  • mvn dependency:tree -Dincludes=com.fasterxml.jackson.core narrows the output.
  • mvn dependency:analyze finds undeclared and unused dependencies.
  • mvn dependency:list -DoutputFile=... for scripting.

Fixing:

  • Manage the version in dependencyManagement: it applies to all paths, the cleanest fix.
  • Exclude a transitive dependency on a specific path: <exclusions><exclusion><groupId>..</groupId><artifactId>..</artifactId></exclusion></exclusions>, for example removing commons-logging in favor of jcl-over-slf4j, or an old log4j.
  • Declare the dependency directly (depth 1) to force it nearest; prefer management over this trick.
  • <optional>true</optional> on your own dependency stops it from propagating to consumers.

Watch for duplicate classes from relocated artifacts (javax.* vs jakarta.*, commons-lang vs commons-lang3), which no mediation can solve; the enforcer plugin's banDuplicateClasses (from extra-enforcer-rules) and dependencyConvergence rules catch these in CI.

Example

<dependencies>
  <dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
    <version>5.4.3</version>
    <exclusions>
      <exclusion>                              <!-- drop a transitive on this path -->
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>   <!-- bridge replacing it -->
    <version>2.0.17</version>
  </dependency>
</dependencies>

<!-- $ mvn dependency:tree -Dverbose -Dincludes=com.fasterxml.jackson.core
[INFO] com.acme:shop-api:jar:1.0.0-SNAPSHOT
[INFO] +- com.fasterxml.jackson.core:jackson-databind:jar:2.18.3:compile
[INFO] \- com.example:legacy-lib:jar:1.0:compile
[INFO]    \- (com.fasterxml.jackson.core:jackson-databind:jar:2.12.7:compile
             - omitted for conflict with 2.18.3)
   Fix for the other direction (an old version winning): pin it in dependencyManagement. -->

Remember

  • Nearest path wins, then first declaration; not the highest version
  • dependency:tree -Dverbose shows omitted versions and why
  • Fix with dependencyManagement first, exclusions second
  • Enforcer rules catch convergence and duplicate-class problems in CI

Watch out for

  • Excluding a transitive dependency that the library actually needs, moving the failure to run time.

Check yourself

  1. Your POM depends on A (which needs X 1.0) and B (which needs C, which needs X 2.0). Which X does Maven select?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#transitive-dependencies

Builds on

Unlocks

Nothing builds on this yet.