





















































In this article by Hubert Klein Ikkink, author of the book Gradle Dependency Management, you are going to learn how to define dependencies in your Gradle project. We will see how we can define the configurations of dependencies. You will learn about the different dependency types in Gradle and how to use them when you configure your build.
When we develop software, we need to write code. Our code consists of packages with classes, and those can be dependent on the other classes and packages in our project. This is fine for one project, but we sometimes depend on classes in other projects we didn't develop ourselves, for example, we might want to use classes from an Apache Commons library or we might be working on a project that is part of a bigger, multi-project application and we are dependent on classes in these other projects.
Most of the time, when we write software, we want to use classes outside of our project. Actually, we have a dependency on those classes. Those dependent classes are mostly stored in archive files, such as Java Archive (JAR) files. Such archive files are identified by a unique version number, so we can have a dependency on the library with a specific version.
(For more resources related to this topic, see here.)
In Gradle, we define dependency configurations to group dependencies together. A dependency configuration has a name and several properties, such as a description and is actually a special type of FileCollection. Configurations can extend from each other, so we can build a hierarchy of configurations in our build files. Gradle plugins can also add new configurations to our project, for example, the Java plugin adds several new configurations, such as compile and testRuntime, to our project. The compile configuration is then used to define the dependencies that are needed to compile our source tree. The dependency configurations are defined with a configurations configuration block. Inside the block, we can define new configurations for our build. All configurations are added to the project's ConfigurationContainer object.
In the following example build file, we define two new configurations, where the traffic configuration extends from the vehicles configuration. This means that any dependency added to the vehicles configuration is also available in the traffic configuration. We can also assign a description property to our configuration to provide some more information about the configuration for documentation purposes. The following code shows this:
// Define new configurations for build.
configurations {
// Define configuration vehicles.
vehicles {
description = 'Contains vehicle dependencies'
}
traffic {
extendsFrom vehicles
description = 'Contains traffic dependencies'
}
}
To see which configurations are available in a project, we can execute the dependencies task. This task is available for each Gradle project. The task outputs all the configurations and dependencies of a project. Let's run this task for our current project and check the output:
$ gradle -q dependencies
------------------------------------------------------------
Root project
------------------------------------------------------------
traffic - Contains traffic dependencies
No dependencies
vehicles - Contains vehicle dependencies
No dependencies
Note that we can see our two configurations, traffic and vehicles, in the output. We have not defined any dependencies to these configurations, as shown in the output.
The Java plugin adds a couple of configurations to a project, which are used by the tasks from the Java plugin. Let's add the Java plugin to our Gradle build file:
apply plugin: 'java'
To see which configurations are added, we invoke the dependencies task and look at the output:
$ gradle -q dependencies
------------------------------------------------------------
Root project
------------------------------------------------------------
archives - Configuration for archive artifacts.
No dependencies
compile - Compile classpath for source set 'main'.
No dependencies
default - Configuration for default artifacts.
No dependencies
runtime - Runtime classpath for source set 'main'.
No dependencies
testCompile - Compile classpath for source set 'test'.
No dependencies
testRuntime - Runtime classpath for source set 'test'.
No dependencies
We see six configurations in our project just by adding the Java plugin. The archives configuration is used to group the artifacts our project creates. The other configurations are used to group the dependencies for our project. In the following table, the dependency configurations are summarized:
Name |
Extends |
Description |
compile |
none |
These are dependencies to compile. |
runtime |
compile |
These are runtime dependencies. |
testCompile |
compile |
These are extra dependencies to compile tests. |
testRuntime |
runtime, testCompile |
These are extra dependencies to run tests. |
default |
runtime |
These are dependencies used by this project and artifacts created by this project. |
We defined configurations or applied a plugin that added new configurations to our project. However, a configuration is empty unless we add dependencies to the configuration. To declare dependencies in our Gradle build file, we must add the dependencies configuration block. The configuration block will contain the definition of our dependencies. In the following example Gradle build file, we define the dependencies block:
// Dependencies configuration block.
dependencies {
// Here we define our dependencies.
}
Inside the configuration block, we use the name of a dependency configuration followed by the description of our dependencies. The name of the dependency configuration can be defined explicitly in the build file or can be added by a plugin we use. In Gradle, we can define several types of dependencies. In the following table, we will see the different types we can use:
Dependency type |
Description |
External module dependency |
This is a dependency on an external module or library that is probably stored in a repository. |
Client module dependency |
This is a dependency on an external module where the artifacts are stored in a repository, but the meta information about the module is in the build file. We can override meta information using this type of dependency. |
Project dependency |
This is a dependency on another Gradle project in the same build. |
File dependency |
This is a dependency on a collection of files on the local computer. |
Gradle API dependency |
This is a dependency on the Gradle API of the current Gradle version. We use this dependency when we develop Gradle plugins and tasks. |
Local Groovy dependency |
This is a dependency on the Groovy libraries used by the current Gradle version. We use this dependency when we develop Gradle plugins and tasks. |
External module dependencies are the most common dependencies in projects. These dependencies refer to a module in an external repository. Later in the article, we will find out more about repositories, but basically, a repository stores modules in a central location. A module contains one or more artifacts and meta information, such as references to the other modules it depends on.
We can use two notations to define an external module dependency in Gradle. We can use a string notation or a map notation. With the map notation, we can use all the properties available for a dependency. The string notation allows us to set a subset of the properties but with a very concise syntax.
In the following example Gradle build file, we define several dependencies using the string notation:
// Define dependencies.
dependencies {
// Defining two dependencies.
vehicles 'com.vehicles:car:1.0', 'com.vehicles:truck:2.0'
// Single dependency.
traffic 'com.traffic:pedestrian:1.0'
}
The string notation has the following format: moduleGroup:moduleName:version. Before the first colon, the module group name is used, followed by the module name, and the version is mentioned last.
If we use the map notation, we use the names of the attributes explicitly and set the value for each attribute. Let's rewrite our previous example build file and use the map notation:
// Compact definition of configurations.
configurations {
vehicles
traffic.extendsFrom vehicles
}
// Define dependencies.
dependencies {
// Defining two dependencies.
vehicles(
[group: 'com.vehicles', name: 'car', version: '1.0'],
[group: 'com.vehicles', name: 'truck', version: '2.0'],
)
// Single dependency.
traffic group: 'com.traffic', name: 'pedestrian', version:
'1.0'
}
We can specify extra configuration attributes with the map notation, or we can add an extra configuration closure. One of the attributes of an external module dependency is the transitiveattribute. In the next example build file, we will set this attribute using the map notation and a configuration closure:
dependencies {
// Use transitive attribute in map notation.
vehicles group: 'com.vehicles', name: 'car',
version: '1.0', transitive: false
// Combine map notation with configuration closure.
vehicles(group: 'com.vehicles', name: 'car', version: '1.0') {
transitive = true
}
// Combine string notation with configuration closure.
traffic('com.traffic:pedestrian:1.0') {
transitive = false
}
}
Once of the advantages of Gradle is that we can write Groovy code in our build file. This means that we can define methods and variables and use them in other parts of our Gradle file. This way, we can even apply refactoring to our build file and make maintainable build scripts. Note that in our examples, we included multiple dependencies with the com.vehicles group name. The value is defined twice, but we can also create a new variable with the group name and reference of the variable in the dependencies configuration. We define a variable in our build file inside an ext configuration block. We use the ext block in Gradle to add extra properties to an object, such as our project.
The following sample code defines an extra variable to hold the group name:
// Define project property with
// dependency group name 'com.vehicles'
ext {
groupNameVehicles = 'com.vehicles'
}
dependencies {
// Using Groovy string support with
// variable substition.
vehicles "$groupNameVehicles:car:1.0"
// Using map notation and reference
// property groupNameVehicles.
vehicles group: groupNameVehicles, name: 'truck', version:
'2.0'
}
If we define an external module dependency, then Gradle tries to find a module descriptor in a repository. If the module descriptor is available, it is parsed to see which artifacts need to be downloaded. Also, if the module descriptor contains information about the dependencies needed by the module, those dependencies are downloaded as well. Sometimes, a dependency has no descriptor in the repository, and it is only then that Gradle downloads the artifact for that dependency.
A dependency based on a Maven module only contains one artifact, so it is easy for Gradle to know which artifact to download. But for a Gradle or Ivy module, it is not so obvious, because a module can contain multiple artifacts. The module will have multiple configurations, each with different artifacts. Gradle will use the configuration with the name default for such modules. So, any artifacts and dependencies associated with the default configuration are downloaded. However, it is possible that the default configuration doesn't contain the artifacts we need. We, therefore, can specify the configuration attribute for the dependency configuration to specify a specific configuration that we need.
The following example defines a configuration attribute for the dependency configuration:
dependencies {
// Use the 'jar' configuration defined in the
// module descriptor for this dependency.
traffic group: 'com.traffic',
name: 'pedestrian',
version: '1.0',
configuration: 'jar'
}
When there is no module descriptor for a dependency, only the artifact is downloaded by Gradle. We can use an artifact-only notation if we only want to download the artifact for a module with a descriptor and not any dependencies. Or, if we want to download another archive file, such as a TAR file, with documentation, from a repository.
To use the artifact-only notation, we must add the file extension to the dependency definition. If we use the string notation, we must add the extension prefixed with an @ sign after the version. With the map notation, we can use the ext attribute to set the extension. If we define our dependency as artifact-only, Gradle will not check whether there is a module descriptor available for the dependency. In the next build file, we will see examples of the different artifact-only notations:
dependencies {
// Using the @ext notation to specify
// we only want the artifact for this
// dependency.
vehicles 'com.vehicles:car:2.0@jar'
// Use map notation with ext attribute
// to specify artifact only dependency.
traffic group: 'com.traffic', name: 'pedestrian',
version: '1.0', ext: 'jar'
// Alternatively we can use the configuration closure.
// We need to specify an artifact configuration closure
// as well to define the ext attribute.
vehicles('com.vehicles:car:2.0') {
artifact {
name = 'car-docs'
type = 'tar'
extension = 'tar'
}
}
}
A Maven module descriptor can use classifiers for the artifact. This is mostly used when a library with the same code is compiled for different Java versions, for example, a library is compiled for Java 5 and Java 6 with the jdk15 and jdk16 classifiers. We can use the classifier attribute when we define an external module dependency to specify which classifier we want to use. Also, we can use it in a string or map notation. With the string notation, we add an extra colon after the version attribute and specify the classifier. For the map notation, we can add the classifier attribute and specify the value we want. The following build file contains an example of the different definitions of a dependency with a classifier:
dependencies {
// Using string notation we can
// append the classifier after
// the version attribute, prefixed
// with a colon.
vehicles 'com.vehicles:car:2.0:jdk15'
// With the map notation we simply use the
// classifier attribute name and the value.
traffic group: 'com.traffic', name: 'pedestrian',
version: '1.0', classifier: 'jdk16'
// Alternatively we can use the configuration closure.
// We need to specify an artifact configuration closure
// as well to define the classifier attribute.
vehicles('com.vehicles:truck:2.0') {
artifact {
name = 'truck'
type = 'jar'
classifier = 'jdk15'
}
}
}
When we define external module dependencies, we expect that there is a module descriptor file with information about the artifacts and dependencies for those artifacts. Gradle will parse this file and determine what needs to be downloaded. Remember that if such a file is not available on the artifact, it will be downloaded. However, what if we want to override the module descriptor or provide one if it is not available? In the module descriptor that we provide, we can define the dependencies of the module ourselves.
We can do this in Gradle with client module dependencies. Instead of relying on a module descriptor in a repository, we define our own module descriptor locally in the build file. We now have full control over what we think the module should look like and which dependencies the module itself has. We use the module method to define a client module dependency for a dependency configuration.
In the following example build file, we will write a client module dependency for the dependency car, and we will add a transitive dependency to the driver:
dependencies {
// We use the module method to instruct
// Gradle to not look for the module descriptor
// in a repository, but use the one we have
// defined in the build file.
vehicles module('com.vehicles:car:2.0') {
// Car depends on driver.
dependency('com.traffic:driver:1.0')
}
}
Projects can be part of a bigger, multi-project build, and the projects can be dependent on each other, for example, one project can be made dependent on the generated artifact of another project, including the transitive dependencies of the other project. To define such a dependency, we use the project method in our dependencies configuration block. We specify the name of the project as an argument. We can also define the name of a dependency configuration of the other project we depend on. By default, Gradle will look for the default dependency configuration, but with the configuration attribute, we can specify a specific dependency configuration to be used.
The next example build file will define project dependencies on the car and truck projects:
dependencies {
// Use project method to define project
// dependency on car project.
vehicles project(':car')
// Define project dependency on truck
// and use dependency configuration api
// from that project.
vehicles project(':truck') {
configuration = 'api'
}
// We can use alternative syntax
// to specify a configuration.
traffic project(path: ':pedestrian',
configuration: 'lib')
}
In this article, you learned how to create and use dependency configurations to group together dependencies. We saw how to define several types of dependencies, such as external module dependency and internal dependencies.
Also, we saw how we can add dependencies to code in Gradle build scripts with the classpath configuration and the buildscript configuration.
Finally, we looked at some maintainable ways of defining dependencies using code refactoring and the external dependency management plugin.
Further resources on this subject: