Some combinations of transitive dependencies in a project can cause issues, but fortunately Gradle has several ways to exclude those unwanted dependencies. In this article you’ll learn why you’d want to exclude dependencies in the first place, as well as how to use each of Gradle’s exclude options.
Reasons to exclude dependencies
When you declare a dependency in your build script, Gradle automatically pulls any transitive dependencies (dependencies of that dependency) into your project. In Java projects these dependencies make their way onto the compile or runtime classpaths.
Let’s explore some scenarios where certain combinations of dependencies can cause an issue in your project.
1. Multiple SLF4J bindings
The SLF4J logging library requires that only one binding appears on the classpath, otherwise it doesn’t know which implementation to use for logging. For example, if you included both the Logback and Log4J bindings you’d get this error.
SLF4J: Class path contains multiple SLF4J bindings.
This means one of those bindings needs to be removed from the classpath, which you’ll see shortly how to do using exclude rules.
2. Unused transitive dependency
Sometimes we only need to use a very small part of a dependency artifact. One or more of its transitive dependencies may not be needed at compile or runtime.
For example, Google’s popular Guava utility library pulls in several transitive dependencies, such as com.google.code.findbugs:jsr305. This artifact, which we’ll refer to as findbugs, contains annotations that might not be needed at runtime. If we’re only using one method from Guava that we’re confident doesn’t use findbugs, then findbugs is a potential candidate for exclusion.
Of course if we’re taking such an approach we need to be confident that the excluded library won’t be required now or at any time in the future. This can be validated with automated tests to exercise that area of code.
The benefits of excluding unused transitive dependencies include:
-
cleaner compile classpath improves performance
-
smaller application deployable due to less artifacts on the runtime classpath
Now you know what problems you might see with transitive dependencies, let’s explore how Gradle solves them. All the following code samples are in Groovy, although each approach is also given in Kotlin where stated.
Option 1) per-dependency exclude rules
When you specify a dependency in your build script, you can provide an exclude rule at the same time telling Gradle not to pull in the specified transitive dependency.
For example, say we have a Gradle project that depends on Google’s Guava library, or more specifically com.google.guava:guava:30.1.1-jre
.
Here’s how the dependencies look in the build script.
dependencies {
implementation 'com.google.guava:guava:30.1.1-jre'
}
Inspecting the compile and runtime classpaths shows us they’re identical with the following transitive dependencies.
\--- com.google.guava:guava:30.1.1-jre
+--- com.google.guava:failureaccess:1.0.1
+--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
+--- com.google.code.findbugs:jsr305:3.0.2
+--- org.checkerframework:checker-qual:3.8.0
+--- com.google.errorprone:error_prone_annotations:2.5.1
\--- com.google.j2objc:j2objc-annotations:1.3
Guava’s pulling in a lot of extra stuff! You’ll see later how to generate such a dependency graph yourself.
Say we just wanted to use a tiny subset of Guava, like the endlessly helpful ImmutableMap.of(K k1, V v1)
method. If we wanted to we could exclude, for example, the findbugs dependency. Nothing against findbugs, but it seems particularly unnecessary!
Here’s how the exclude syntax looks in the Groovy build.gradle.
dependencies {
implementation('com.google.guava:guava:30.1.1-jre') {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
}
}
And in the Kotlin build.gradle.kts.
dependencies {
implementation("com.google.guava:guava:30.1.1-jre") {
exclude(group = "com.google.code.findbugs", module = "jsr305")
}
}
Within the closure we call exclude
, passing:
-
group
the group of the artifact we want to exclude -
module
the name of the artifact we want to exclude. This is equivalent to thename
used to declare a dependency in Gradle.
It’s also entirely valid to pass only group
or only module
to match more generically. In the above example though, all combinations would result in the following updated transitive dependencies appearing on the classpaths.
\--- com.google.guava:guava:30.1.1-jre
+--- com.google.guava:failureaccess:1.0.1
+--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
+--- org.checkerframework:checker-qual:3.8.0
+--- com.google.errorprone:error_prone_annotations:2.5.1
\--- com.google.j2objc:j2objc-annotations:1.3
This shows that we have one less dependency now. Findbugs is no more! 🔫
A caveat
The problem with adding an exclude per-dependency is that if another dependency also pulls in the excluded dependency, the exclude is ignored.
Using the above example, imagine that your well-meaning colleague decided to add another dependency jackson-datatype-guava.
Our build script dependencies now look like this.
dependencies {
implementation('com.google.guava:guava:30.1.1-jre') {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
}
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-guava:2.12.4'
}
On the surface this seems like an innocent change, but how do the classpaths look now?
+--- com.google.guava:guava:30.1.1-jre
| +--- com.google.guava:failureaccess:1.0.1
| +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
| +--- com.google.code.findbugs:jsr305:3.0.2
| +--- org.checkerframework:checker-qual:3.8.0
| +--- com.google.errorprone:error_prone_annotations:2.5.1
| \--- com.google.j2objc:j2objc-annotations:1.3
\--- com.fasterxml.jackson.datatype:jackson-datatype-guava:2.12.4
+--- com.google.guava:guava:21.0 -> 30.1.1-jre (*)
Findbugs has sneaked its way back in again! 😩 That’s because jackson-datatype-guava depends on guava, meaning all of guava’s transitive dependencies get pulled in again by Gradle.
This functionality can be helpful, since it means we have to think carefully how an exclusion applies to each dependency. If we’re confident that jackson-datatype-guava also doesn’t need findbugs, we can add another exclude rule to its dependency definition.
What if we have a dependency that we’re absolutely sure should never be included though? Can we end this game of whack-a-mole permanently?
Fortunately Gradle has another trick up its sleeve…
Option 2) per-configuration exclude rules
For the scenario where we’re confident that a transitive dependency should be excluded across all dependencies, Gradle offers exclusion rules against dependency configurations.
Let’s use example from earlier, where we declared an implementation dependency on guava, which transitively depended on findbugs. The per-configuration exclude rule for this in the Groovy build.gradle looks like this.
dependencies {
implementation 'com.google.guava:guava:30.1.1-jre'
}
configurations.implementation {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
}
And in the Kotlin build.gradle.kts.
dependencies {
implementation("com.google.guava:guava:30.1.1-jre")
}
configurations.implementation {
exclude(group = "com.google.code.findbugs", module = "jsr305")
}
This time we pass a closure to the dependency configuration. Once again, within the closure we call the exclude
method with a group
and/or module
.
With only the per-configuration exclude rule applied, the compile and runtime classpath dependencies look like this.
\--- com.google.guava:guava:30.1.1-jre
+--- com.google.guava:failureaccess:1.0.1
+--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
+--- org.checkerframework:checker-qual:3.8.0
+--- com.google.errorprone:error_prone_annotations:2.5.1
\--- com.google.j2objc:j2objc-annotations:1.3
Findbugs successfully squished again! Even if we add more dependencies which transitively depend on findbugs, it won’t appear on our classpaths.
An SLF4J + Spring Boot example
Consider a Spring Boot web application in which we want to use the SLF4J logging framework with a Log4j2 implementation. The project’s build script has the following dependencies.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:2.5.3'
implementation 'org.springframework.boot:spring-boot-starter-log4j2:2.5.3'
}
The application contains the following Java code to get and use a logger, using only the SLF4J APIs.
package com.tomgregory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LoggingExample {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(LoggingExample.class);
logger.error("Error level output");
}
}
When we run the application there are 2 problems:
-
at startup we get the SLF4J: Class path contains multiple SLF4J bindings warning mentioned at the start of this article
-
when we try to get an instance of
org.slf4j.Logger
we get the following exception.
org.apache.logging.log4j.LoggingException: log4j-slf4j-impl cannot be present with log4j-to-slf4j
Let’s deal with the exception first.
Here are the dependencies on the runtime classpath (...
indicates entries left out for clarity).
+--- org.springframework.boot:spring-boot-starter-web:2.5.3
| +--- org.springframework.boot:spring-boot-starter:2.5.3
| | +--- ...
| | +--- org.springframework.boot:spring-boot-starter-logging:2.5.3
| | | +--- ch.qos.logback:logback-classic:1.2.4
| | | | +--- ch.qos.logback:logback-core:1.2.4
| | | | \--- org.slf4j:slf4j-api:1.7.31 -> 1.7.32
| | | +--- org.apache.logging.log4j:log4j-to-slf4j:2.14.1
| | | | +--- org.slf4j:slf4j-api:1.7.25 -> 1.7.32
| | | | \--- org.apache.logging.log4j:log4j-api:2.14.1
| | | \--- org.slf4j:jul-to-slf4j:1.7.32
| | | \--- org.slf4j:slf4j-api:1.7.32
| | +--- ...
| +--- ...
\--- org.springframework.boot:spring-boot-starter-log4j2:2.5.3
+--- org.apache.logging.log4j:log4j-slf4j-impl:2.14.1
| +--- org.slf4j:slf4j-api:1.7.25 -> 1.7.32
| +--- org.apache.logging.log4j:log4j-api:2.14.1
| \--- org.apache.logging.log4j:log4j-core:2.14.1
| \--- org.apache.logging.log4j:log4j-api:2.14.1
+--- org.apache.logging.log4j:log4j-core:2.14.1 (*)
+--- org.apache.logging.log4j:log4j-jul:2.14.1
| \--- org.apache.logging.log4j:log4j-api:2.14.1
\--- org.slf4j:jul-to-slf4j:1.7.32 (*)
Highlighted above are the dependencies listed in the exception. log4j-to-slf4j according to its docs allows applications coded to the Log4j 2 API to be routed to SLF4J. We can safely exclude it as we won’t be calling the Log4j 2 APIs within our application, only the SLF4J APIs.
Here’s how the exclude rule looks.
configurations.implementation {
exclude group: 'org.apache.logging.log4j', module: 'log4j-to-slf4j'
}
This fixes the exception and now the logging framework can actually be used. But we still get this unfriendly warning on startup.
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/C:/Users/Tom/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-slf4j-impl/2.14.1/9a40554b8dab7ac9606089c87ae8a5ba914ec932/log4j-slf4j-impl-2.14.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/C:/Users/Tom/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.2.4/f3bc99fd0b226065012b24fe9f808299048bab54/logback-classic-1.2.4.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]
So it’s findings bindings for both log4j and logback. Let’s review the classpath dependencies again.
+--- org.springframework.boot:spring-boot-starter-web:2.5.3
| +--- org.springframework.boot:spring-boot-starter:2.5.3
| | +--- ...
| | +--- org.springframework.boot:spring-boot-starter-logging:2.5.3
| | | +--- ch.qos.logback:logback-classic:1.2.4
| | | | +--- ch.qos.logback:logback-core:1.2.4
| | | | \--- org.slf4j:slf4j-api:1.7.31 -> 1.7.32
| | | \--- org.slf4j:jul-to-slf4j:1.7.32
| | | \--- org.slf4j:slf4j-api:1.7.32
| | +--- ...
| +--- ...
\--- org.springframework.boot:spring-boot-starter-log4j2:2.5.3
+--- org.apache.logging.log4j:log4j-slf4j-impl:2.14.1
| +--- org.slf4j:slf4j-api:1.7.25 -> 1.7.32
| +--- org.apache.logging.log4j:log4j-api:2.14.1
| \--- org.apache.logging.log4j:log4j-core:2.14.1
| \--- org.apache.logging.log4j:log4j-api:2.14.1
+--- org.apache.logging.log4j:log4j-core:2.14.1 (*)
+--- org.apache.logging.log4j:log4j-jul:2.14.1
| \--- org.apache.logging.log4j:log4j-api:2.14.1
\--- org.slf4j:jul-to-slf4j:1.7.32 (*)
When using the SLF4J logging framework, we should only have one binding to a logging implementation on the runtime classpath. The highlighted line above shows that spring-boot-starter-logging is bringing in logback-classic, which we don’t need.
At this point rather than excluding logback-classic, we can actually exclude the whole spring-boot-starter-logging dependency in favour of spring-boot-starter-log4j2, so our exclude rule looks like this.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:2.5.3'
implementation 'org.springframework.boot:spring-boot-starter-log4j2:2.5.3'
}
configurations.implementation {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
No warnings on startup now. 👍 This is also the same approach suggested by the Spring Boot Logging docs.
Exclude dependency from all configurations
Excluding the dependency from the implementation dependency configuration is enough to fix the Spring Boot + SLF4J error. In fact, this excludes the dependency from the compile, runtime, testCompile, and testRuntime classpaths!
But, Gradle offers a way to exclude dependencies from all dependency configurations. This could be helpful, for example, if you had the same dependency on the annotationProcessor path and wanted to exclude it.
configurations.all {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
Option 3) module replacement exclude alternative
One alternative suggested in the Spring Boot documentation is to replace a dependency rather than exclude it.
We can achieve this with the following entry in our Groovy build.gradle.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:2.5.3'
implementation 'org.springframework.boot:spring-boot-starter-log4j2:2.5.3'
modules {
module('org.springframework.boot:spring-boot-starter-logging') {
replacedBy 'org.springframework.boot:spring-boot-starter-log4j2', 'Use Log4j2 instead of Logback'
}
}
}
And here’s how that looks in the Kotlin build.gradle.kts.
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:2.5.3")
implementation("org.springframework.boot:spring-boot-starter-log4j2:2.5.3")
modules {
module("org.springframework.boot:spring-boot-starter-logging") {
replacedBy("org.springframework.boot:spring-boot-starter-log4j2", "Use Log4j2 instead of Logback")
}
}
}
This has the advantage of being more explicit than an exclude rule, as it ties the dependency to be excluded and the replacement together in one place. We can also pass a reason for the replacement to help with issue diagnosis.
Reviewing your project’s dependencies
You can check the dependencies in your own project using Gradle’s built-in dependencies task. Here’s how to review all dependencies across all dependency configurations:
./gradlew dependencies
Or to review the dependencies for a specific dependency configuration.
./gradlew dependencies --configuration <dependency-configuration-name>
The most interesting dependency configurations to plug into the above command are probably compileClasspath and runtimeClasspath as they’re used by Gradle directly when compiling and running your application.
All the dependency graphs in this article were generated using this mechanism. For a full understanding of using the dependencies task, as well as other troubleshooting options available in Gradle, I highly recommend signing up to the Gradle Hero course.
Common pitfalls
If you’re having problems check out these common errors from my own experience and those of others online.
Trying to exclude with name
instead of module
For reasons still unknown, when specifying an exclude in Gradle you pass a module
key as opposed to a name
key used when specifying a dependency. They represent the same value, the name/id of the artifact.
Here’s the INVALID syntax.
configurations.implementation {
exclude group: 'org.springframework.boot', name: 'spring-boot-starter-logging'
}
Which produces this error in the build.
Could not set unknown property 'name' for object of type org.gradle.api.internal.artifacts.DefaultExcludeRule.
Easily fixed by replacing name
with module
.
configurations.implementation {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
Trying to exclude using string instead of map notation
Another difference between using the exclude
method and declaring dependencies is that you can’t pass a string instead of a map.
Here’s an example of an INVALID build script exclusion rule.
configurations.implementation {
exclude 'org.springframework.boot:spring-boot-starter-logging'
}
Which results in this error.
Could not find method exclude() for arguments [org.springframework.boot:spring-boot-starter-logging] on configuration ':implementation'
This is fixed by replacing the string with a map.
configurations.implementation {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
Comparison of Gradle’s dependency exclude approaches
Approach name | Description | When to use | How to specify |
---|---|---|---|
Per-dependency exclude | Add an exclude rule to a specific dependency | You want to exclude a transitive dependency from one specific dependency, but not necessarily if it gets pulled in by another | Map of group and/or module |
Per-configuration exclude | Add an exclude rule to an entire dependency configuration or all dependency configurations | You’re sure you want to exclude the transitive dependency across all dependencies | Map of group and/or module |
Module replacement | Replace one dependency with another | You know that whenever there’s one dependency, it should always be replaced with another | String of ‘ |
Resources
Try out for yourself the SLF4J + Spring Boot example from this article in this GitHub repository.
If you’ve come across a relevant scenario which hasn’t been mentioned then please leave a comment below to start a discussion.
Video
Check out the accompanying video from the Tom Gregory Tech YouTube channel.