How to exclude Gradle dependencies

Gradle exclude dependencies

Last Updated on November 29, 2021

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 the name 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 it’s 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.

Dependency configurations primer

A dependency configuration is just a grouping of dependencies. One such dependency configuration is implementation, which is a group of dependencies that appear on the runtime and compile classpaths. Other dependency configurations in Java projects include runtimeOnly and compileOnly. You can probably guess which classpaths they end up on. 😉

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:

  1. at startup we get the SLF4J: Class path contains multiple SLF4J bindings warning mentioned at the start of this article
  2. 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 nameDescriptionWhen to useHow to specify
Per-dependency excludeAdd an exclude rule to a specific dependencyYou want to exclude a transitive dependency from one specific dependency, but not necessarily if it gets pulled in by anotherMap of group and/or module
Per-configuration excludeAdd an exclude rule to an entire dependency configuration or all dependency configurationsYou’re sure you want to exclude the transitive dependency across all dependenciesMap of group and/or module
Module replacementReplace one dependency with anotherYou know that whenever there’s one dependency, it should always be replaced with anotherString of ‘<group>:<module>’

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.

Get going with Gradle course
Gradle icon

Want to learn more about Gradle?
Check out the full selection of Gradle tutorials.

How to exclude Gradle dependencies

One thought on “How to exclude Gradle dependencies

  1. Please note that changes done with module replacement rules will not be reflected in a potentially published pom while excludes in dependencies and configurations are.
    This has to be considered with downstream projects that consume those poms.
    I think it depends on the use case which way you want or need to go.

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to top

Get the newsletter

Found this article helpful? Subscribe for monthly updates.

✅ All of my latest articles for the month
✅ Access to video tutorials
✅ Exclusive tips not found on my website