Resolving Dependency Conflicts and Using Version Ranges
With the introduction of transitive dependencies in Maven 2.0, it became possible to simplify a POM by including only the dependencies you need directly, and allowing Maven to calculate the full dependency graph. However, as the graph grows, it is inevitable that two or more artifacts will require different versions of a particular dependency. In this case, Maven must choose which version to provide.
In Maven, the version selected is the one declared “nearest” to the top of the tree – that is, Maven selects the version that requires the least number of dependencies to be traversed. A dependency in the POM being built will be used over anything else. However, this has limitations:
- The version chosen may not have all the features required by the other dependencies.
- If multiple versions are selected at the same depth, then the result is undefined.
While further dependency management features are scheduled for the next release of Maven at the time of writing, there are ways to manually resolve these conflicts as the end user of a dependency, and more importantly ways to avoid it as the author of a reusable library.
To manually resolve conflicts, you can remove the incorrect version from the tree, or you can override both with the correct version. Removing the incorrect version requires identifying the source of the incorrect version by running Maven with the
-X flag (for more information on how to do this, see section 6.9 in Chapter 6). For example, if you run
mvn -X test on the
proficio-core module, the output will contain something similar to:
proficio-core:1.0-SNAPSHOT junit:3.8.1 (selected for test) plexus-container-default:1.0-alpha-9 (selected for compile) plexus-utils:1.0.4 (selected for compile) classworlds:1.1-alpha-2 (selected for compile) junit:3.8.1 (not setting scope to compile; local scope test wins) proficio-api:1.0-SNAPSHOT (selected for compile) proficio-model:1.0-SNAPSHOT (selected for compile) plexus-utils:1.1 (selected for compile)
It should be noted that running
mvn -X test depends on other parts of the build having been executed beforehand, so it is useful to run
mvn install at the top level of the project (in the proficio directory)to ensure that needed components are installed into the local repository.
Once the path to the version has been identified, you can exclude the dependency from the graph by adding an exclusion to the dependency that introduced it. In this example,
plexus-utils occurs twice, and Proficio requires version 1.1 be used. To ensure this, modify the plexus-container-default dependency in the
proficio-core/pom.xml file as follows:
<dependency> <groupId>org.codehaus.plexus</groupId> <artifactId>plexus-container-default</artifactId> <version>1.0-alpha-9</version> <exclusions> <exclusion> <groupId>org.codehaus.plexus</groupId> <artifactId>plexus-utils</artifactId> </exclusion> </exclusions> </dependency>
This ensures that Maven ignores the 1.0.4 version of
plexus-utils in the dependency graph, so that the 1.1 version is used instead.
The alternate way to ensure that a particular version of a dependency is used, is to include it directly in the POM, as follows:
<dependencies> <dependency> <groupId>org.codehaus.plexus</groupId> <artifactId>plexus-utils</artifactId> <version>1.1</version> <scope>runtime</scope> </dependency> </dependencies>
However, this approach is not recommended unless you are producing an artifact that is bundling its dependencies and is not used as a dependency itself (for example, a WAR file). The reason for this is that it distorts the true dependency graph, which will accumulate if this project is reused as a dependency itself.
You’ll notice that the runtime scope is used here. This is because, in this situation, the dependency is used only for packaging, not for compilation. In fact, if the dependency were required for compilation, for stability it would always be declared in the current POM as a dependency – regardless of whether another dependency introduces it.
Neither of these solutions is ideal, but it is possible to improve the quality of your own dependencies to reduce the risk of these issues occurring with your own build artifacts. This is extremely important if you are publishing a build, for a library or framework, that will be used widely by others. To accomplish this, use version ranges instead.
When a version is declared as
1.1, as shown above for
plexus-utils, this indicates that the preferred version of the dependency
1.1, but that other versions may be acceptable. Maven has no knowledge regarding which versions will work, so in the case of a conflict with another dependency, Maven assumes that all versions are valid and uses the “nearest dependency” technique described previously to determine which version to use.
However, you may require a feature that was introduced in
plexus-utils version 1.1. In this case, the dependency should be specified as follows:
<dependency> <groupId>org.codehaus.plexus</groupId> <artifactId>plexus-utils</artifactId> <version>[1.1,)</version> </dependency>
What this means is that, while the nearest dependency technique will still be used in the case of a conflict, the version that is used must fit the range given. If the nearest version does not match, then the next nearest will be tested, and so on. Finally, if none of them match, or there were no conflicts originally, the version you are left with is
[1.1,). This means that the latest version, which is greater than or equal to 1.1, will be retrieved from the repository.
The notation used above is set notation, and the table below shows some of the values that can be used.
Table 3-2: Examples of Version Ranges
|(,1.0]||Less than or equal to 1.0|
|[1.2,1.3]||Between 1.2 and 1.3 (inclusive)|
|[1.0,2.0)||Greater than or equal to 1.0, but less than 2.0|
|[1.5,)||Greater than or equal to 1.5|
|(,1.1),(1.1,)||Any version, except 1.1|
By being more specific through the use of version ranges, it is possible to make the dependency mechanism more reliable for your builds and to reduce the number of exception cases that will be required. However, you need to avoid being overly specific as well. For instance, if two version ranges in a dependency graph do not intersect at all, the build will fail.
To understand how version ranges work, it is necessary to understand how versions are compared. In figure 3-3, you can see how a version is partitioned by Maven.
Figure 3-3: Version parsing
As you can see, a version is broken down into five parts: the major, minor and bug fix releases, then the qualifier and finally a build number. In the current version scheme, the snapshot (as shown above) is a special case where the qualifier and build number are both allowed. In a regular version, you can provide only the qualifier, or only the build number. It is intended that the qualifier indicates a version prior to release (for example,
alpha-1, beta-1, rc1). For a qualifier to be a snapshot the qualifier must be the text “snapshot” or a time stamp. The time stamp in figure 3-3 was generated on 11-02-2006 at 13:11:41. The build number is an increment after release to indicate patched builds.
With regard to ordering, the elements are considered in sequence to determine which is newer – first by major version, second – if the major versions were equal – by minor version, third by bug fix version, fourth by qualifier (using string comparison), and finally, by build number. A version that contains a qualifier is older than a version without a qualifier; for example, 1.2-beta is older than version 1.2. A version that also contains a build number is considered newer than a version without a build number; for example, 1.2-beta-1 is newer than 1.2-beta. In some cases, the versions will not match this syntax. In those cases, the two versions are compared entirely as strings. Please see the figure below for more examples of the ordering of version parsing schemes.
Figure 3-4: Version Parsing
All of these elements are considered part of the version and as such the ranges do not differentiate. If you use the range [1.1,), and the versions 1.1 and 1.2-beta-1 exist in a referenced repository, then 1.2-beta-1 will be selected. Often this is not desired, so to avoid such a situation, you must structure your releases accordingly, either avoiding the naming convention that would result in that behavior, or through the use of a separate repository containing only the artifacts and versions you strictly desire.
Whether you use snapshots until the final release, or release betas as milestones along the way, you should deploy them to a snapshot repository as is discussed in Chapter 7 of this book. This will ensure that the beta versions are used in a range only if the project has declared the snapshot (or development) repository explicitly.
A final note relates to how version updates are determined when a range is in use. This mechanism is identical to that of the snapshots that you learned in section 3.6. By default, the repository is checked once a day for updates to the versions of artifacts in use. However, this can be configured per-repository to be on a more regular interval, or forced from the command line using the
-U option for a particular Maven execution.
If it will be configured for a particular repository, the
updatePolicy value (which is in minutes) is changed for releases. For example:
<repository> [...] <releases> <updatePolicy>interval:60</updatePolicy> </releases> </repository>