Something fishy with transitive dependencies

I’ve been struggling with this for a few hours and it has just got me stumped how transitive dependency resolution is working in Gradle.

I am in the process of converting a project from Maven to Gradle. The end result is to generate a .war with the same versions as the Maven build. Simple enough, right?

Here is the problem that has me stumped. I need to remove ‘org.apache.geronimo.specs:geronimo-javamail_1.4_spec:1.7.1’ from the runtime configuration. However, I have two cxf libraries which declare it as a dependency and the seem to exclusions fail.

So my dependencies declared like this:

dependencies {
 compile(group: 'org.apache.cxf', name: 'cxf-rt-core', version: '2.6.10')
}

The runtime dependency tree looks like this:

runtime - Runtime classpath for source set 'main'.
+--- org.apache.cxf:cxf-rt-core:2.6.10
|
  +--- org.apache.cxf:cxf-api:2.6.10
|
  |
  +--- org.apache.geronimo.specs:geronimo-javamail_1.4_spec:1.7.1
|
  |
  \--- wsdl4j:wsdl4j:1.6.3
|
  +--- org.apache.ws.xmlschema:xmlschema-core:2.0.3
|
  \--- org.apache.geronimo.specs:geronimo-javamail_1.4_spec:1.7.1

When I declare an exclusion in my dependencies:

dependencies {
 compile(group: 'org.apache.cxf', name: 'cxf-rt-core', version: '2.6.10') {
  exclude(module: 'geronimo-javamail_1.4_spec')
 }
}

All is good:

runtime - Runtime classpath for source set 'main'.
+--- org.apache.cxf:cxf-rt-core:2.6.10
|
  +--- org.apache.cxf:cxf-api:2.6.10
|
  |
  \--- wsdl4j:wsdl4j:1.6.3
|
  \--- org.apache.ws.xmlschema:xmlschema-core:2.0.3

I don’t run into problems until I add an additional cxf dependency.

dependencies {
 compile(group: 'org.apache.cxf', name: 'cxf-rt-core', version: '2.6.10') {
  exclude(module: 'geronimo-javamail_1.4_spec')
 }
 compile(group: 'org.apache.cxf', name: 'cxf-rt-frontend-jaxws', version: '2.6.10')
}

The dependency tree now looks like this:

runtime - Runtime classpath for source set 'main'.
+--- org.apache.cxf:cxf-rt-core:2.6.10
|
  +--- org.apache.cxf:cxf-api:2.6.10
|
  |
  +--- org.apache.geronimo.specs:geronimo-javamail_1.4_spec:1.7.1
|
  |
  \--- wsdl4j:wsdl4j:1.6.3
|
  +--- org.apache.ws.xmlschema:xmlschema-core:2.0.3
|
  \--- org.apache.geronimo.specs:geronimo-javamail_1.4_spec:1.7.1
+--- org.apache.cxf:cxf-rt-frontend-jaxws:2.6.10
|
  +--- org.apache.cxf:cxf-api:2.6.10 (*)
|
  +--- org.apache.cxf:cxf-rt-core:2.6.10 (*)
|
  +--- org.apache.cxf:cxf-rt-bindings-soap:2.6.10

I’m confused by the report. Since my exclusion is on the ‘cxf-rt-core’, it seems like the report is incorrect or at least somewhat deceiving in helping me track down my problem. However, I just want to solve my issue, so I’ll ignore the where the report identifies the dependency on ‘geronimo’.

Since both cxf-rt-core’ in ‘cxf-rt-frontend-jaws’ declare a dependency on 'cxf-api, it is unclear whether the dependency on ‘geronimo’ is coming from ‘cxf-rt-core’ or ‘cxf-rt-frontend-jaws’. So as a test, I intentionally exclude ‘cxf-api’ from both dependencies.

dependencies {
 compile(group: 'org.apache.cxf', name: 'cxf-rt-core', version: '2.6.10') {
  exclude(module: 'geronimo-javamail_1.4_spec')
  exclude(module: 'cxf-api')
 }
 compile(group: 'org.apache.cxf', name: 'cxf-rt-frontend-jaxws', version: '2.6.10') {
  exclude(module: 'cxf-api')
        }
}

The dependency tree now looks like this:

runtime - Runtime classpath for source set 'main'.
+--- org.apache.cxf:cxf-rt-core:2.6.10
|
  +--- org.apache.ws.xmlschema:xmlschema-core:2.0.3
|
  \--- org.apache.geronimo.specs:geronimo-javamail_1.4_spec:1.7.1
+--- org.apache.cxf:cxf-rt-frontend-jaxws:2.6.10
|
  +--- org.apache.cxf:cxf-rt-core:2.6.10 (*)
|
  +--- org.apache.cxf:cxf-rt-bindings-soap:2.6.10

I’m not sure what I’m missing here, but this seems fishy to me. I would expect the dependency for ‘cxf-rt-core’ explicitly declared in my dependencies to take precedence over the transitive dependency ‘cxf-rt-core’ declared by ‘cxf-rt-frontend-jaxws’.

Just to make sure that there was not yet another dependency somewhere in ‘cxf-rt-frontend-jaxws’, I excluded the ‘cxf-rt-core’.

compile(group: 'org.apache.cxf', name: 'cxf-rt-core', version: '2.6.10') {
  exclude(module: 'geronimo-javamail_1.4_spec')
  exclude(module: 'cxf-api')
 }
 compile(group: 'org.apache.cxf', name: 'cxf-rt-frontend-jaxws', version: '2.6.10') {
  exclude(module: 'cxf-api')
  exclude(module: 'cxf-rt-core')
 }

The dependency tree now looks like what I would expect:

runtime - Runtime classpath for source set 'main'.
+--- org.apache.cxf:cxf-rt-core:2.6.10
|
  \--- org.apache.ws.xmlschema:xmlschema-core:2.0.3
+--- org.apache.cxf:cxf-rt-frontend-jaxws:2.6.10
|
  +--- org.apache.cxf:cxf-rt-bindings-soap:2.6.10

Here is the question: How the reconciliation of attributes resolved when the dependency with identical version numbers occurs multiple times in the dependency tree? Do I need to insure that each dependency only occurs once within the dependency tree if I declare any additional attributes on a dependency? Which dependency is actually used? The first one declared? The last one declared? The nearest dependency?

As far as I know, a per-dependency exclude only affects the subtree of that direct dependency. In most cases, it’s more useful to exclude a dependency from a whole configuration (‘configurations.compile { exclude module: ‘geronimo-javamail_1.4_spec’ }’), or even from all configurations (‘configurations.all { exclude module: ‘geronimo-javamail_1.4_spec’ }’).

Conceptually I understand that Gradle recommends you declare exclusions on a per-configuration basis. In fact, the Gradle documentation in 50.4.7. Excluding transitive dependencies states:

Should you exclude per-dependency or per-configuration? It turns out that in majority of cases you want to use the per-configuration exclusion.

However, dependencies exclusions are transitive and configuration exclusions are not. In other words, if the right answer is to declare the exclusion in the configuration, I can only do those in the final consumer of a project rather than the project itself where I think it belongs.

However, dependencies exclusions are transitive and configuration exclusions are not.

How did you come to this conclusion? Both should be transitive.

You are right. Please excuse the error.

I was observing the dependency tree of the end state war project and a core library project. What I failed to realize was a very similarly-named, intermediate project between the two. The intermediate project was also including some cxf dependencies and therefore pulling in the geronimo dependencies.

I ran further tests and confirmed your statement. The configuration is in fact transitive.

THANKS!