JVM Dependency Conflict Detection and Resolution plugins

Build Status Gradle Plugin Portal

These plugins add dependency capabilities to the metadata of well-known components hosted on Maven Central that are used in many Java projects. They also provide ways to resolve the potential capability conflict that can happen in the dependency graph of your application.

What is a dependency 'Capability' in Gradle and why should I care?

  • Ever seen this infamous Slf4J warning?

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:.../slf4j-log4j12-1.7.29.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:.../logback-classic-1.2.3.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.slf4j.impl.Log4jLoggerFactory]
  • Ever wondered how to make sure all your dependencies' logging ends up in your Log4J 2 configured outputs?

  • Ever had to handle a javax to jakarta migration and wondered how to make sure all your dependencies are compatible?

In the videos below, Jendrik explains the concept of Capability Conflicts and why they can help you to avoid "dependency hell" in your project.

29 11

ℹ️
With these plugins, you enable your build to detect and automatically resolve typical capability conflicts in the JVM ecosystem.

Plugins

💡
These plugins require Gradle 6.8.3 at a minimum.

The benefits described above are achieved through two plugins:

org.gradlex.jvm-dependency-conflict-detection

This plugin adds capability declarations to the metadata of well-known components hosted on Maven Central that are used in many Java projects. This plugin can be applied as a Settings or a Project plugin.

org.gradlex.jvm-dependency-conflict-resolution

This plugin adds opinionated capability resolutions to conflicting API or implementation libraries in the JVM ecosystem. This plugin can be applied as a Project plugin. It adds a DSL to configure resolution strategies, select logging implementations, and fix/enhance metadata of published components.

See each plugin’s documentation for more details.

JVM Dependency Conflict Detection

This plugin is responsible for applying metadata patching rules that add capability and alignment information to existing Java libraries.

⚠️
Applying only the jvm-dependency-conflict-detection plugin will result in broken builds if capability conflicts are found. Conflict resolution is handled by the jvm-dependency-conflict-resolution plugin.

Plugin dependency

Add the following to the build file of your convention plugin’s build (e.g. build-logic/build.gradle(.kts) or buildSrc/build.gradle(.kts)). Note that the jvm-dependency-conflict-resolution dependency contains both the detection and the resolution plugins.

dependencies {
    implementation("org.gradlex:jvm-dependency-conflict-resolution:2.1.2")
}

Apply the plugin

plugins {
    id("org.gradlex.jvm-dependency-conflict-detection")
}

You can apply the plugin as a project plugin (build.gradle(.kts)) or a setting plugin (settings.gradle(.kts)). If you don’t know what the difference is, using it as a project plugin (most Gradle plugins are project plugins) is the preferred way.

The following things are to consider:

  • If you use it as a project plugin make sure to apply it to all (sub)projects using a convention plugin

  • If you use it as a settings plugin you may directly apply it in your settings.gradle(.kts)

    • If you write additional component metadata rules, or use other plugins that add more rules, make sure to consistently do either everything in projects or in settings as Gradle cannot combine both approaches.

What is the concrete effect of the plugin?

The plugin makes sure that during dependency resolution, you do not end up with two components that 'do the same thing' in the dependency resolution result. That is, you won’t have two or more Jars with different names (e.g. jsr311-api-1.1.1.jar, javax.ws.rs-api-2.1.1.jar, jakarta.ws.rs-api-3.0.0.jar and jaxrs-api-3.0.1.Final.jar) but same/similar classes on the classpath. In this example, Gradle will use jaxrs-api in all places. You can see all effects in this build scan from this artificial sample project that includes dependencies to all components covered by rules in this plugin. See Appendix for a full list of all capabilities that are added by this plugin and the components they apply to.

build scan

I use the plugin and now there is a conflict - what now?

💡
You can apply the jvm-dependency-conflict-resolution plugin to get some automatic conflict resolution.

If you get an error like this:

> Module 'com.sun.mail:jakarta.mail' has been rejected:
     Cannot select module with conflict on capability 'javax.mail:mail:2.0.1' also provided by [com.sun.mail:mailapi:2.0.1(compile)]

It means that you need to make a decision for the given capability - in this case javax.mail:mail - by selecting one of the modules that both provide the capability. In this case, you can decide between com.sun.mail:jakarta.mail (see first line of message) and com.sun.mail:mailapi (see end of second line).

A decision is made by defining a resolution strategy for the capability. This is best done in the place where you applied this plugin (e.g. one of your convention plugins):

configurations.all {
  resolutionStrategy.capabilitiesResolution {
    withCapability("javax.mail:mail") {        // Capability for which to make the decision
      select("com.sun.mail:jakarta.mail:0")    // The component to select
    }
  }
}

One of the rules added by the plugin has an undesired effect - what now?

The goal of this plugin is to enrich the metadata of widely used components from Maven Central to allow Gradle to detect conflicts. The rules implemented in this plugin extend existing metadata with the mindset that the metadata should look like that in the first place. It just doesn’t for technical limitations during the development of the component. In most cases, because the component is published with Maven and only published POM metadata which cannot express capability information.

With that in mind, the rules should be usable as they are for almost all Gradle builds. If you encounter a problem with a rule in your build:

  1. Maybe there is a mistake/bug in this plugin. Please open an issue to discuss it.

  2. You might have a very special setup, where one of the rules causes trouble only in that setup.

In the second case, you cannot deactivate one of the rules in the plugin. But you can treat the modified metadata as if it was the original metadata and add another rule on top to modify it further or to revert the effect of the rule in this plugin. This can be expressed in a compact way if you also use the jvm-dependency-conflict-resolution plugin. For example:

jvmDependencyConflicts {
    patch.module("javax.xml.stream:stax-api") {
        // Additional rule to revert the effect of the plugin on 'javax.xml.stream:stax-api'
        removeCapability(CapabilityDefinition.STAX_API)
    }
}

If you do not want to use the jvm-dependency-conflict-resolution plugin, you can use Gradle’s general Component Metadata Rule API. For example like this:

dependencies {
    components.withModule("javax.xml.stream:stax-api") {
        // Additional rule to revert the effect of the plugin on 'javax.xml.stream:stax-api'
        allVariants {
            withCapabilities {
                removeCapability(
                    CapabilityDefinition.STAX_API.group,
                    CapabilityDefinition.STAX_API.capabilityName
                )
            }
        }
    }
}

Such additional rules are best added in the place where you applied this plugin (e.g. one of your convention plugins). The snippet above shows how to add a rule without putting it into a separate class. You can put it into a class (written in Java, Kotlin or Groovy) and use the @CacheableRule annotation for better performance. That’s how the rules in this plugin are implemented. Consult the Gradle documentation on Component Metadata Rules for more details.

Something seems to be missing

This plugin collects rules that universally apply in the Java ecosystem. That means, that the information this plugin adds would ideally be already published in the metadata of the corresponding components. The idea is that every Java project can apply this plugin to avoid certain 'dependency hell' situations. Even if the project does not use any of the components this plugin affects directly, transitive dependency might bring in components that cause conflicts.

At the moment this plugin is only covering a fraction of the components on Maven Central that miss capability information. If you encounter more cases, please…​

…​contribute!

If you use this plugin and think it is missing a rule for a well-known component (or that a rule is incomplete/wrong), please let us know by

Please make sure, you clearly state which Capability it is about and which Components provide the Capability.

I maintain a Component on Maven Central - How can I publish Capability information myself?

It would be great to see more components publishing capability information directly. If you wonder how you could do it, here is how:

Publishing with Gradle

Assuming the component you are publishing is org.ow2.asm:asm. You add the asm:asm capability as follows:

configurations {
    apiElements {
        outgoing {
            // keep default capability 'org.ow2.asm:asm'
            capability("${project.group}:${project.name}:${project.version}")
            // add 'asm:asm'
            capability("asm:asm:${project.version}")
        }
    }
    runtimeElements {
        outgoing {
            // keep default capability 'org.ow2.asm:asm'
            capability("${project.group}:${project.name}:${project.version}")
            // add 'asm:asm'
            capability("asm:asm:${project.version}")
        }
    }
}
Publishing with Maven

Assuming the component you are publishing is org.ow2.asm:asm. You add the asm:asm capability as follows:

<!-- do_not_remove: published-with-gradle-metadata -->

<build>
  <plugins>
    <plugin>
      <groupId>de.jjohannes</groupId>
      <artifactId>gradle-module-metadata-maven-plugin</artifactId>
      <version>0.3.0</version>
      <executions>
        <execution>
          <goals>
            <goal>gmm</goal>
          </goals>
        </execution>
      </executions>
      <configuration>
        <capabilities>
          <capability>
            <groupId>asm</groupId>
            <artifactId>asm</artifactId>
          </capability>
        </capabilities>
      </configuration>
    </plugin>
  </plugins>
</build>

JVM Dependency Conflict Resolution

Plugin dependency

Add this to the build file of your convention plugin’s build (e.g. build-logic/build.gradle(.kts) or buildSrc/build.gradle(.kts)).

dependencies {
    implementation("org.gradlex:jvm-dependency-conflict-resolution:2.1.2")
}

Apply the plugin

plugins {
    id("org.gradlex.jvm-dependency-conflict-resolution")
}

This plugin should be applied to all projects, ideally as part of your convention plugin.

If the jvm-dependency-conflict-detection plugin has not been explicitly applied as well, either as a settings or project plugin, it will be automatically applied (as project plugin).

What is the concrete effect of the plugin?

The plugin registers capability conflict resolutions for the capabilities added by the jvm-dependency-conflict-plugin. This enables a number of capability conflicts to be resolved automatically. For conflicts that…​

  • cannot be resolved automatically

  • should be resolved differently (than the default)

  • are not detected correctly due to incomplete metadata

…​this plugin offers the jvmDependencyConflicts DLS with the following sections.

jvmDependencyConflicts {
    conflictResolution {
        // Customize resolution of capability conflicts
        select(JAVAX_ACTIVATION_API, "com.sun.activation:jakarta.activation")
    }

    logging {
        // Customize resolution of conflicts for a certain logging setup
        enforceSlf4JSimple()
    }

    patch {
        // Patch or extend wrong metadata
        module("com.googlecode.json-simple:json-simple") { removeDependency("junit:junit") }
    }

    consistentResolution {
        // The runtime classpath of ':app' is always respected in version conflict detection and resolution
        providesVersions(":app")
    }
}

Customize resolution of capability conflicts

The plugin adds a default resolution strategy for each capability. Which in most cases means that Gradle will automatically pick the highest version of all components in conflict. This is to cover the cases where users just want things to work somehow. But it might not always be the right solution.

You can use the conflictResolution section of jvmDependencyConflicts to make an explicit selection for a given capability.

jvmDependencyConflicts {
    conflictResolution {
        // Explicitly select candidates for capabilities
        select(CapabilityDefinition.CGLIB, "cglib:cglib")
        select(CapabilityDefinition.JAVAX_MAIL_API, "com.sun.mail:jakarta.mail")
    }
}
ℹ️
You may also use selectLenient(). In a larger build, there can be situations where the desired selection is not always available. For example, if the compile classpath of a module does not contain all dependencies of the final application runtime classpath. In such cases, it may be okay and simplify things by just selecting one of the candidates, which selectLenient() does.

If you prefer to get a conflict reported without making a selection, you can also deactivate the default resolution strategy.

jvmDependencyConflicts {
    conflictResolution {
        // Deactivate default resolution strategy for selected rules
        deactivateResolutionStrategy(CapabilityDefinitions.CGLIB)
        deactivateResolutionStrategy(CapabilityDefinitions.JAVAX_MAIL_API)
    }
}

The following table lists all available functionality:

Method Documentation

deactivateResolutionStrategy(capability)

Deactivate the default resolution strategy for a given capability.

selectHighestVersion(capability)

Select the highest available version if there is a conflict on the given capability.

select(capability, module)

Select the given module if there is a conflict on the given capability. If the module is not part of the conflict: fail

selectLenient(capability, module)

Select the given module if there is a conflict on the given capability. If the module is not part of the conflict: select first module found

Select and enforce a logging framework

The logging section of jvmDependencyConflicts enables you to declaratively enforce the logging framework your application should use at build time.

💡
The different configuration options documented below do not add dependencies. Make sure to have the expected dependency in your graph, either as a direct or transitive one.

The logging { } section first provides a number of high-level, one stop solutions, for selecting a logging solution:

Method Documentation Required dependency

enforceLogback()
enforceLogback(String configurationName)

This will configure all capabilities to resolve in favour of LOGBack and route all alternative logging solutions through Slf4J.

ch.qos.logback:logback-classic

enforceLog4J2()
enforceLog4J2(String configurationName)

This will configure all capabilities to resolve in favour of Log4J 2 and route all alternative logging solutions through Log4J 2.

org.apache.logging.log4j:log4j-slf4j-impl

enforceSlf4JSimple()
enforceSlf4JSimple(String configurationName)

This will configure all capabilities to resolve in favour of Slf4J simple and route all alternative logging solutions through Slf4J.

org.slf4j:slf4j-simple

💡
The method without parameter will apply the setup to all dependency configuration, while the other one will limit the setup to the specified dependency configuration.

If you want a finer grained control, the logging { } section provides lower level entry points for solving the different logging capability conflicts:

Method Accepted parameter values Documentation

selectSlf4JBinding(Object notation)

Value must be an Slf4J binding implementation known by the plugin: org.slf4j:slf4j-simple, org.slf4j:slf4j-log4j12, org.slf4j:slf4j-jcl, org.slf4j:slf4j-jdk14, ch.qos.logback:logback-classic or org.apache.logging.log4j:log4j-slf4j-impl

Configures the provided Slf4J binding for selection, configuring related capabilities if needed

selectSlf4JBinding(String configurationName, Object notation)

A dependency configuration name, that canBeResolved=true
A notation as above

Configures the provided Slf4J binding for selection, configuring related capabilities if needed, only for the provided dependency configuration

selectLog4J12Implementation(Object notation)

Value must be a Log4J 1.2 implementation known by the plugin: org.slf4j:log4j-over-slf4j, org.apache.logging.log4j:log4j-1.2-api, log4:log4j or org.slf4j:slf4j-log4j12

Configures the provided Log4J 1.2 implementation for selection, configuring related capabilities if needed

selectLog4J12Implementation(String configurationName, Object notation)

A dependency configuration name, that canBeResolved=true
A notation as above

Configures the provided Log4J 1.2 implementation for selection, configuring related capabilities if needed, only for the provided dependency configuration

selectJulDelegation(Object notation)

Value must be a java.util.logging interceptor or binding known by the plugin: org.slf4j:jul-to-slf4j, org.slf4j:slf4j-jdk14 or org.apache.logging.log4j:log4j-jul

Configures the provided JUL integration of binding for selection, configuring related capabilities if needed

selectJulDelegation(String configurationName, Object notation)

A dependency configuration name, that canBeResolved=true
A notation as above

Configures the provided JUL integration for selection, configuring related capabilities if needed, only for the provided dependency configuration

selectJCLImplementation(Object notation)

Value must be a Apache Commons Logging interceptor or binding known by the plugin: org.slf4j:jcl-over-slf4j, commons-logging:commons-logging, org.slf4j:slf4j-jcl or org.apache.logging.log4j:log4j-jcl

Configures the provided commons logging interceptor or binding for selection, configuring related capabilities if needed

selectJCLImplementation(String configurationName, Object notation)

A dependency configuration name, that canBeResolved=true
A notation as above

Configures the provided commons logging interceptor or binding for selection, configuring related capabilities if needed, only for the provided dependency configuration

selectSlf4JLog4J2Interaction(Object notation)

Value must be a Log4J 2 module for Slf4J interaction known by the plugin: org.apache.logging.log4j:log4j-to-slf4j or org.apache.logging.log4j:log4j-slf4j-impl

Configures the Log4J 2 / Slf4J integration, configuring related capabilities if needed

selectSlf4JLog4J2Interaction(Sting configurationName, Object notation)

A dependency configuration name, that canBeResolved=true
A notation as above

Configures the Log4J 2 / Slf4J integration, configuring related capabilities if needed, only for the provided dependency configuration

💡
Notations above are those accepted by DependencyHandler.create(notation) in Gradle that resolves to an ExternalDependency. Most often this is a group:name:version String.

Patch metadata of published components

The patch section of jvmDependencyConflicts enables you to do individual adjustments to the metadata of published components. This can be done to add information the jvm-dependency-conflict-detection plugin does not yet cover or to make opinionated adjustments for your context. In the case of generally applicable adjustments, like adding a capability, please consider contributing your discovery back to the plugin by creating a PR.

jvmDependencyConflicts {
    patch {
         // patch metadata of the given module
        module("io.netty:netty-common") {
            // required adjustments (see table below)
        }
        // align versions (through BOM)
        alignWithBom("org.ow2.asm:asm-bom", "org.ow2.asm:asm", "org.ow2.asm:asm-util")
        // align versions (without using a BOM)
        align("org.ow2.asm:asm", "org.ow2.asm:asm-util")
    }
}
Method Documentation

addApiDependency(dependency)

Add a dependency in 'api' scope (visible at runtime and compile time).

addRuntimeOnlyDependency(dependency)

Add a dependency in 'runtimeOnly' scope (visible at runtime).

addCompileOnlyApiDependency(dependency)

Add a dependency in 'compileOnlyApi' scope (visible at compile time).

removeDependency(dependency)

Remove the given dependency from all scopes.

reduceToRuntimeOnlyDependency(dependency)

Reduce the given 'api' dependency to 'runtimeOnly' scope.

reduceToCompileOnlyApiDependency(dependency)

Reduce the given 'api' dependency to 'compileOnlyApi' scope.

addCapability(capability)

Add a capability.

removeCapability(capability)

Remove a capability.

addFeature(classifier)

Make the Jar with the give 'classifier' known as Feature Variant so that it can be selected via capability in a dependency declaration.

addTargetPlatformVariant(classifier, os, arch)

Make the Jar with the give 'classifier' known as additional variant with the OperatingSystemFamily and MachineArchitecture attributes set.

setStatusToIntegration(markerInVersion)

Set the status of pre-release versions that are identified by one of the marker string (e.g. -rc, -m) to integration (will then not be considered when using latest.release as version).

Configure global consistent resolution

The consistentResolution section of jvmDependencyConflicts allows to configure consistent resolution for all modules (subprojects) of your build. By configuring which projects aggregate the final software product (applications or services that are delivered) you make sure that the same versions of all third party dependencies you deliver as part of your product are also used when compiling and testing parts of your softare (single subprojects) in isolation.

jvmDependencyConflicts {
    consistentResolution {
        // The runtime classpaths of the configured projects are always respected in
        // version conflict detection and resolution
        providesVersions(":app")
        providesVersions(":service")
        // If the build has a platform project, use it for additional version information
        platform(":versions")
    }
}
Method Documentation

providesVersions(project)

Respect runtime classpaths of given project in all version conflict detection and resolution.

platform(project)

A platform/BOM to provide versions not available through consistent resolution alone.

Appendix

Appendix A: All Capabilities

The following list shows all capabilities and the components they are added to. Most capabilities use org.gradlex as group and the name of the Component that first introduced the capability. For capabilities that already exists because they are mentioned in published metadata (like com.google.collections:google-collections) the official capability groups and names are used.

Appendix B: Capabilities for logging

The following lists the capabilities important for the logging topic (see logging DSL block).

Capability Impacted modules Comment

slf4j-impl

org.slf4j:slf4j-simple, org.slf4j:slf4j-log4j12, org.slf4j:slf4j-jcl, org.slf4j:slf4j-jdk14, ch.qos.logback:logback-classic, org.apache.logging.log4j:log4j-slf4j-impl, org.apache.logging.log4j:log4j-slf4j2-impl, org.slf4j:slf4j-nop

Represents an Slf4J binding

log4j2-impl

org.apache.logging.log4j:log4j-to-slf4j, org.apache.logging.log4j:log4j-core

Represents the native Log4J 2 implementation or delegation to Slf4J

log4j2-vs-slf4j

org.apache.logging.log4j:log4j-slf4j-impl, org.apache.logging.log4j:log4j-to-slf4j

Represents the Slf4J / Log4J 2 relationship: which one delegates to the other

slf4j-vs-log4j

org.slf4j:log4j-over-slf4j, org.slf4j:slf4j-log4j12

Represents the Slf4J / Log4J 1.2 relationship: either Slf4J intercepts or binds to Log4J

slf4j-vs-log4j2-log4j

org.slf4j:log4j-over-slf4j, org.apache.logging.log4j:log4j-1.2-api, log4j:log4j

Represents the available Log4J implementation: native, with Slf4J or with Log4J 2

slf4j-vs-jul

org.slf4j:jul-to-slf4j, org.slf4j:slf4j-jdk14

Represents the Slf4J / java.util.logging relationship: either Slf4 intercepts or binds to JUL

slf4j-vs-log4j2-jul

org.slf4j:jul-to-slf4j, org.apache.logging.log4j:log4j-jul

Represents JUL replacement: either with Slf4J or with Log4J 2

commons-logging-impl

commons-logging:commons-logging, org.slf4j:jcl-over-slf4j, org.springframework:spring-jcl

Represents Apache Commons Logging implementation: native or Slf4J

slf4j-vs-jcl

org.slf4j:jcl-over-slf4j, org.slf4j:slf4j-jcl

Represents the Slf4J / Apache Commons Logging relationship: either Slf4J intercepts or binds to commons-logging

slf4j-vs-log4j2-jcl

org.slf4j:jcl-over-slf4j, org.apache.logging.log4j:log4j-jcl

Represents the Slf4J or Log4J 2 interception of commons-logging

Appendix C: Alignment

In addition to the capability setting and conflict detection, the plugin also registers alignment rules for Slf4J and Log4J 2 modules.