How to pin transitive dependency versions across multi-pom Maven project

Viewed 3078

I'm working on a large Java codebase that's split into multiple modules, each with a separate pom.xml, all parented by a top-level pom.xml.

I'm currently in the process of bringing in a couple of library dependencies. The transitive set of dependencies is large, and as luck would have it, there are conflicting dependency versions for different modules.

Here's a simplification of my situation:

project/pom.xml
       /module-a/pom.xml # references library-a, depends on library-c:v1
       /module-b/pom.xml # references library-b, depends on library-c:v2
       /module-c/pom.xml # references module-a and module-b

Now the unit tests for module-a will exercise library-a in the presence of library-c:v1, while module-b will exercise library-b in the presence of library-c:v2.

The trouble is that module-a and module-b need to live together on the same classpath when module-c is deployed, but whatever version of library-c is chosen when module-c is packaged, at least one combination of libraries hasn't been unit tested!

I'd like to pin the version of library-c at the parent pom level somehow, rather than repeating myself in every module that transitively depends on library-c; ideally it would be added in such a way indicating that it's a transitive dependency that is allowed to go away should library-a and library-b no longer rely on it.

I'd like a guarantee that there is exactly one version selected for every transitive dependency across the entire project rooted from this parent pom, and I'd like the build to blow up if this isn't true. I wrote a tool to parse the output of mvn dependency:tree (turning the leaves of the tree into a forest of paths from leaf to root, then finding all different versions of leaf with the dependency path) so I can see the problem, but without explicitly resolving the transitive dependencies for every conflict and bloating out poms with redundant declarations, this doesn't seem fruitful. It's what I'll do if I have no alternative, naturally.

What's the best way to handle this transitive dependency conflict problem with Maven?

How severe is this problem? Quite apart from getting unconvincing test coverage, in practice I see JVM-killing NoSuchMethodError at runtime from the wrong versions getting deployed. I'd prefer to see these at test time at the very least.

1 Answers

Looks like there are two aspects to this:

  1. You need to insist on a single version of a dependency, whether it is declared explicitly or acquired transitively

You can use <dependencyManagement/> here. For example in the top-level pom.xml you can pin the version of library-c:

<dependencyManagement>
 <dependencies>
    <dependency>
        <groupId>your.group.id</groupId>
        <artifactId>library-c</artifactId>
        <version>2</version>
    <dependency>
  <dependencies>
<dependencyManagement>

And then in library-a, library-b you would declare the dependency on library-c as follows:

<dependencies>
  <dependency>
    <groupId>your.group.id</groupId>
    <artifactId>library-c</artifactId>
  <dependency>
<dependencies>

By declaring this dependency in the parent's dependencyManagement you are insisting on both of the child modules using the version declared in the parent.

  1. You want to protect yourself from unhappy dependency additions occurring in future

You can use the Maven Enforcer plugin here, specifically the dependencyConvergence rule. For example:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-enforcer-plugin</artifactId>
    <version>3.0.0-M1</version>
    <executions>
      <execution>
        <id>enforce</id>
        <configuration>
          <rules>
            <dependencyConvergence/>
          </rules>
        </configuration>
        <goals>
          <goal>enforce</goal>
        </goals>
      </execution>
    </executions>
</plugin>

The enforcer can be configured to either fail or warn if it discovers a non convergent dependency.

Related