How to solve dependency conflicts with Maven

What are dependency conflicts?

Here is an example, this project has 2 dependencies, and both of them depend on the Guava library with different versions.

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- Project dependencies -->
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>study-maven-2</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>study-maven-3</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
<groupId>org.example</groupId>
<artifactId>study-maven-2</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
<groupId>org.example</groupId>
<artifactId>study-maven-3</artifactId>
<version>1.0-SNAPSHOT</version>

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

As you all know, we can’t have two classes with the same package and name in one project. So there would be conflicts if different versions of a package are imported. Obviously, in this case, only one version of Guava will be imported.

How to find dependency conflicts?

I’d like to introduce 2 useful tools to you. The first one is ‘maven-dependency-plugin’. It’s a Maven plugin, so you can use it directly. The command is listed below:

mvn dependency:tree -Dverbose -Dincludes=com.google.guava:guava

The ‘dependency’ is the plugin prefix, the ‘tree’ is the plugin goal.

1
2
3
4
5
6
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ study-maven ---
[INFO] org.example:study-maven:jar:1.0-SNAPSHOT
[INFO] +- org.example:study-maven-2:jar:1.0-SNAPSHOT:compile
[INFO] | \\- com.google.guava:guava:jar:30.1.1-jre:compile
[INFO] \\- org.example:study-maven-3:jar:1.0-SNAPSHOT:compile
[INFO] \\- (com.google.guava:guava:jar:29.0-jre:compile - omitted for conflict with 30.1.1-jre)

The result tells us that 2 dependencies imported the guava package, but only version 30 is available in this project. Version 29 is omitted.

Another tool is an IntelliJ IDEA plugin, called Maven Helper. It’s more convenient if you are using this development editor. You can see the conflicts directly in its panel.

image-20210924183132709.png

image-20210924190701716

How Maven chooses the version of dependency?

Actually, the rule is quite simple. You just need to remember 2 rules.

The first one is: nearest definition wins

“nearest definition” means that the version used will be the closest one to your project in the tree of dependencies.

1
2
3
4
5
6
7
POM
|-- A
| `-- B 1.0
|-- C
`-- D
`-- B 2.0

From this dependency tree, we can get 2 dependency paths: root-A-B1.0 and root-C-D-B2.0. You can see the first path is shorter than the second one, so according to this rule, version 1.0 will be chosen by Maven.

If two dependency versions are at the same depth, then you’ll need the second rule: first declaration wins. It means which dependency imported earlier will be chosen.

1
2
3
4
5
6
POM
|-- A
| `-- B 1.0
|-- C
`-- B 2.0

Version 1.0 is imported earlier, so it will be the final winner.

More details: Introduction to the Dependency Mechanism

Do you remember the example I mentioned before? The project imported 2 dependencies, study-maven-2 and study-maven-3. Both of these 2 libraries imported Guava directly.

Let’s have a look at the dependency tree of the project. You can see that version 30 is available, and another version of Guava is omitted. Because version 30 is imported firstly.

1
2
3
4
5
6
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ study-maven ---
[INFO] org.example:study-maven:jar:1.0-SNAPSHOT
[INFO] +- org.example:study-maven-2:jar:1.0-SNAPSHOT:compile
[INFO] | \- com.google.guava:guava:jar:30.1.1-jre:compile
[INFO] \- org.example:study-maven-3:jar:1.0-SNAPSHOT:compile
[INFO] \- (com.google.guava:guava:jar:29.0-jre:compile - omitted for conflict with 30.1.1-jre)

But there is an exceptional situation, it’s not a usual case; it happens when you importing the same library with a different version in one POM file. Then the latter one will win.

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.0.1-jre</version>
</dependency>
</dependencies>
1
2
3
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ study-maven ---
[INFO] org.example:study-maven:jar:1.0-SNAPSHOT
[INFO] \\- com.google.guava:guava:jar:27.0.1-jre:compile

How to specify the version we want?

3 solutions

Here I’ll give you 3 solutions.

The first way to achieve this goal is that excluding the version you don’t want when importing the dependency. You need to add some exclusion configurations with the dependency importing. You can do it manually or with the Maven Helper plugin.

image-20210924193152686

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>study-maven-2</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>study-maven-3</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

After you do that, you can check the dependency tree again.

1
2
3
4
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ study-maven ---
[INFO] org.example:study-maven:jar:1.0-SNAPSHOT
[INFO] \\- org.example:study-maven-3:jar:1.0-SNAPSHOT:compile
[INFO] \\- com.google.guava:guava:jar:29.0-jre:compile

The second way is importing the version you want directly in your project, according to the rule we mentioned before, the directly imported version will be the available one. Let’s check the dependency tree again, now version 29 is the winner.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>study-maven-2</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>study-maven-3</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
</dependencies>
1
2
3
4
5
6
7
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ study-maven ---
[INFO] org.example:study-maven:jar:1.0-SNAPSHOT
[INFO] +- org.example:study-maven-2:jar:1.0-SNAPSHOT:compile
[INFO] | \\- (com.google.guava:guava:jar:30.1.1-jre:compile - omitted for conflict with 29.0-jre)
[INFO] +- org.example:study-maven-3:jar:1.0-SNAPSHOT:compile
[INFO] | \\- (com.google.guava:guava:jar:29.0-jre:compile - omitted for conflict with 30.1.1-jre)
[INFO] \\- com.google.guava:guava:jar:29.0-jre:compile

The 3rd solution is using dependency management, you can assign a version directly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>study-maven-2</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>study-maven-3</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
</dependencies>
</dependencyManagement>
1
2
3
4
5
6
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ study-maven ---
[INFO] org.example:study-maven:jar:1.0-SNAPSHOT
[INFO] +- org.example:study-maven-2:jar:1.0-SNAPSHOT:compile
[INFO] | \\- com.google.guava:guava:jar:29.0-jre:compile (version managed from 30.1.1-jre)
[INFO] \\- org.example:study-maven-3:jar:1.0-SNAPSHOT:compile
[INFO] \\- (com.google.guava:guava:jar:29.0-jre:compile - version managed from 30.1.1-jre; omitted for duplicate)

You can see, now version 29 is the winner.

Please attention that the dependency management’s priority is lower than the directly imported one. It means the dependency management can not change the version you imported directly.

What if we have to keep two versions?

Some libraries only work with the old version of Guava, but some of them only work with the new version.

Let’s say study-maven-2 only works with Guava 30.1, but study-maven-3 can not work without Guava 29.
Here are the steps:

  1. Create a new project;
  2. Relocate all classes in package ‘com.google.common’ by maven-shade-plugin;
  3. Import the shaded one.

Here what we need is the ‘maven-shade-plugin’. This plugin can relocate some classes, by changing its package name.

Here is an example, we’d like to relocate all classes in package ‘com.google.common’ to another package.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<groupId>org.example</groupId>
<artifactId>shaded-study-maven-3</artifactId>
<version>1.1-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>study-maven-3</artifactId>
<version>1.1-SNAPSHOT</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>com.google.common</pattern>
<shadedPattern>org.example.shaded.com.google.common</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

What happened with relocation?

Your code will use the relocated classes after being compiled.

Before relocation, your code was importing the class of Guava from the original package; After relocation, your code is importing the same class from a new package.

Before:

1
2
3
4
5
6
7
8
9
package com.example;

import com.google.common.base.Preconditions;

public class Demo {
public static void main(String[] args) {
Preconditions.checkNotNull(args);
}
}

After:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example;

import org.example.shaded.com.google.common.base.Preconditions;

public class Demo {
public Demo() {
}

public static void main(String[] args) {
Preconditions.checkNotNull(args);
}
}

After you install or deploy the package, you can import the shaded library, there won’t be conflict.

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>study-maven-2</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>shaded-study-maven-3</artifactId>
<version>1.1-SNAPSHOT</version>
</dependency>
</dependencies>

What if your dependency conflict with the runtime library?

In a runtime environment, like Spark cluster is using guava 19.0, but you need to use 30.x version.

You can not shade the library provided by the runtime environment, but you can shade your package.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>com.google.common</pattern>
<shadedPattern>org.example.shaded.com.google.common</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

There is no best solution here, so you need to make your choice case by case.

Conclusion

Now you know more about the solution to deal with conflicts.

Let’s summarize briefly what we’ve looked at.

Firstly, we’ve talked about how to find dependency conflicts.

Secondly, we’ve got to know how Maven works with conflicts.

Lastly, we’ve discussed the solutions.