Introduction
The dependency mechanism is Maven’s way of deciding the version of artifacts that will be used to build the application. Maven is a build and dependency management tool that helps in creating reproducible builds using build configuration files. Deciding the version of artifacts used in the build process is not a simple process because there is no limit to the number of levels a project’s dependency tree can grow.
A dependency tree is a way of representing dependency order and priority in the case of large projects with multiple modules. The position of dependencies in this tree and the rules of priority set by Maven decide the version of the artifact that will be used during the build process. This blog aims to demystify Maven’s dependency mechanism.
Understanding types of dependencies
A project can have two kinds of dependencies. A direct dependency or a transitive dependency.
Direct Dependency
A direct dependency is one that is referenced from the application as its first level dependency. It means there is a direct relationship between this dependency and the application. These dependencies are straightforward to fetch and do not add much to the complexity of Maven’s build process.
Transitive Dependency
A transitive dependency can be thought of as the dependency of a dependency. Since there is no limit to the levels of dependency supported by Maven, such dependencies add a lot of complexity to the build process. It is not uncommon to find large projects having numerous levels of transitive dependencies before the end of the tree is reached. The bulk of the dependency mechanism logic for Maven deals with transitive dependency. Many factors like dependency mediation, dependency management, and dependency scopes come into the picture in the case of transitive dependencies.
Dependency Scopes
Maven provides scopes to enable the developer to have fine control of defining the version of dependency artifacts. In simple terms, it defines the location where a dependency can be found and the time where it has to be used. For example, it defines whether the dependency is required during runtime, compile-time, or only for tests. It can also define whether the dependency needs to be imported from the parent, or will be provided by the run time. Let us look into Dependency scopes in detail now.
Compile
This is the default scope that is assumed if no specific scope is defined. It adds the dependencies to the classpaths of the project. These dependencies are also made available to the dependent projects.
Provided
This scope denotes that the dependency will be provided during runtime by the framework or the environment and is not required to be added specifically for runtime. The dependency belonging to this scope is generally not considered transitive. Such dependencies are added to the classpath only during compilation and test.
Runtime
This scope indicates that the dependency is required only during runtime and not during compile time or test. These dependencies are needed only during execution.
Test
This scope indicates that dependency is only required during tests and not for runtime or compile time.
System
This scope provides an opportunity for the developer to provide the artifact explicitly. The build process will not search for this artifact in the repository.
Import
This is meant to be used while importing the dependencies of another project as the dependencies of the application. It is supported only for the dependency type ‘pom’. It helps to easily import all the dependencies of another project by importing the pom configuration of that project.
Let us now try to understand how scope helps developers precisely control the version of the artifact that will be used during the build process.
Maven’s Dependency Mechanism
The dependency mechanism uses the concepts of dependency mediation and explicit dependency management to decide the version of the artifact to be used. Dependency mediation deals with deciding the correct version in case multiple versions of the same artifact occur in the dependency tree. The dependency management block helps in explicitly defining the version of dependency to be used and overrides the location of the dependency in the dependency tree. Together, they provide granular control of the build process to the developer.
Dependency Mediation
Maven’s method of dependency mediation is to select the nearest dependency when multiple versions of the same dependency are found. This means if you want to make the build process use a specific version of a transitive dependency, you can just declare it in your project POM file. This will ensure it comes as closest in the dependency tree.
Let us try to understand dependency mediation through an example. Let’s say there are four projects - Project1, Project2, Project3, and Project4.
Project2 is a dependency for Project1 and Project3 is a dependency for Project2. Project3 then depends on an artifact called logging-artifact with version 2.0.
Project4 is also a dependency for Project1 and it depends upon logging-artifact with version 1.0
Summarizing the dependency tree for Project1 will look as below.
Project1 -> Project2->Project3->logging-artifact-2.0
-> Project4 -> logging-artifcat-1.0
The dependency mediation logic of Maven will select logging-artifact-1.0 since that is the closest in the dependency tree. If the same dependencies with different versions are found at the same levels in the tree, the order in which they were defined in the pom.xml will determine the version that will be chosen. For example, if Project2 was directly using logging-artifact-2.0 instead of through Project3, then logging-artifact-2.0 and logging artifact-1.0 would have been at the same level. In that case, the project that was mentioned first in Project1’s pom.xml will take precedence.
Dependency Management
The dependency management block helps to explicitly specify versions of dependencies irrespective of their location in the dependency tree. In other words, the dependency management block overrides the dependency mediation rules. It also helps one to import all the dependencies of a project to another project. Let us explore all of these through various pom.xml configurations now.
Overriding the dependency mediation rules
To understand this, let us consider two projects. Project1 has a dependency on logging-artifact with version 2.0 and Project2 has dependency on logging artifact with version 2.2. The POM configuration for Project1 is as below.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.packagecloud</groupId>
<artifactId>project1</artifactId>
<version>0.0.0</version>
<dependencies>
<dependency>
<groupId>org.packagecloud.logging</groupId>
<artifactId>logging-artifact</artifactId>
<version>2.0</version>
</dependency>
</dependencies>
The POM configuration for Project2 is as below.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.packagecloud</groupId>
<artifactId>project1</artifactId>
<version>0.0.0</version>
<dependencies>
<dependency>
<groupId>org.packagecloud.logging</groupId>
<artifactId>logging-artifact</artifactId>
<version>2.2</version>
</dependency>
</dependencies>
Now, let us create a third project Project3 that declares both the above projects as dependencies. The POM configuration for Project3 will be as below.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.packagecloud</groupId>
<artifactId>project3</artifactId>
<version>0.0.0</version>
<dependencies>
<dependency>
<groupId>com.packagecloud</groupId>
<artifactId>project1</artifactId>
<version>0.0.0</version>
</dependency>
<dependency>
<groupId>com.packagecloud</groupId>
<artifactId>project2</artifactId>
<version>0.0.0</version>
</dependency>
</dependencies>
Applying the dependency mediation rules, we see that two versions of logging-artifact are at the same level in the dependency tree. So the order of declaration of the project matters and logging-artifact-2.0 will be considered by Maven for building since Project1 was declared first.
If you want to override this dependency mediation, all you need to do is to use the dependency management block. It will ensure the version of logging-artifact declared in that block will be considered irrespective of its location in the dependency tree.
The POM configuration for project3 will look as below.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.activewizards</groupId>
<artifactId>project3</artifactId>
<version>0.0.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.packagecloud</groupId>
<artifactId>logging-artifact</artifactId>
<version>2.2</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.packagecloud</groupId>
<artifactId>project1</artifactId>
<version>0.0.0</version>
</dependency>
<dependency>
<groupId>com.packagecloud</groupId>
<artifactId>project2</artifactId>
<version>0.0.0</version>
</dependency>
</dependencies>
Maven will now use logging-artifact version 2.2 for building the project.
Importing dependencies
The dependency management block is also used to import dependencies from another project. This feature is very useful in large projects since inheritance rules only allow for a single parent and large projects often need to consume dependencies from multiple projects. Let us say we are building a project called Project5 that uses Project1 and Project2 as dependencies and imports dependencies from Project3. It can be represented using the below POM configuration.
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>B</artifactId>
<packaging>pom</packaging>
<name>B</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.packagecloud</groupId>
<artifactId>Project3</artifactId>
<version>1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.packagecloud</groupId>
<artifactId>Project4</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.packagecloud</groupId>
<artifactId>Project1</artifactId>
<version>1.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.packagecloud</groupId>
<artifactId>Project2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
Note that in the dependency management block, Project3 is mentioned with scope ‘import’. You can also see another artifact called Project4 listed as a dependency. What happens if Project4 is already a dependency of Project3? In that case, the version listed in the dependency management block will be considered and all dependencies of Project3 except for Project4 will be imported. While trying such complex dependency imports, ensure that you don’t run into circular dependencies where a parent becomes a dependency of a child.
Conclusion
Maven’s dependency mechanism provides scopes, dependency mediation rules, and dependency management blocks to allow developers granular control over the versions of dependencies. Maven being a standard in build and deployment rules, it is important for the developers to have a complete grasp on the dependency mechanism to be able to create clean and reproducible builds. If your organization uses Maven as one of the dependency management tools along with other languages and frameworks, you should take a look at PackageCloud.- A cloud-based unified package distribution service.
Packagecloud is a cloud-based service for distributing different software packages in a unified, reliable, and scalable way, without owning any infrastructure. You can keep all of the packages that need to be distributed across your organization's machines in one repo, regardless of OS, or programming language. Then, you can efficiently distribute your packages to your devices in a secure way, without having to own any of the infrastructure involved in doing so.
This enables users to save time and money on setting up servers for hosting packages for each OS. Packagecloud allows users to set up and update machines faster and with less overhead than ever before.
Sign up for the packagecloud 14 day trial to get your machines set up and updated easily!