June 09, 2017

Gradle Dependency Resolution Between Java 7 and Java 8 Artifacts (Dependencies with Classifier and No classifier)

CCC has a mix of systems built with Java 7 and 8. In order to allow systems to migrate over time, to handle the case where a Java 8 system produces a client library that must be consumed by a Java 7 system, we used to produce Java 7 artifact jars (1.7 Compatibility Mode) along with a Java 8 differentiated by classifier - jdk7, as most of us do, and these artifacts will be published to our Artifactory.

Let’s say a project foo with version - 1.0.0 will produce artifacts

foo-1.0.0.jar
foo-1.0.0-jdk7.jar
ivy-1.0.1.xml

and coming to the consuming side: let’s say, project - bar building on Java 8, will consume the above mentioned dependencies with usual practice (with no classifier),

  compile "com.copyright.foo:foo:1.0.0"

and any project, say baz, building on Java 7 consumes them with classifier - jdk7 along with group, name and version.

  compile "com.copyright.foo:foo:1.0.0:jdk7"

Refer to gradle dependency with classifier for usage details.

But we came across a problem, when building a Java 7 project which got dependencies com.copyright.foo:foo:1.0.0:jdk7 as direct and com.copyright.foo:foo:1.0.0 as transitive.

We are finding the consuming project ended up having both

foo:1.0.0:jdk7
foo:1.0.0

in the dependency graph / classpath, where Gradle fails to resolve between Ivy dependencies with and without classifier. This will cause applications to encounter problems like

java.lang.IncompatibleClassChangeError: class net.sf.cglib.core.DebuggingClassWriter has interface org.objectweb.asm.ClassVisitor

or

org.springframework.core.NestedIOException: ASM ClassReader failed to parse class file - probably due to a new Java class file version that isn't supported yet: 

or (during test executions using EasyMock)

java.lang.NoClassDefFoundError: Could not initialize class org.easymock.internal.ClassProxyFactory$2

because class readers are failing to parse class file due to a new Java class file version (Java 8). Gradle dependencySubstitution or resolution strategy - force (with classifier) will not work, at least with Gradle 2.x and 3.x, because of a known limitation - Dependency resolve rule can set/change classifier attribute

With the current limitations with Gradle, we decided to avoid classifier and here is the approach we have been following.

We have been using Artifactory for publishing our build artifacts. And we have a repository

rup-releases for all our release artifacts.

Now, we introduced a new repository

rup-release-alternate for publishing our alternate release artifacts.

Alternate artifacts here mean Java 7 artifacts in the case of Java 8 builds (and may be Java 8 artifacts during Java 9 builds). So, main artifacts (Java 8 artifacts for Java 8 builds, Java 7 artifacts for Java 7 builds) will continue to publish to rup-release.

Make note, here both main and alternate artifacts will publish with the same artifact group, name and version (and no classifiers in any cases). In the example above,

com.copyright.foo:foo:1.0.0

will be published to rup-release (Java 8 artifact) and also published to rup-release-alternate (Java 7 artifact).

Technically, Java 8 projects will publish artifacts to rup-release and rup-release-alternate and Java 7 projects will consume artifacts from rup-release and rup-release-alternate.

But now the question to be answered is how will this approach resolve the issue reported above? The magic here is, order of repositories,

repositories {
   ivy('../rup-release-alternate')
   ivy('../rup-release')
}

If we look at one of the principles for Gradle / Maven that, if we have a specific artifact (with the same group, name and version) existing in more than one repository, then the build tools will pick up the artifact from first repository. And we envision, this solution will support the Java 8 to 9 transition as well where Java 8 artifacts will be published to rup-release-alternate when building Java 9 projects.

So in the example above, the artifact com.copyright.foo:foo:1.0.0 will get resolved from rup-release-alternate as this is the first repository in the list and which is our jdk7 artifacts.

References:

Mohan Kornipati

Software Architect