The more libraries you pull in, the more likely you are to hit conflicting dependencies. Here are a couple of ways to resolve them when using Maven.
Maven dependency conflicts can be really hard to solve. The purpose of this post is for readers to better understand what a version conflict is and why it is better to avoid them. I will start with a short story that most readers can probably relate to.
Firstly, imagine you have started working on a large and interesting project that uses a lot of different technology libraries that make your life easier as an engineer. As an additional benefit, Maven manages the library downloads, together with the library version of choice; so they can be easily updated. Development on the project continues, until one day you encounter a library that reduces the complexity of authentication development significantly; you then decide to include it in your project.
But suddenly, you get an error in the line 2223 of your code, which is a line different to the one you were working on. The first thing that pops into your mind is, “How strange! That line of code did not fail before; it must be an error in my code; I must have created/configured something incorrectly”. However, you realize that the code has no issues whatsoever. A more thorough debugging process ‘may’ reveal that there is a NullPointerException or NoSuchMethodException in a library class that you had no idea existed. The immediate reaction would be to update a library dependency to a newer version; but that does not solve the problem. The next thing you do is look at the code of the library class itself; discovering that the method/class does not exist at all; even after updating the new library added to your project.
Furthermore, things become even weirder when you decide to visit the local Maven repository .m2, and you discover that there are two different versions of the same library. Your project is using the older version, and you have no idea why…
Congratulations! You have just made and discovered a version conflict of a library that crashed your project.
Note: You made it when you added the authentication library in the story.
Why Did It Happen?
Since dependencies can be linked to other dependencies with different versions, that can produce conflicts. Based on this, we can draw a dependency tree scheme for our project X to explain this:
From the tree scheme above, it is clear that our project X will use all the libraries (Y, Z, and G) even if we didn’t specify them explicitly in the POM.
Actually, in this case, library Z will be imported into your project as a Maven dependency library, even if you had no idea that the library exists. Such dependencies are better known as transitive dependencies.
Since Y and G depend on different versions of Z, we have created a library conflict version. The project can use just one version of the Z library at runtime (1.0 or 2.0); but not both. If we use an incompatible version of a library with another library; the project could end up producing errors and crashing. Let us assume that library Z is the culprit behind the error in our project; and we would like to know which version of Z is creating the error.
Which Z Version Are We Using?
Our project X uses a Maven default mechanism called the dependency mechanism to solve the dependencies and to know which library to use.
Let’s see how the dependency mechanism works.
First and foremost, the library’s version whose node is nearest to the root (project X) in the dependency tree will be used. However, what happens if there are several versions of the same library — whose nodes are on the same level in the tree? In such a case, the first library version found is used. This means that the choice of library versions depends on the dependency order inside the POM file, where those dependencies declared first will be chosen first.
How to Solve the Conflict
There are two ways the above conflict can be resolved. The first and easiest solution is to import library G before library Y inside X’s POM file; as I explained above. However, a neater solution would be to import the last version of Z (2.0) as a direct dependency of X; inside X’s POM file. With some luck, the latter solution would work if library Z supports backward compatibility (as library Y uses v1.0 of Z). Tests are needed to increase reliability in this case
Important note: If there are version conflicts, it doesn’t always mean that your project will crash, usually there will be version conflicts when you use a lot of libraries, but we must take care that the library version used doesn’t crash our project.
How Can I Discover Conflicts Faster?
The problem presented in the above story is quite trivial and we were lucky to resolve it quickly. However, sometimes, it is not an easy task to find the dependency conflict; even if you already know that the error is not originating from your code logic.
Wouldn’t it be nice to have a tool that tests which libraries are conflicting in a project? You’re in luck! Enforcer – the loving iron fist of Maven – does just that. Enforcer can help developers solve dependency conflicts in Maven by analyzing all libraries declared in the POM file.
The plugin uses a lot of different rules, but we are only interested in one:
- dependencyConvergence – ensures all dependencies converge to the same version.
Let’s configure the plugin to use that rule by writing it in our pom.xml:
<plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> <version>1.4.1</version> <configuration> <rules><dependencyConvergence/></rules> </configuration> </plugin> </plugins>
From the above configuration, you can execute the goal enforcer:enforce via the command line of your project (mvn enforcer:enforce); or by binding the goal to a Maven stage. This is extremely useful to find any dependency conflicts that might be crashing your project. Once executed, the plugin returns a list tree showing all conflicts (if any) inside the project:
Dependency convergence error for log4j:log4j:1.2.17 paths to dependency are: +-com.ricston.conflict:conflict-info:2.1.3-SNAPSHOT +-org.slf4j:slf4j-log4j12:1.7.6 +-log4j:log4j:1.2.17 and +-com.ricston.conflict:conflict-info:2.1.3-SNAPSHOT +-log4j:log4j:1.2.1
Enforcer writes a dependency tree, as you can see above, where the root is our project “conflict-info”.
In this case, the library log4j is having version conflicts. We have two log4j library versions (1.2.17 and 1.2.16).
Version 1.2.16 of log4j is depended on by the project “conflict-info”; whilst version 1.2.17 of log4j is depended on by “slf4j-log4j12”. During compilation and runtime, log4j version 1.2.16 will be used, since its node is nearest to the root in our project dependency tree. Since only one library version can be used during runtime, the same version will be used for “slf4j-log4j12”.
Please note that not all dependency version conflicts will crash your project. Maven’s dependency mechanism is responsible for choosing the library version, as illustrated in the previous sections.
Finally, to have a better chance of avoiding dependency version conflicts; extract the dependency (and the preferred version) as a direct child of the project. This ensures that the chosen version is used in all child dependencies of that library; provided that the chosen version does not crash the project.
This conclusion provides further information about version conflicts, and how to overcome them in your project.
There are many other ways to resolve such conflicts; there are even books with hundreds of pages illustrating solutions using different Maven configurations as well. This article depicts only one method of resolving them.
Check these links for more information:
Also take a look at this awesome book. It includes how to detect and fix conflicts in different ways: Apache Maven Dependency Management by Jonathan Lalou.