Dependency Management for Java
It is common practice to reuse software libraries to speed up development. However, it introduces a problem of dependency management in your project. I will break down some of our solutions for dependency management in Java. You will learn how to use Maven and Gradle for this task, and how to troubleshoot dependency conflicts along the way.
What are dependencies?
Let’s begin with a quick example. You plan to create a checkout page for a retail website. It must include the customer’s shipping details, billing details, shipping method and payment method, and give an option to submit the order. You will probably want to extend if further and support multiple payment methods, logging, security and so much more. This is a long and expensive project if you implement all this functionality from scratch. As an alternative, you can rely on existing software (dependencies) and reduce your development time and cost. Dependencies are quick solutions with convenient packaging. They are modules which provide specific functionality. For example, the Azure Storage Blobs package is a solution for managing your data on Azure cloud, which you can use in your project. You could rely on multiple such libraries, but there is a price to pay. You will need to manage them in your project.
How to manage dependencies?
Dependency management can be a difficult and time-consuming task. Dependencies may have many versions and conflict with each other. A package manager would help you resolve these issues. Maven and Gradle are common package managers for Java. They offer two primary ways for configuring dependencies: individual management and grouped management. Individual management adds dependencies one by one. Grouped management adds dependencies through a configuration file called “Bill of Materials” or BOM.
Individual dependencies allow for a project to add or remove them one at a time. And, for each dependency to use flexible versioning. But, this forces version management and handling conflicts.
Given a project using Azure Storage Blobs, Identity, and KeyVault Secrets. The following would be their dependency configuration.
<dependencies> <dependency> <groupId>com.azure</groupId> <artifactId>azure-storage-blob</artifactId> <version>12.7.0</version> </dependency> <dependency> <groupId>com.azure</groupId> <artifactId>azure-identity</artifactId> <version>1.0.7</version> </dependency> <dependency> <groupId>com.azure</groupId> <artifactId>azure-security-keyvault-secrets</artifactId> <version>4.1.4</version> </dependency> </dependencies>
implementation 'com.azure:azure-storage-blob:12.7.0' implementation 'com.azure:azure-identity:1.0.7' implementation 'com.azure:azure-security-keyvault-secrets:4.1.4'
Notice how each dependency includes its version. These dependencies are in a common group. So, they may share dependencies among each other. If the versions aren’t aligned they may have dependency conflicts. But, their major versions are different. So, choosing a version isn’t as simple as taking latest release.
This is where the flexibility of managing versions individually becomes complicated. For smaller projects it may be simple. But, complexity grow rapidly for each additional dependency.
Bill of materials
A BOM contains a set of dependencies that are available to a project. A project will add the dependencies it wants. Dependencies in the BOM that aren’t selected don’t get added to the project. Unlike individual management, a dependency doesn’t need to include a version. The BOM will determine the dependency’s version. Although, if required, a version could be set overriding the BOM’s version.
A BOM simplifies dependency management by bundling grouped dependencies. Validating dependency versions is no longer required. But, the BOM reduces flexibility as it’s expected to use its versions. Additionally, a BOM doesn’t prevent conflicts with dependencies that aren’t managed by it.
Azure SDK BOM
The Azure SDK for Java team now offers a BOM, azure-sdk-bom. It contains Azure SDKs whose versions share common dependencies. In addition to Azure SDKs, azure-sdk-bom includes dependencies used by the Azure SDKs. This simplifies adding Azure SDKs as version management is no longer required. Which is important as SDK versions aren’t indicative of being dependency compatible. The azure-sdk-bom will release on a periodic cadence. This cadence isn’t the same as the SDK release cadence.
Given the same project as above. The following would be their dependency configuration when using the azure-sdk-bom.
<dependencyManagement> <dependencies> <dependency> <groupId>com.azure</groupId> <artifactId>azure-sdk-bom</artifactId> <version>1.0.1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>com.azure</groupId> <artifactId>azure-storage-blob</artifactId> </dependency> <dependency> <groupId>com.azure</groupId> <artifactId>azure-identity</artifactId> </dependency> <dependency> <groupId>com.azure</groupId> <artifactId>azure-security-keyvault-secrets</artifactId> </dependency> </dependencies>
implementation platform('com.azure:azure-sdk-bom:1.0.1') implementation 'com.azure:azure-storage-blob' implementation 'com.azure:azure-identity' implementation 'com.azure:azure-security-keyvault-secrets'
Notice how each dependency no longer includes its version. These dependencies are now versioned by the BOM. So, the dependencies will share the same common dependencies with each other.
This is where the convenience of the BOM becomes restrictive. If one of the dependencies has a bug upgrading becomes more complicated. Upgrading the BOM will upgrade all the dependencies it offers. Upgrading the individual dependency may lead to a dependency conflict.
A common issue when using many dependencies is running into conflicts. Dependency conflicts could be trivial and won’t result in compile or runtime exception. But, some times they become show stoppers. So, knowing how to investigate and resolve them is useful.
Dependency resolution in Maven uses a breadth first search. Tie-breaking uses the left most dependency. This is the dependency listed first in the project. Maven offers tooling to help investigate and resolve dependency conflicts. I’ll discuss one of the more useful tools,
dependency:tree generates a graphical view of a project’s dependency. By default, the report generated by
dependency:tree will only include resolved dependencies. The flag
-Dverbose will include omitted dependencies. Including omitted dependencies shows where Maven resolved conflicts. The flag
-Dscopes will select the dependency tree scope. This could limit the report to runtime dependencies or to include everything.
For example, a project using
1.0.0. Running it runs into a class not found exception.
dependency:tree -Dverbose -Dscopes=compile will create a report of its runtime dependencies including omitted dependencies.
org.example:BlobDependencies:jar:1.0-SNAPSHOT +- com.azure:azure-storage-blob:jar:12.7.0:compile | +- com.azure:azure-core:jar:1.5.1:compile | \- com.azure:azure-storage-common:jar:12.7.0:compile | +- (com.azure:azure-core:jar:1.5.1:compile - omitted for duplicate) | \- (com.azure:azure-core-http-netty:jar:1.5.2:compile - omitted for conflict with 1.0.0) \- com.azure:azure-core-http-netty:jar:1.0.0:compile +- (com.azure:azure-core:jar:1.0.0:compile - omitted for conflict with 1.5.1)
The report shows that there are conflicts and the version resolved. It indicates that the
azure-core-http-netty version is much older than the
azure-storage-blob version. The Netty depedency is using an older version of
azure-core. Based on the dependency two solutions exist. Removing the Netty package as the Blob package includes it or upgrading to