Welcome back to the Mastering Maven series. In recent entries we've explored additional features provided by Maven related to dependency resolution: <optional> dependencies, <dependencyManagement>, exclusions. These features not only let you configure dependencies for your project but also fix common issues when the dependency tree does not have the appropriate contents. Some of these issues are easy to spot and fox, but there will be times when spotting issues is not that easy, considering the size of the dependency graph and the multiple configurations applied to it. In this post, we'll discuss the Maven Enforcer plugin in the context of dependency resolution.
The Maven Enforcer plugin is an additional element you may apply to a build to ensure certain conditions are met. These conditions may be evaluated at any time during the build depending on execution-to-lifecycle bindings. Given that we've yet to discuss the mechanism that allows plugins to bind their behavior to particular lifecycle phases we'll rely on explicit invocation of the Enforcer's enforce goal for the time being. The plugin accepts a set of rules to configure the conditions that must be evaluated. The plugin provides a sizable set of rules for you to choose from; there are also additional rules you may want to have a look at as well. For now, we'll stay with the core set of rules.
All snippets rely on the following dependencies:
Let's cover with the issues related to dependency resolution that can be spotted by the Enforcer plugin.
This is a particularly nasty problem as we've seen that direct dependencies are selected differently (last) from transitive dependencies (closest), likely resulting in the wrong dependency being chosen. As it turns out, Maven 3.6.3 (latest release at the time of writing) does not flag duplicate dependencies as errors but rather as warnings, allowing the build to continue. If you think you'd be in better shape by relying on the IDE to flag such problems as errors, think again. Of the 4 popular IDEs (Eclipse, Vs.Code, NetBeans, Intellij) only the last 2 will offer a visual hint in the editor when duplicate dependencies are found, however they will not fail the build. The following POM exemplifies the problem
Invoking mvn dependency:tree to resolve dependencies results in the following output
As you can appreciate Maven does output a warning and the build succeeds. Configuring the <banDuplicatePomDependencyVersions> enforcer rule will cause the build to fail
Invoking mvn enforcer:enforce triggers the rule
FIX: Remove duplicate definitions until the rule no longer breaks the build.
This rule checks that dependency versions converge, that is, for any given dependency whose coordinates are found more than once in the graph (matched by groupId and artifactId) then all version numbers for said dependency must be equal. This condition can cause potential problems with binary compatibility, when there may be multiple incompatible versions of the same dependency in the graph, the chose dependency may cause breakages if their dependents can't deal with it (i.e, ClassNotFoundException, MethodNotFoundException, and so on). The following snippet configures project1 and project2 as dependencies which in turn depend on different versions of the same org.apache.commons:commons-lang3 dependency
Resolving the dependency graph yields
We can verify that a different version of the org.apache.commons:commons-lang3 will be selected if we invert the definition order of project1 and project2
The <dependencyConvergence> rule shows that the build has a problem because the numbers do not converge
What's more, the same rule triggers if the dependency order is changed, because the order does not matter at all for this rule
FIX: Add a <dependencyManagement> block that defines the version numbers you desire in the build. Here for example the number is set to 3.10.
There may be times when you only wat to consume direct dependencies, in which case any transitive dependency found in the graph should raise an error. For our current settings, org.apache.commons:commons-lang3 is a transitive dependency from the consumer's POV when either project1 or project2 are included. The following POM demonstrates this problem
Resolving dependencies shows that we do have a transitive dependency in the graph
Enabling the <banTransitiveDependencies> rule should raise an error
FIX: Well, it depends. If the included dependencies should not expose their dependencies (thus making them transitive to consumers) then their POMs should be updated, likely marking those dependencies as <optional> and perhaps setting their scope to provided. This is easy if those dependencies are under your control, that is, you have access to the source and can change their metadata. On the other hand, if you can't change the metadata or the dependencies must remain and it's your consumer the one that requires no transitive dependencies then you must use exclusions, as shown next
Another possible scenario may be that a particular dependency creeps into the dependency graph but your team has decided that said dependency is not a good fit for a given reason, such as it may contain harmful code, or that it has classes with incompatible bytecode, or it may enable idioms that are against regulations. In any case, the dependency must be purged from the dependency graph. In our case let's assume that org.apache.commons:commons-lang3 has been deemed harmful and it should be banned from usage. Any project that has it as a dependency must fail the build.
Resolving the dependency graph does show that the problematic dependency is found.
The bannedDependencies rule can be configured with a list of dependencies that should be flagged as problematic. The configuration for this rule accepts different patterns that let you specify with great accuracy the conditions to be evaluated; for now, we simply specify the groupId and artifactId of the dependency we want to see excluded.
FIX: You'll have to add an exclusion at each dependency root whose dependency graph contains the banned dependency.
Last but not least, if your project follows semantic versioning then you'd like its dependencies to follow suit. You must be extra careful here as we've seen that Apache Maven does not apply semantic versioning rules when resolving versions. The order in which dependencies are defined and encountered will alter the result. The following POM shows project1 configured first and project2 second.
Which results in version 3.0 of org.apache.commons:commons-lang3 being chosen.
The build will fail (as expected) when the requireUpperBoundDeps is configured
However, if the dependency order is reversed so that project2 is listed first then the rule does not trigger as the chosen version for org.apache.commons:commons-lang3 is now 3.5.
FIX: Once again the answer lies in the <dependencyManagement> block as it will ensure a specific version regardless of the order in which dependencies are defined.
This is just the tip of the iceberg in terms of enforcer rules, there are other conditions you may want to explore such as JDK version, system/project properties, whether a given file should exist or not. Whatever the case is you can count on the Enforcer plugin to fail the build when any of those conditions is not met.
Image by Quang Le