Welcome back to the Mastering Maven series. We'll continue exploring the options offered by Apache Maven when it comes to dependency resolution. Dependency resolution is comprised by several concepts explained at Introduction to the Dependency Mechanism. So far we've covered dependency mediation, scopes, and optional dependencies. Now is the turn for dependency management.
The <dependencyManagement> block has a couple of uses, the first is to help resolving versions of transitive dependencies, no matter where they may be located in the dependency tree. What are transitive dependencies? Up to this point we've discussed direct dependencies, those that can be found declared in your pom.xml inside the <dependencies> block. However, it may be the case that any of those dependencies may have dependencies of their own. If any of those dependencies are found in the compile or runtime scopes then they will automatically be added to your project's dependency tree, becoming transitive dependencies. This ability lets you define as little information as possible in a POM file and yet still have all required dependencies available when compiling or running the project. However, resolving transitive dependencies is not without a few problems.
When there's only a single GAV definition for a particular transitive dependency then that artifact will be resolved and added to the dependency tree. But if there happens to be more than one where the version (V) is different then a rule kicks in: Maven resolves transitive dependencies depending on their location in the dependency tree and grabs the first match, as of Maven 3.x it does not look at the version at all! Yes, even if dependencies follow semantic versioning you may find that an older version may get resolved instead of the latest, just because the older is closer in the tree than the newer. Here's an example
Suppose you have 3 projects in the following configuration
project1 has a direct dependency on org.apache.commons:commons-lang3:3.0 while project2 also declares a dependency on org.apache.commons:commons-lang3 but using version 3.5. Finally, project3 declares project2 as dependency. Their POM files are defined as follows:
Let's consume these projects and see what happens. On the first try we'll consume project1 and project2, in that order. This means we'll have 2 different versions of org.apache.commons:commons-lang3 as a transitive dependency, with version 3.0 being first in the tree. The POM file for this project is thus:
Resolving the dependency tree of this project yields the following result
As described earlier, Maven looks at the position in the dependency tree and takes the first match, which results in version 3.0 being the chosen one. Let's see what happens when we invert the definition of the direct dependencies, by putting project2 first and project1 second. This is done with the following POM:
Now resolving the dependency tree shows a different result
The chosen version is now 3.5 which matches the one provided by project2. This clears it up then, the order in which direct dependency definitions are found in the consuming POM matters when resolving their transitive dependencies. Let's turn this up a notch by changing the shape of the tree when org.apache.commons:commons-lang3 is now at different depths in the dependency tree. The consumer3 project declares direct dependencies on project1 and project3 which depend on org.apache.commons:commons-lang3 with 1 and 2 hops each. The POM for this project looks like so:
Based on the first consumer example you may be thinking that the chosen version will be 3.0 as project1 is found in the before project3 is, and you would be correct, as witnessed by the following output
However, inverting the order of these two dependencies as shown by consumer4's POM results in version 3.0 being chosen again.
Thus proving that order and location matters. How can we ensure that a specific version is chosen? This is where the <dependencyManagement> block comes in. It functions like a lookup table where the GA is the key and the V is the value. Maven checks this block before dependency mediation occurs; if there is a GA match for a given transitive dependency, regardless of its order and location, then the associated V will be chosen. We can verify this fact by adding a <dependencyManagement> block to the next consumer project, and just to make sure the resolved version is the one we specified this block uses version 3.10, as shown next
The consumer5 project requires 1 hop to reach org.apache.commons:commons-lang3. Based on the first consumer the chosen version would be 3.0. Now that we have the <dependencyManagement> block in place the chosen version will be 3.10, here's the proof:
Now, if the order of dependencies were to be inverted, like it's shown by the second consumer showed where the chosen version is 3.5, we would see that resolving the tree still results in version 3.10 being the chosen one. What happens then if we set org.apache.commons:commons-lang3 at different depths in the tree? consumer6 shows this particular setup
Again, just because we defined a <dependencyManagement> block Maven will consult with the lookup table, find a match for org.apache.commons:commons-lang3's GA and pick 3.10 as the chosen version, no matter where org.apache.commons:commons-lang3 is found the tree
If you happen to invert the order of the direct dependencies you would still get version 3.10 which means the <dependencyManagement> block continues to perform its role as provider of versions given GA matches. As it turns out this block will also match the classifier (if given). You may also supply a <scope> element that will be used when a match is found however it's best to leave it out and use the <scope> explicitly where needed.
If you're like me you may be left wondering if there's a better way to find if there's a problem in a dependency tree other than trial and error of reordering dependency definitions, well yes, there is! The maven-enforcer-plugin a set of rules that can tell you a few things about your project. We'll cover this plugin in more detail in another entry of the Mastering Maven series but if you can't wait til that time I'd suggest you to have a look at the enforcer plugin and the following rules banDuplicatePomDependencyVersions, bannedDependencies, dependencyConvergence, requireUpperBoundDeps.
Source code available at 07-dependency-management.
Photo by Orlando