Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

Multi-module Maven projects can have complex dependency graphs. These can have unusual results, the more the modules import from each other.

In this tutorial, we’ll see how to resolve version collision of artifacts in Maven.

We’ll start with a multi-module project where we’ve deliberately used different versions of the same artifact. Then, we’ll see how to prevent getting the wrong version of an artifact with either exclusion or dependency management.

Finally, we’ll try using the maven-enforcer-plugin to make things easier to control, by banning the use of transitive dependencies.

2. Version Collision of Artifacts

Each dependency that we include in our project might link to other artifacts. Maven can automatically bring in these artifacts, also called transitive dependencies. Version collision happens when multiple dependencies link to the same artifact, but use different versions.

As a result, there may be errors in our applications both in the compilation phase and also at runtime.

2.1. Project Structure

Let’s define a multi-module project structure to experiment with. Our project consists of a version-collision parent and three children modules:

version-collision
    project-a
    project-b
    project-collision

The pom.xml for project-a and project-b are almost identical. The only difference is the version of the com.google.guava artifact that they depend on. In particular, project-a uses version 22.0:

<dependencies>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>22.0</version>
    </dependency>
</dependencies>

But, project-b uses the newer version, 29.0-jre:

<dependencies>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>29.0-jre</version>
    </dependency>
</dependencies>

The third module, project-collision, depends on the other two:

<dependencies>
    <dependency>
        <groupId>com.baeldung</groupId>
        <artifactId>project-a</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.baeldung</groupId>
        <artifactId>project-b</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

So, which version of guava will be available to project-collision?

2.2. Using Features from Specific Dependency Version

We can find out which dependency is used by creating a simple test in the project-collision module that uses the Futures.immediateVoidFuture method from guava:

@Test
public void whenVersionCollisionDoesNotExist_thenShouldCompile() {
    assertThat(Futures.immediateVoidFuture(), notNullValue());
}

This method is only available from the 29.0-jre version. We’ve inherited this from one of the other modules, but we can only compile our code if we got the transitive dependency from project-b.

2.3. Compilation Error Caused by Version Collision

Depending on the order of dependencies in the project-collision module, in certain combinations Maven returns a compilation error:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:testCompile (default-testCompile) on project project-collision: Compilation failure
[ERROR] /tutorials/maven-all/version-collision/project-collision/src/test/java/com/baeldung/version/collision/VersionCollisionUnitTest.java:[12,27] cannot find symbol
[ERROR]   symbol:   method immediateVoidFuture()
[ERROR]   location: class com.google.common.util.concurrent.Futures

That’s the result of the version collision of the com.google.guava artifact. By default, for dependencies at the same level in a dependency tree, Maven chooses the first library it finds. In our case, both com.google.guava dependencies are at the same height and the older version is chosen.

2.4. Using maven-dependency-plugin

The maven-dependency-plugin is a very helpful tool to present all dependencies and their versions:

% mvn dependency:tree -Dverbose

[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ project-collision ---
[INFO] com.baeldung:project-collision:jar:0.0.1-SNAPSHOT
[INFO] +- com.baeldung:project-a:jar:0.0.1-SNAPSHOT:compile
[INFO] |  \- com.google.guava:guava:jar:22.0:compile
[INFO] \- com.baeldung:project-b:jar:0.0.1-SNAPSHOT:compile
[INFO]    \- (com.google.guava:guava:jar:29.0-jre:compile - omitted for conflict with 22.0)

The -Dverbose flag displays conflicting artifacts. In fact, we have a com.google.guava dependency in two versions: 22.0 and 29.0-jre. The latter is the one we would like to use in the project-collision module.

3. Excluding a Transitive Dependency From an Artifact

One way to resolve a version collision is by removing a conflicting transitive dependency from specific artifacts. In our example, we don’t want to have the com.google.guava library transitively added from the project-a artifact.

Therefore, we can exclude it in the project-collision pom:

<dependencies>
    <dependency>
        <groupId>com.baeldung</groupId>
        <artifactId>project-a</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <exclusions>
            <exclusion>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>com.baeldung</groupId>
        <artifactId>project-b</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

Now, when we run the dependency:tree command, we can see that it’s not there anymore:

% mvn dependency:tree -Dverbose

[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ project-collision ---
[INFO] com.baeldung:project-collision:jar:0.0.1-SNAPSHOT
[INFO] \- com.baeldung:project-b:jar:0.0.1-SNAPSHOT:compile
[INFO]    \- com.google.guava:guava:jar:29.0-jre:compile

As a result, the compilation phase ends without an error and we can use the classes and methods from version 29.0-jre.

4. Using the dependencyManagement Section

Maven’s dependencyManagement section is a mechanism for centralizing dependency information. One of its most useful features is to control versions of artifacts used as transitive dependencies.

With that in mind, let’s create a dependencyManagement configuration in our parent pom:

<dependencyManagement>
   <dependencies>
      <dependency>
         <groupId>com.google.guava</groupId>
         <artifactId>guava</artifactId>
         <version>29.0-jre</version>
      </dependency>
   </dependencies>
</dependencyManagement>

As a result, Maven will make sure to use version 29.0-jre of com.google.guava artifact in all child modules:

% mvn dependency:tree -Dverbose

[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ project-collision ---
[INFO] com.baeldung:project-collision:jar:0.0.1-SNAPSHOT
[INFO] +- com.baeldung:project-a:jar:0.0.1-SNAPSHOT:compile
[INFO] |  \- com.google.guava:guava:jar:29.0-jre:compile (version managed from 22.0)
[INFO] \- com.baeldung:project-b:jar:0.0.1-SNAPSHOT:compile
[INFO]    \- (com.google.guava:guava:jar:29.0-jre:compile - version managed from 22.0; omitted for duplicate)

5. Prevent Accidental Transitive Dependencies

The maven-enforcer-plugin provides many built-in rules that simplify the management of a multi-module project. One of them bans the use of classes and methods from transitive dependencies.

Explicit dependency declaration removes the possibility of version collision of artifacts. Let’s add the maven-enforcer-plugin with that rule to our parent pom:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-enforcer-plugin</artifactId>
    <version>3.0.0-M3</version>
    <executions>
        <execution>
            <id>enforce-banned-dependencies</id>
            <goals>
                <goal>enforce</goal>
            </goals>
            <configuration>
                <rules>
                    <banTransitiveDependencies/>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

As a consequence, we must now explicitly declare the com.google.guava artifact in our project-collision module if we want to use it ourselves. We must either specify the version to use, or set up dependencyManagement in the parent pom.xml. This makes our project more mistake proof, but requires us to be more explicit in our pom.xml files.

6. Dependency Convergence

The Apache Maven Enforcer plugin supports dependencyConvergence as a built-in rule. When we enable this rule, the enforcer plugin ensures that dependency version numbers must converge. Let’s explore this rule in detail.

6.1. Understanding the Default Behavior

Let’s start by adding the dependencyConvergence rule to maven-enforcer-plugin within the project’s pom.xml:

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

We must note that we have specified the rule using the <dependencyConvergence/> tag with no child tags. This triggers the default behavior of the dependencyConvergence rule while building the project.

Next, let’s comment out the dependencyManagement section in the pom.xml file and compile the project:

$ mvn compile
# build output is trimmed
...
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.055 s
[INFO] Finished at: 2023-04-11T18:39:41+05:30
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-enforcer-plugin:3.3.0:enforce (enforce) on project project-collision: 
[ERROR] Rule 0: org.apache.maven.enforcer.rules.dependency.DependencyConvergence failed with message:
[ERROR] Failed while enforcing releasability.
[ERROR] 
[ERROR] Dependency convergence error for com.google.guava:guava:jar:22.0 paths to dependency are:
[ERROR] +-com.baeldung:project-collision:jar:0.0.1-SNAPSHOT
[ERROR]   +-com.baeldung:version-collision-project-a:jar:0.0.1-SNAPSHOT:compile
[ERROR]     +-com.google.guava:guava:jar:22.0:compile
[ERROR] and
[ERROR] +-com.baeldung:project-collision:jar:0.0.1-SNAPSHOT
[ERROR]   +-com.baeldung:version-collision-project-b:jar:0.0.1-SNAPSHOT:compile
[ERROR]     +-com.google.guava:guava:jar:29.0-jre:compile
[ERROR] 
...

Unfortunately, since the dependency version number for the com.google.guava:guava artifact couldn’t converge, so the compilation step failed. That’s the expected default behavior of the dependencyConvergence rule, where the build fails for even a single dependency convergence error.

Furthermore, let’s remember that dependency version management through the dependencyManagement section is taken into account before execution of the dependencyConvergence rule. So, we need to comment it out in our project to study the dependencyConvergence rule independently.

6.2. Excluding Artifacts for Dependency Convergence

If we’re sure our project will run fine with any of the two dependency version numbers, we can exclude such an artifact from the dependencyConvergence rule.

Let’s go ahead and exclude the com.google.guava:guava artifact from the rule:

...
<dependencyConvergence>
    <excludes>
        <exclude>com.google.guava:guava</exclude>
    </excludes>
</dependencyConvergence>
...

Now, let’s try to compile the project:

$ mvn compile
# build output trimmed
...
[INFO] version-collision .................................. SUCCESS [  0.560 s]
[INFO] version-collision-project-a ........................ SUCCESS [  0.265 s]
[INFO] version-collision-project-b ........................ SUCCESS [  0.043 s]
[INFO] project-collision .................................. SUCCESS [  0.093 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
...

Great! It looks like we’ve got this case covered.

6.3. Including Artifacts for Dependency Convergence

By default, the dependencyConvergence rule will apply to all the artifacts. We can relax the rule enforcement to a specific list of artifacts by including them within the <includes></includes> section.

Let’s include the com.google.guava:guava artifact in the dependencyConvergence rule:

...
<dependencyConvergence>
    <includes>
        <include>com.google.guava:guava</include>
    </includes>
</dependencyConvergence>
...

Moving on, let’s compile the project and verify that the dependency convergence error causes the build to fail:

$ mvn compile
# build fails with the same error as earlier

7. Conclusion

In this article, we’ve seen how to resolve a version collision of artifacts in Maven.

First, we explored an example of a version collision in a multi-module project.

Then, we showed how to exclude transitive dependencies in the pom.xml. We looked at how to control dependencies versions with the dependencyManagement section in the parent pom.xml.

Finally, we tried the maven-enforcer-plugin to ban the use of transitive dependencies in order to force each module to take control of its own.

As always, the code shown in this article is available over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – Maven (eBook) (cat=Maven)
Comments are closed on this article!