Managing Transitive Dependencies

Resolution behavior for transitive dependencies can be customized to a high degree to meet enterprise requirements.

Managing versions of transitive dependencies with dependency constraints

Dependency constraints allow you to define the version or the version range of both dependencies declared in the build script and transitive dependencies. It is the preferred method to express constraints that should be applied to all dependencies of a configuration. When Gradle attempts to resolve a dependency to a module version, all dependency declarations with version, all transitive dependencies and all dependency constraints for that module are taken into consideration. The highest version that matches all conditions is selected. If no such version is found, Gradle fails with an error showing the conflicting declarations. If this happens you can adjust your dependencies or dependency constraints declarations, or make other adjustments to the transitive dependencies if needed. Similar to dependency declarations, dependency constraint declarations are scoped by configurations and can therefore be selectively defined for parts of a build. If a dependency constraint influenced the resolution result, any type of dependency resolve rules may still be applied afterwards.

Example: Define dependency constraints

build.gradle

dependencies {
    implementation 'org.apache.httpcomponents:httpclient'
    constraints {
        implementation('org.apache.httpcomponents:httpclient:4.5.3') {
            because 'previous versions have a bug impacting this application'
        }
        implementation('commons-codec:commons-codec:1.11') {
            because 'version 1.9 pulled from httpclient has bugs affecting this application'
        }
    }
}

In the example, all versions are omitted from the dependency declaration. Instead, the versions are defined in the constraints block. The version definition for commons-codec:1.11 is only taken into account if commons-codec is brought in as transitive dependency, since commons-codec is not defined as dependency in the project. Otherwise, the constraint has no effect.

Dependency constraints are not yet published, but that will be added in a future release. This means that their use currently only targets builds that do not publish artifacts to maven or ivy repositories.

Dependency constraints themselves can also be added transitively. If a modules’s metadata is defined in a .pom file that contains dependency entries with <optional>true</optional>, Gradle will create a dependency constraint for each of these so-called optional dependencies. This leads to a similar resolution behavior as provided by Maven: if the corresponding module is brought in by another, non-optional dependency declaration, then the constraint will apply when choosing the version for that module (e.g., if the optional dependency defines a higher version, that one is chosen).

Support for optional dependencies from pom files is active by default with Gradle 5.0+. For using it in Gradle 4.6+, you need to activate it by adding enableFeaturePreview('IMPROVED_POM_SUPPORT') in settings.gradle.

Excluding transitive module dependencies

Declared dependencies in a build script can pull in a lot of transitive dependencies. You might decide that you do not want a particular transitive dependency as part of the dependency graph for a good reason.

  • The dependency is undesired due to licensing constraints.

  • The dependency is not available in any of the declared repositories.

  • The metadata for the dependency exists but the artifact does not.

  • The metadata provides incorrect coordinates for a transitive dependency.

Transitive dependencies can be excluded on the level of a declared dependency or a configuration. Let’s demonstrate both use cases. In the following two examples the build script declares a dependency on Log4J, a popular logging framework in the Java world. The metadata of the particular version of Log4J also defines transitive dependencies.

Example: Unresolved artifacts for transitive dependencies

build.gradle

apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'log4j:log4j:1.2.15'
}

If resolved from Maven Central some of the transitive dependencies provide metadata but not the corresponding binary artifact. As a result any task requiring the binary files will fail e.g. a compilation task.

> gradle -q compileJava

* What went wrong:
Could not resolve all files for configuration ':compileClasspath'.
> Could not find jms.jar (javax.jms:jms:1.1).
  Searched in the following locations:
      https://repo.maven.apache.org/maven2/javax/jms/jms/1.1/jms-1.1.jar
> Could not find jmxtools.jar (com.sun.jdmk:jmxtools:1.2.1).
  Searched in the following locations:
      https://repo.maven.apache.org/maven2/com/sun/jdmk/jmxtools/1.2.1/jmxtools-1.2.1.jar
> Could not find jmxri.jar (com.sun.jmx:jmxri:1.2.1).
  Searched in the following locations:
      https://repo.maven.apache.org/maven2/com/sun/jmx/jmxri/1.2.1/jmxri-1.2.1.jar

The situation can be fixed by adding a repository containing those dependencies. In the given example project, the source code does not actually use any of Log4J’s functionality that require the JMS (e.g. JMSAppender) or JMX libraries. It’s safe to exclude them from the dependency declaration.

Exclusions need to spelled out as a key/value pair via the attributes group and/or module. For more information, refer to ModuleDependency.exclude(java.util.Map).

Example: Excluding transitive dependency for a particular dependency declaration

build.gradle

dependencies {
    implementation('log4j:log4j:1.2.15') {
        exclude group: 'javax.jms', module: 'jms'
        exclude group: 'com.sun.jdmk', module: 'jmxtools'
        exclude group: 'com.sun.jmx', module: 'jmxri'
    }
}

You may find that other dependencies will want to pull in the same transitive dependency that misses the artifacts. Alternatively, you can exclude the transitive dependencies for a particular configuration by calling the method Configuration.exclude(java.util.Map).

Example: Excluding transitive dependency for a particular configuration

build.gradle

configurations {
    implementation {
        exclude group: 'javax.jms', module: 'jms'
        exclude group: 'com.sun.jdmk', module: 'jmxtools'
        exclude group: 'com.sun.jmx', module: 'jmxri'
    }
}

dependencies {
    implementation 'log4j:log4j:1.2.15'
}

As a build script author you often times know that you want to exclude a dependency for all configurations available in the project. You can use the method DomainObjectCollection.all(org.gradle.api.Action) to define a global rule.

You might encounter other use cases that don’t quite fit the bill of an exclude rule. For example you want to automatically select a version for a dependency with a specific requested version or you want to select a different group for a requested dependency to react to a relocation. Those use cases are better solved by the ResolutionStrategy API. Some of these use cases are covered in Customizing Dependency Resolution Behavior.

Enforcing a particular dependency version

Gradle resolves any dependency version conflicts by selecting the latest version found in the dependency graph. Some projects might need to divert from the default behavior and enforce an earlier version of a dependency e.g. if the source code of the project depends on an older API of a dependency than some of the external libraries.

Enforcing a version of a dependency requires a conscious decision. Changing the version of a transitive dependency might lead to runtime errors if external libraries do not properly function without them. Consider upgrading your source code to use a newer version of the library as an alternative approach.

Let’s say a project uses the HttpClient library for performing HTTP calls. HttpClient pulls in Commons Codec as transitive dependency with version 1.10. However, the production source code of the project requires an API from Commons Codec 1.9 which is not available in 1.10 anymore. A dependency version can be enforced by declaring it in the build script and setting ExternalDependency.setForce(boolean) to true.

Example: Enforcing a dependency version

build.gradle

dependencies {
    implementation 'org.apache.httpcomponents:httpclient:4.5.4'
    implementation('commons-codec:commons-codec:1.9') {
        force = true
    }
}

If the project requires a specific version of a dependency on a configuration-level then it can be achieved by calling the method ResolutionStrategy.force(java.lang.Object[]).

Example: Enforcing a dependency version on the configuration-level

build.gradle

configurations {
    compileClasspath {
        resolutionStrategy.force 'commons-codec:commons-codec:1.9'
    }
}

dependencies {
    implementation 'org.apache.httpcomponents:httpclient:4.5.4'
}

Disabling resolution of transitive dependencies

By default Gradle resolves all transitive dependencies specified by the dependency metadata. Sometimes this behavior may not be desirable e.g. if the metadata is incorrect or defines a large graph of transitive dependencies. You can tell Gradle to disable transitive dependency management for a dependency by setting ModuleDependency.setTransitive(boolean) to true. As a result only the main artifact will be resolved for the declared dependency.

Example: Disabling transitive dependency resolution for a declared dependency

build.gradle

dependencies {
    implementation('com.google.guava:guava:23.0') {
        transitive = false
    }
}

Disabling transitive dependency resolution will likely require you to declare the necessary runtime dependencies in your build script which otherwise would have been resolved automatically. Not doing so might lead to runtime classpath issues.

A project can decide to disable transitive dependency resolution completely. You either don’t want to rely on the metadata published to the consumed repositories or you want to gain full control over the dependencies in your graph. For more information, see Configuration.setTransitive(boolean).

Example: Disabling transitive dependency resolution on the configuration-level

build.gradle

configurations.all {
    transitive = false
}

dependencies {
    implementation 'com.google.guava:guava:23.0'
}

Importing version recommendations from a Maven BOM

Gradle provides support for importing bill of materials (BOM) files, which are effectively .pom files that use <dependencyManagement> to control the dependency versions of direct and transitive dependencies. The BOM support in Gradle works similar to using <scope>import</scope> when depending on a BOM in Maven. In Gradle however, it is done via a regular dependency declaration on the BOM:

Example: Depending on a BOM to import its dependency constraints

build.gradle

dependencies {
    // import a BOM
    implementation 'org.springframework.boot:spring-boot-dependencies:1.5.8.RELEASE'

    // define dependencies without versions
    implementation 'com.google.code.gson:gson'
    implementation 'dom4j:dom4j'
}

In the example, the versions of gson and dom4j are provided by the Spring Boot BOM. This way, if you are developing for a platform like Spring Boot, you do not have to declare any versions yourself but can rely on the versions the platform provides.

Gradle treats all entries in the <dependencyManagement> block of a BOM similar to Gradle’s dependency constraints. This means that any version defined in the <dependencyManagement> block can impact the dependency resolution result. In order to qualify as a BOM, a .pom file needs to have <packaging>pom</packaging> set.

Importing dependency constraints from Maven BOMs is active by default with Gradle 5.0+. For using it in Gradle 4.6+, you need to activate it by adding enableFeaturePreview('IMPROVED_POM_SUPPORT') in settings.gradle.