# Managing Transitive Dependencies

version 5.4.1

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 1. 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'
            }
        }
    }

build.gradle.kts

    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.

## 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 2. Unresolved artifacts for transitive dependencies
build.gradle

    plugins {
        id 'java'
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        implementation 'log4j:log4j:1.2.15'
    }

build.gradle.kts

    plugins {
        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 3. 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'
        }
    }

build.gradle.kts

    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 4. 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'
    }

build.gradle.kts

    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 5. Enforcing a dependency version
build.gradle

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

build.gradle.kts

    dependencies {
        implementation("org.apache.httpcomponents:httpclient:4.5.4")
        implementation("commons-codec:commons-codec:1.9") {
            isForce = 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 6. 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'
    }

build.gradle.kts

    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 `false`. As a result only the main artifact will be resolved for the declared dependency.

Example 7. Disabling transitive dependency resolution for a declared dependency
build.gradle

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

build.gradle.kts

    dependencies {
        implementation("com.google.guava:guava:23.0") {
            isTransitive = 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 8. Disabling transitive dependency resolution on the configuration-level
build.gradle

    configurations.all {
        transitive = false
    }

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

build.gradle.kts

    configurations.all {
        isTransitive = 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 9. Depending on a BOM to import its dependency constraints
build.gradle

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

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

build.gradle.kts

    dependencies {
        // import a BOM
        implementation(platform("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.

However often BOMs are not only providing versions as recommendations, but also a way to override any other version found in the graph. You can enable this behavior by using the `enforcedPlatform` keyword, instead of `platform`, when importing the BOM:

Example 10. Importing a BOM, making sure the versions it defines override any other version found
build.gradle

    dependencies {
        // import a BOM. The versions used in this file will override any other version found in the graph
        implementation enforcedPlatform('org.springframework.boot:spring-boot-dependencies:1.5.8.RELEASE')

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

        // this version will be overridden by the one found in the BOM
        implementation 'org.codehaus.groovy:groovy:1.8.6'
    }

build.gradle.kts

    dependencies {
        // import a BOM. The versions used in this file will override any other version found in the graph
        implementation(enforcedPlatform("org.springframework.boot:spring-boot-dependencies:1.5.8.RELEASE"))

        // define dependencies without versions
        implementation("com.google.code.gson:gson")
        implementation("dom4j:dom4j")

        // this version will be overridden by the one found in the BOM
        implementation("org.codehaus.groovy:groovy:1.8.6")
    }

## Dependency version alignment

Dependency version alignment allows different modules belonging to the same logical group (a platform) to have identical versions in a dependency graph.

### Handling inconsistent module versions

Gradle supports aligning versions of modules which belong to the same "platform". It is often preferable, for example, that the API and implementation modules of a component are using the same version. However, because of the game of transitive dependency resolution, it is possible that different modules belonging to the same platform end up using different versions. For example, your project may depend on the `jackson-databind` and `vert.x` libraries, as illustrated below:

Example 11. Declaring dependencies
build.gradle

    dependencies {
        // a dependency on Jackson Databind
        implementation 'com.fasterxml.jackson.core:jackson-databind:2.8.9'

        // and a dependency on vert.x
        implementation 'io.vertx:vertx-core:3.5.3'
    }

build.gradle.kts

    dependencies {
        // a dependency on Jackson Databind
        implementation("com.fasterxml.jackson.core:jackson-databind:2.8.9")

        // and a dependency on vert.x
        implementation("io.vertx:vertx-core:3.5.3")
    }

Because `vert.x` depends on `jackson-core`, we would actually resolve the following dependency versions:

- `jackson-core` version `2.9.5` (brought by `vertx-core`)

- `jackson-databind` version `2.9.5` (by conflict resolution)

- `jackson-annotation` version `2.9.0` (dependency of `jackson-databind:2.9.5`)

It’s easy to end up with a set of versions which do not work well together. To fix this, Gradle supports dependency version alignment, which is supported by the concept of platform. A platform represents a set of modules which "work well together". Either because they are actually published as a whole (when one of the members of the platform is published, all other modules are also published with the same version), or because someone tested modules and indicates that they work well together (typically, the Spring Platform).

### Aligning versions natively with Gradle

Gradle natively supports alignment of modules produced by Gradle. This is a direct consequence of the transitivity of dependency constraints. So if you have a multi-project build, and that you wish that consumers get the same version of all your modules, Gradle provides a simple way to do this using the Java Platform Plugin.

For example, if you have a project that consists of 3 modules:

- `lib`

- `utils`

- `core`, depending on `lib` and `utils`

And a consumer that declares the following dependencies:

- `core` version 1.0

- `lib` version 1.1

then by default resolution would select `core:1.0` and `lib:1.1`, because `lib` has no dependency on `core`. We can fix this by adding a new module in our project, a platform, that will add constraints on all the modules of your project:

Example 12. The platform module
build.gradle

    plugins {
        id 'java-platform'
    }

    dependencies {
        // The platform declares constraints on all components that
        // require alignment
        constraints {
            api(project(":core"))
            api(project(":lib"))
            api(project(":utils"))
        }
    }

build.gradle.kts

    plugins {
        `java-platform`
    }

    dependencies {
        // The platform declares constraints on all components that
        // require alignment
        constraints {
            api(project(":core"))
            api(project(":lib"))
            api(project(":utils"))
        }
    }

Once this is done, we need to make sure that all modules now depend on the platform, like this:

Example 13. Declaring a dependency on the platform
build.gradle

    dependencies {
        // Each project has a dependency on the platform
        api(platform(project(":platform")))

        // And any additional dependency required
        implementation(project(":lib"))
        implementation(project(":utils"))
    }

build.gradle.kts

    dependencies {
        // Each project has a dependency on the platform
        api(platform(project(":platform")))

        // And any additional dependency required
        implementation(project(":lib"))
        implementation(project(":utils"))
    }

It is important that the platform contains a constraint on all the components, but also that each component has a dependency on the platform. By doing this, whenever Gradle will add a dependency to a module of the platform on the graph, it will also include constraints on the other modules of the platform. This means that if we see another module belonging to the same platform, we will automatically upgrade to the same version.

In our example, it means that we first see `core:1.0`, which brings a platform `1.0` with constraints on `lib:1.0` and `lib:1.0`. Then we add `lib:1.1` which has a dependency on `platform:1.1`. By conflict resolution, we select the `1.1` platform, which has a constraint on `core:1.1`. Then we conflict resolve between `core:1.0` and `core:1.1`, which means that `core` and `lib` are now aligned properly.

✨ | This behavior is enforced for published components only if you use Gradle Module Metadata.

### Aligning versions of modules not published with Gradle

Whenever the publisher doesn’t use Gradle, like in our Jackson example, we can explain to Gradle that that all Jackson modules "belong to" the same platform and benefit from the same behavior as with native alignment:

Example 14. A dependency version alignment rule
build.gradle

    class JacksonAlignmentRule implements ComponentMetadataRule {
        void execute(ComponentMetadataContext ctx) {
            ctx.details.with {
                if (id.group.startsWith("com.fasterxml.jackson")) {
                    // declare that Jackson modules all belong to the Jackson virtual platform
                    belongsTo("com.fasterxml.jackson:jackson-platform:${id.version}")
                }
            }
        }
    }

build.gradle.kts

    open class JacksonAlignmentRule: ComponentMetadataRule {
        override fun execute(ctx: ComponentMetadataContext) {
            ctx.details.run {
                if (id.group.startsWith("com.fasterxml.jackson")) {
                    // declare that Jackson modules all belong to the Jackson virtual platform
                    belongsTo("com.fasterxml.jackson:jackson-platform:${id.version}")
                }
            }
        }
    }

By using the `belongsTo` keyword, we declare that all modules belong to the same virtual platform, which is treated specially by the engine, in particular with regards to alignment. We can use the rule we just created by registering it:

Example 15. Making use of a dependency version alignment rule
build.gradle

    dependencies {
        components.all(JacksonAlignmentRule)
    }

build.gradle.kts

    dependencies {
        components.all(JacksonAlignmentRule::class.java)
    }

Then all versions in the example above would align to `2.9.5`. However, Gradle would let you override that choice by specifying a dependency on the Jackson platform:

Example 16. Forceful platform downgrade
build.gradle

    dependencies {
        // Forcefully downgrade the Jackson platform to 2.8.9
        implementation enforcedPlatform('com.fasterxml.jackson:jackson-platform:2.8.9')
    }

build.gradle.kts

    dependencies {
        // Forcefully downgrade the Jackson platform to 2.8.9
        implementation(enforcedPlatform("com.fasterxml.jackson:jackson-platform:2.8.9"))
    }

### Virtual vs published platforms

A platform defined by a component metadata rule for which the `belongsTo` target module isn’t published on a repository is called a virtual platform. A virtual platform is considered specially by the engine and participates in dependency resolution like a published module, but triggers dependency version alignment. On the other hand, we can find "real" platforms published on public repositories. Typical examples include BOMs, like the Spring BOM. They differ in the sense that a published platform may refer to modules which are effectively different things. For example the Spring BOM declares dependencies on Spring as well as Apache Groovy. Obviously those things are versioned differently, so it doesn’t make sense to align in this case. In other words, if a platform is published, Gradle trusts its metadata, and will not try to align dependency versions of this platform.

## Component capabilities

### Introduction to capabilities

Often a dependency graph would accidentally contain multiple implementations of the same API. This is particularly common with logging frameworks, where multiple bindings are available, and that one library chooses a binding when another transitive dependency chooses another. Because those implementations live at different GAV coordinates, the build tool has usually no way to find out that there’s a conflict between those libraries. To solve this, Gradle provides the concept of capability.

It’s illegal to find two components providing the same capability in a single dependency graph. Intuitively, it means that if Gradle finds two components that provide the same thing on classpath, it’s going to fail with an error indicating what modules are in conflict. In our example, it means that different bindings of a logging framework provide the same capability.

### Capability coordinates

A capability is defined by a `(group, module, version)` triplet. Each component defines an implicit capability corresponding to its GAV coordinates (group, artifact, version). For example, the `org.apache.commons:commons-lang3:3.8` module has an implicit capability with group `org.apache.commons`, name `commons-lang3` and version `3.8`. It is important to realize that capabilities are versioned.

### Declaring component capabilities

✨ | Capabilities are a core feature of the experimental Gradle metadata file format. This means that components published with the experimental Gradle metadata file format can declare capabilities, but also that this feature is only natively understood by Gradle. However, it’s possible to declare capabilities on components which were not built by Gradle, as explained in this section.

If your build file contains the following dependencies:

Example 17. A build file with an implicit conflict of logging frameworks
build.gradle

    dependencies {
        // This dependency will bring log4:log4j transitively
        implementation 'org.apache.zookeeper:zookeeper:3.4.9'

        // We use log4j over slf4j
        implementation 'org.slf4j:log4j-over-slf4j:1.7.10'
    }

build.gradle.kts

    dependencies {
        // This dependency will bring log4:log4j transitively
        implementation("org.apache.zookeeper:zookeeper:3.4.9")

        // We use log4j over slf4j
        implementation("org.slf4j:log4j-over-slf4j:1.7.10")
    }

As is, it’s pretty hard to figure out that you will end up with two logging frameworks on the classpath. In fact, `zookeeper` will bring in `log4j`, where what we want to use is `log4j-over-slf4j`. We can preemptively detect the conflict by adding a rule which will declare that both logging frameworks provide the same capability:

Example 18. A build file with an implicit conflict of logging frameworks
build.gradle

    dependencies {
        // Activate the "LoggingCapability" rule
        components.all(LoggingCapability)
    }

    @CompileStatic
    class LoggingCapability implements ComponentMetadataRule {
        final static Set<String> LOGGING_MODULES = ["log4j", "log4j-over-slf4j"] as Set<String>

        void execute(ComponentMetadataContext context) {
            context.details.with {
                if (LOGGING_MODULES.contains(id.name)) {
                    allVariants {
                        it.withCapabilities {
                            // Declare that both log4j and log4j-over-slf4j provide the same capability
                            it.addCapability("org.slf4j", "slf4j-capability", "1.0")
                        }
                    }
                }
            }
        }
    }

build.gradle.kts

    dependencies {
        // Activate the "LoggingCapability" rule
        components.all(LoggingCapability::class.java)
    }

    class LoggingCapability : ComponentMetadataRule {
        val loggingModules = setOf("log4j", "log4j-over-slf4j")

        override
        fun execute(context: ComponentMetadataContext) = context.details.run {
            if (loggingModules.contains(id.name)) {
                allVariants {
                    withCapabilities {
                        // Declare that both log4j and log4j-over-slf4j provide the same capability
                        addCapability("org.slf4j", "slf4j-capability", "1.0")
                    }
                }
            }
        }
    }

By adding this rule, we will make sure that Gradle will detect conflicts and properly fail:

    Cannot choose between log4j:log4j:1.2.16 and org.slf4j:log4j-over-slf4j:1.7.10 because they provide the same capability: org.slf4j:slf4j-capability:1.0

It does not, however, choose what component to use for you: detecting a conflict is the first step, then you have to fix it.

### Solving capability conflicts

By default, Gradle will automatically choose the component with the highest version of a capability. This can be useful whenever a component is relocated at different coordinates in a new release. For example, the ASM library lived at `asm:asm` coordinates until version `3.3.1`, then changed to `org.ow2.asm:asm` since `4.0`. It is illegal to have both ASM ⇐3.3.1 and 4.0+ on the classpath, because they provide the same feature, it’s just that the component has been relocated. Because each component has an implicit capability corresponding to its GAV coordinates, we can fix this by having a rule that will declare that the `asm:asm` module provides the `org.ow2.asm:asm` capability:

Example 19. Automatic conflict resolution by capability versioning
build.gradle

    @CompileStatic
    class AsmCapability implements ComponentMetadataRule {
        void execute(ComponentMetadataContext context) {
            context.details.with {
                if (id.group == "asm" && id.name == "asm") {
                    allVariants {
                        it.withCapabilities {
                            // Declare that ASM provides the org.ow2.asm:asm capability, but with an older version
                            it.addCapability("org.ow2.asm", "asm", id.version)
                        }
                    }
                }
            }
        }
    }

build.gradle.kts

    class AsmCapability : ComponentMetadataRule {
        override
        fun execute(context: ComponentMetadataContext) = context.details.run {
            if (id.group == "asm" && id.name == "asm") {
                allVariants {
                    withCapabilities {
                        // Declare that ASM provides the org.ow2.asm:asm capability, but with an older version
                        addCapability("org.ow2.asm", "asm", id.version)
                    }
                }
            }
        }
    }

However, fixing by automatic capability version conflict resolution is not always suitable. In our logging example, it doesn’t matter what version of the logging frameworks we use, we should always select the slf4j bridge.

In this case, we can fix it by using dependency substitution:

Example 20. Substitute log4j with slf4j
build.gradle

        configurations.compileClasspath.resolutionStrategy.dependencySubstitution {
            substitute(module("log4j:log4j"))
                .because("Prefer SLF4J for logging")
                .with(module("org.slf4j:log4j-over-slf4j:1.7.10"))

        }

build.gradle.kts

        configurations.compileClasspath.resolutionStrategy.dependencySubstitution {
            substitute(module("log4j:log4j"))
                .because("Prefer SLF4J for logging")
                .with(module("org.slf4j:log4j-over-slf4j:1.7.10"))

        }