Certain combinations of transitive dependencies in a Java project cause compile and runtime issues. Fortunately, Gradle has several ways to exclude those unwanted dependencies from the Java classpath to fix your project. In this article you’ll learn the common reasons you’d want to exclude dependencies and how to use each of Gradle’s exclude strategies.

Why 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 are added to the compile and 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 (i.e. implementation) appears on the classpath, otherwise it doesn’t know which to use for logging. For example, if you include both the Logback and Log4J bindings you get this runtime error.

SLF4J: Class path contains multiple SLF4J bindings.

This means one of those bindings must be removed from the classpath, which you’ll see how to do shortly using exclude rules.

2. Unused transitive dependency

Sometimes we only need to use a small part of a dependency. 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 (which we’ll simply call findbugs). If we only use one specific method from Guava that we’re confident doesn’t use findbugs, then findbugs is a candidate for exclusion from the Java classpath.

Of course, when taking this approach we must be confident that the excluded library won’t be required now or in the future. This can be verified with automated tests to exercise the area of code that uses the dependency.

The benefits of excluding unused transitive dependencies are twofold:

  • a cleaner compile classpath which can improve performance
  • a smaller application deployable due to less artifacts on the runtime classpath

Now you know what problems transitive dependencies can cause, let’s explore 3 ways in which Gradle excludes them.

Option 1) Per-Dependency Exclude Rule

When you specify a dependency in your build script, at the same time you can provide an exclude rule telling Gradle not to pull in a specific transitive dependency.

For example, say we have a Gradle project that depends on Google’s Guava library with co-ordinates com.google.guava:guava:32.1.3-jre.

Here’s the build script dependency.

dependencies {
    implementation("com.google.guava:guava:32.1.3-jre")
}
dependencies {
    implementation 'com.google.guava:guava:32.1.3-jre'
}

Inspecting the compile and runtime classpaths shows these transitive dependencies.

\--- com.google.guava:guava:32.1.3-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.37.0
     \--- com.google.errorprone:error_prone_annotations:2.21.1

Guava pulls in a lot of extra stuff! You’ll see later how to generate such a dependency graph yourself.

Imagine we just want to use a tiny subset of Guava, like the endlessly helpful ImmutableMap.of(K k1, V v1) method. That means we can exclude unnecessary transitive dependencies like findbugs.

Here’s the syntax for an appropriate exclude rule.

dependencies {
    implementation("com.google.guava:guava:32.1.3-jre") {
        exclude(group = "com.google.code.findbugs", module = "jsr305")
    }
}
dependencies {
    implementation('com.google.guava:guava:32.1.3-jre') {
        exclude group: 'com.google.code.findbugs', module: 'jsr305'
    }
}

In 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 the same as the name used to declare a dependency.

It’s also valid to pass only group or only module to match more generically.

In our example though, any combination of group/module results in this updated list of transitive dependencies.

\--- com.google.guava:guava:32.1.3-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.37.0
     \--- com.google.errorprone:error_prone_annotations:2.21.1

So we now have one less dependency. Findbugs is no more! 🔫

A caveat

The problem with adding an exclude at the dependency level (i.e. per-dependency) is if another dependency also pulls in the excluded dependency, the exclude is ignored.

Continuing the above example, imagine a well-meaning colleague decides to add another dependency on jackson-datatype-guava.

Our build script dependencies now look like this.

dependencies {
    implementation("com.google.guava:guava:32.1.3-jre") {
        exclude(group = "com.google.code.findbugs", module = "jsr305")
    }
    implementation("com.fasterxml.jackson.datatype:jackson-datatype-guava:2.15.3")
}
dependencies {
    implementation('com.google.guava:guava:32.1.3-jre') {
        exclude group: 'com.google.code.findbugs', module: 'jsr305'
    }
    implementation 'com.fasterxml.jackson.datatype:jackson-datatype-guava:2.15.3'
}

On the surface this seems like an innocent change, but look at the classpaths now.

+--- com.google.guava:guava:32.1.3-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.37.0
|    +--- com.google.errorprone:error_prone_annotations:2.21.1
|    \--- com.google.j2objc:j2objc-annotations:2.8
\--- com.fasterxml.jackson.datatype:jackson-datatype-guava:2.15.3
     +--- com.google.guava:guava:23.6.1-jre -> 32.1.3-jre (*)
     ... truncated for clarity

Despite our exclude rule, findbugs has sneaked its way back in again! 😩 That’s because jackson-datatype-guava depends on guava, meaning Gradle pulls in all of guava’s transitive dependencies again.

The upside to this functionality is that it makes us think carefully how an exclusion applies to each dependency. If we’re confident that jackson-datatype-guava also doesn’t use findbugs, we can add another exclude rule to its dependency definition.

But what if there’s a dependency we’re ABSOLUTELY sure should never be included? Can we end this game of whack-a-mole permanently?

Fortunately Gradle has another trick up its sleeve…

Option 2) Per-Configuration Exclude Rule

When we’re confident a transitive dependency should be excluded from all dependencies, Gradle offers exclude rules per dependency configuration.

Let’s use the earlier example, with an implementation dependency on guava, which transitively depends on findbugs.

Here’s the syntax for the per-configuration exclude rule.

dependencies {
    implementation("com.google.guava:guava:32.1.3-jre")
}

configurations.implementation {
    exclude(group = "com.google.code.findbugs", module = "jsr305")
}
dependencies {
    implementation 'com.google.guava:guava:32.1.3-jre'
}

configurations.implementation {
    exclude group: 'com.google.code.findbugs', module: 'jsr305'
}

This time we pass a closure to the dependency configuration. Similarly, in the closure we call exclude passing a group and/or module.

With only the per-configuration exclude rule applied, here are the compile and runtime classpath dependencies.

\--- com.google.guava:guava:32.1.3-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.37.0
     \--- com.google.errorprone:error_prone_annotations:2.21.1

Findbugs was successfully squished again! Even if we add other dependencies which transitively depend on findbugs, it won’t appear on our classpaths because the exclude rule is per-configuration.

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:3.1.5")
    implementation("org.springframework.boot:spring-boot-starter-log4j2:3.1.5")
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web:3.1.5'
    implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.1.5'
}

The application contains this 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. we get the SLF4J: Class path contains multiple SLF4J bindings warning mentioned earlier

  2. when we try to get an instance of org.slf4j.Logger we get an exception

org.apache.logging.log4j.LoggingException: log4j-slf4j2-impl cannot be present with log4j-to-slf4j

Let’s deal with the exception first.

Here are the runtime classpath dependencies (... indicates entries left out for clarity).

+--- org.springframework.boot:spring-boot-starter-web:3.1.5
|    +--- org.springframework.boot:spring-boot-starter:3.1.5
|    |    +--- ...
|    |    +--- org.springframework.boot:spring-boot-starter-logging:3.1.5
|    |    |    +--- ch.qos.logback:logback-classic:1.4.11
|    |    |    |    +--- ch.qos.logback:logback-core:1.4.11
|    |    |    |    \--- org.slf4j:slf4j-api:2.0.7 -> 2.0.9
|    |    |    +--- org.apache.logging.log4j:log4j-to-slf4j:2.20.0
|    |    |    |    +--- org.apache.logging.log4j:log4j-api:2.20.0
|    |    |    |    \--- org.slf4j:slf4j-api:1.7.36 -> 2.0.9
|    |    |    \--- org.slf4j:jul-to-slf4j:2.0.9
|    |    |         \--- org.slf4j:slf4j-api:2.0.9
|    |    +--- ...
|    +--- ...
\--- org.springframework.boot:spring-boot-starter-log4j2:3.1.5
     +--- org.apache.logging.log4j:log4j-slf4j2-impl:2.20.0
     |    +--- org.apache.logging.log4j:log4j-api:2.20.0
     |    +--- org.slf4j:slf4j-api:2.0.6 -> 2.0.9
     |    \--- org.apache.logging.log4j:log4j-core:2.20.0
     |         \--- org.apache.logging.log4j:log4j-api:2.20.0
     +--- org.apache.logging.log4j:log4j-core:2.20.0 (*)
     \--- org.apache.logging.log4j:log4j-jul:2.20.0
          \--- org.apache.logging.log4j:log4j-api:2.20.0

Highlighted above are dependencies listed in the exception message (log4j-to-slf4j and log4j-slf4j-impl).

According to the log4j-to-slf4j docs, this framework “allows applications coded to the Log4j 2 API to be routed to SLF4J”.

So we can safely exclude this dependency as we won’t be calling the Log4j 2 APIs from our application class. Only the SLF4J APIs.

Here’s the per-configuration exclude rule.

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web:3.1.5")
    implementation("org.springframework.boot:spring-boot-starter-log4j2:3.1.5")
}

configurations.implementation {
    exclude(group = "org.apache.logging.log4j", module = "log4j-to-slf4j")
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web:3.1.5'
    implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.1.5'
}

configurations.implementation {
    exclude group: 'org.apache.logging.log4j', module: 'log4j-to-slf4j'
}

This fixes the exception, so the logging framework can now be used at runtime. But we still get an unfriendly warning on startup.

SLF4J: Class path contains multiple SLF4J providers.
SLF4J: Found provider [org.apache.logging.slf4j.SLF4JServiceProvider@5ce65a89]
SLF4J: Found provider [ch.qos.logback.classic.spi.LogbackServiceProvider@25f38edc]
SLF4J: See https://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual provider is of type [org.apache.logging.slf4j.SLF4JServiceProvider@5ce65a89]
20:37:29.711 [main] ERROR com.tomgregory.Application - Logging class: class org.apache.logging.slf4j.Log4jLogger

So it’s finding bindings for both log4j and logback. Let’s review the runtime classpath dependencies again.

+--- org.springframework.boot:spring-boot-starter-web:3.1.5
|    +--- org.springframework.boot:spring-boot-starter:3.1.5
|    |    +--- ...
|    |    +--- org.springframework.boot:spring-boot-starter-logging:3.1.5
|    |    |    +--- ch.qos.logback:logback-classic:1.4.11
|    |    |    |    +--- ch.qos.logback:logback-core:1.4.11
|    |    |    |    \--- org.slf4j:slf4j-api:2.0.7 -> 2.0.9
|    |    |    \--- org.slf4j:jul-to-slf4j:2.0.9
|    |    |         \--- org.slf4j:slf4j-api:2.0.9
|    |    +--- ...
|    +--- ...
\--- org.springframework.boot:spring-boot-starter-log4j2:3.1.5
     +--- org.apache.logging.log4j:log4j-slf4j2-impl:2.20.0
     |    +--- org.apache.logging.log4j:log4j-api:2.20.0
     |    +--- org.slf4j:slf4j-api:2.0.6 -> 2.0.9
     |    \--- org.apache.logging.log4j:log4j-core:2.20.0
     |         \--- org.apache.logging.log4j:log4j-api:2.20.0
     +--- org.apache.logging.log4j:log4j-core:2.20.0 (*)
     \--- org.apache.logging.log4j:log4j-jul:2.20.0
          \--- org.apache.logging.log4j:log4j-api:2.20.0

When using the SLF4J logging framework, the runtime classpath should only contain a single binding to a logging implementation. The line highlighted above shows that spring-boot-starter-logging is bringing in logback-classic, which we don’t want.

But rather than exclude just logback-classic, we can actually exclude the whole spring-boot-starter-logging dependency in favour of spring-boot-starter-log4j2. This also has the advantage of making our previous exclude rule redundant, as log4j-to-slf4j is pulled in by spring-boot-starter-logging.

Here’s the updated exclude rule.

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web:3.1.5")
    implementation("org.springframework.boot:spring-boot-starter-log4j2:3.1.5")
}

configurations.implementation {
    exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web:3.1.5'
    implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.1.5'
}

configurations.implementation {
    exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}

No warnings on startup now. 👍

It’s good to know this is the approach recommended 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. As an example, this could be helpful if you have the same dependency on the annotationProcessor path and want to exclude it.

Here’s the syntax to exclude spring-boot-starter-logging from all dependency configurations.

configurations.all {
    exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
}
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 rather than exclude a dependency.

Here’s how to replace spring-boot-starter-logging with org.springframework.boot:spring-boot-starter-log4j2.

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web:3.1.5")
    implementation("org.springframework.boot:spring-boot-starter-log4j2:3.1.5")

    modules {
        module("org.springframework.boot:spring-boot-starter-logging") {
            replacedBy(
                "org.springframework.boot:spring-boot-starter-log4j2",
                "Use Log4J2 instead of Logback"
            )
        }
    }
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web:3.1.5'
    implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.1.5'

    modules {
        module('org.springframework.boot:spring-boot-starter-logging') {
            replacedBy 'org.springframework.boot:spring-boot-starter-log4j2', 'Use Log4j2 instead of Logback'
        }
    }
}

Note the <group>:<module> format of the string passed to the module method and then as the first argument to replacedBy. You can also provide a replacement reason to help with troubleshooting and documentation.

The module replacement is more explicit than an exclude rule, as it ties the dependency to be excluded and the replacement together in one place. In terms of the generated classpaths, the end result is the same.

Review Your Project’s Dependencies

Check the dependencies in your own project using Gradle’s built-in dependencies task.

Here’s how to review dependencies across all dependency configurations:

./gradlew dependencies

To review dependencies for a specific dependency configuration, use this.

./gradlew dependencies --configuration <dependency-configuration-name>

The most important dependency configurations to plug into the above command are compileClasspath and runtimeClasspath.

All the dependency graphs in this article were generated with this technique. For a full understanding of the dependencies task, as well as other troubleshooting options available in Gradle, sign up to the Gradle Hero course.

Common Pitfalls

Here are some common problems you might encounter trying to exclude dependencies.

Trying to exclude with name instead of module

For reasons still unknown, when specifying an exclude in Gradle you pass a module key instead of the name key used to specify 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")
}
configurations.implementation {
    exclude group: 'org.springframework.boot', name: 'spring-boot-starter-logging'
}

Which produces this error on build.

Cannot find a parameter with this name: name

It’s easily fixed by replacing name with module.

configurations.implementation {
    exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
}
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 exclude a dependency using string notation e.g. <group>:<module>.

Here’s an example of an INVALID build script exclusion rule.

configurations.implementation {
    exclude("org.springframework.boot:spring-boot-starter-logging")
}
configurations.implementation {
    exclude 'org.springframework.boot:spring-boot-starter-logging'
}

Gradle interprets the group as invalid so doesn’t exclude the dependency as intended.

Fix this by replacing the string with a map.

configurations.implementation {
    exclude(group = "org.springframework.boot", name = "spring-boot-starter-logging")
}
configurations.implementation {
    exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}

Gradle Exclude Dependency Comparison

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 <group>:<module>

Resources

Try out the SLF4J + Spring Boot example from this article by checking out the accompanying GitHub repository.

If you have a scenario which wasn’t mentioned, leave a comment below to start a discussion.

Video

Check out the accompanying video from the Tom Gregory Tech YouTube channel.