7

Java 8 here.

Say there is an old version of the widget libray, with Maven coordinates widgetmakers:widget:1.0.4, that has a class defined in it like so:

public class Widget {
    private String meow;

    // constructor, getters, setters, etc.
}

Years pass. The maintainers of this widget library decide that a Widget should never meow, rather, that it should in fact bark. And so a new release is made, with Maven coordinates widgetmakers:widget:2.0.0 and with Widget looking like:

public class Widget {
    private Bark bark;

    // constructor, getters, setters, etc.
}

So now I go to build my app, myapp. And, wanting to use the latest stable versions of all my dependencies, I declare my dependencies like so (inside of build.gradle):

dependencies {
    compile (
        ,'org.slf4j:slf4j-api:1.7.20'
        ,'org.slf4j:slf4j-simple:1.7.20'
        ,'bupo:fizzbuzz:3.7.14'
        ,'commons-cli:commons-cli:1.2'
        ,'widgetmakers:widget:2.0.0'
    )
}

Now let's say that this (fictional) fizzbuzz library has always depended on a 1.x version of the widget library, where Widget would meow.

So now, I'm specifying 2 versions of widget on my compile classpath:

  1. widgetmakers:widget:1.0.4 which is pulled in by the fizzbuzz library, as a dependency of it; and
  2. widgetmakers:widget:2.0.0 which I am referencing directly

So obviously, depending on which version of Widget gets classloaded first, we will either have a Widget#meow or a Widget#bark.

Does Gradle provide any facilities for helping me out here? Is there any way to pull in multiple versions of the same class, and configure fizzbuzz classes to use the old version of Widget, and my classes to use the new version? If not, the only solutions I can think of are:

  1. I might be able to accomplish some kind of shading- and/or fatjar-based soltuion, where perhaps I pull in all my dependencies as packages under myapp/bin and then give them different version-prefixes. Admittedly I don't see a clear solution here, but am sure something is feasible (yet totally hacky/nasty). Or...
  2. Carefully inspect my entire dependency graph and just make sure that all of my transitive dependencies don't conflict with each other. In this case for me, this means either submitting a pull-request to the fizzbuzz maintainers to upgrade it to the latest widget version, or, sadly, downgrading myapp to use the older widget version.

But Gradle (so far) has been magic for me. So I ask: is there any Gradle magic that can avail me here?

smeeb
  • 27,777
  • 57
  • 250
  • 447
  • The only possible way I see you can properly use two different versions of the same library is through referring each version wherever required by yourself or by the library. If not, then you will have to depend on the version Gradle would provide. Also you will get a class/method not found exception if it's the wrong version. Gradle can find dependencies which are missing and pull them, but manually sabotaging the Gradle's dependency resolution mechanism by confusing it with 2 libraries is not gonna do any good in the long run. – We are Borg Apr 29 '16 at 14:07
  • Thanks @WeareBorg (+1). **(1)** When you say "*referring each version wherever required by yourself or by the library*", can you elaborate a bit more? I've been a JVM dev for 10+ years and am not familiar with this strategy/approach! Can you give an example of what you mean? And **(2)** what is Gradle's default behavior in this situation? Left untouched, which version of the `widget` library will Gradle ultimately retrieve/resolve? Thanks again! – smeeb Apr 29 '16 at 14:11
  • 1
    1) I meant something simple, but I know syntax for maven, where you can exclude the library which you are not going to need with a specific version, so the other version is pulled automatically.. 2) For gradle because of classpath additions, it will be the first in list, for maven, it will be the latest one. – We are Borg Apr 29 '16 at 14:25
  • 1
    Maven Shade plugin can help resolve situations like this, though it has its limitations too. But the basic problem in these situations is library authors not honouring their promises made in the public API and no technical solution can resolve that. – biziclop Apr 29 '16 at 14:26

4 Answers4

7

Don't know the specifics of Gradle, as I'm a Maven person, but this is more generic anyway. You basically have two options (and both are hacky):

  1. ClassLoader magic. Somehow, you need to convince your build system to load two versions of the library (good luck with that), then at runtime, load the classes that use the old version with a ClassLoader that has the old version. I have done this, but it's a pain. (Tools like OSGI may take away some of this pain)
  2. Package shading. Repackage the library A that uses the old version of library B, so that B is actually inside A, but with a B-specific package prefix. This is common practice, e.g. Spring ships its own version of asm. On the Maven side, the maven-shade-plugin does this, there probably is a Gradle equivalent. Or you can use ProGuard, the 800 pound gorilla of Jar manipulation.
Sean Patrick Floyd
  • 292,901
  • 67
  • 465
  • 588
2

Gradle will only set up the classpath with your dependencies, it doesn't provide its own runtime to encapsulate dependencies and its transitive dependencies. The version active at runtime will be the one according to the classloading rules, which I believe is the first jar in the classpath order to contain the class. OSGI provides runtime that can deal with situations like this and so will the upcoming module system.

EDIT: Bjorn is right in that it will try to resolve conflicts in different versions; it'll compile the classpath based on its strategies, so the order you put your dependencies in the file doesn't matter. However you still only get one class per classname, it won't resolve OP's issue

Lev Kuznetsov
  • 3,520
  • 5
  • 20
  • 33
  • 1
    This is wrong. Gradle will resolve the conflict based on the chosen conflict resolution strategy. By default it will use the newest version of all version requested unless you configure Gradle in another way e. g. to error out or to use a specific fixed version or something like that. It will not add both versions to the classpath as long as the coordinates are equal except of the version. – Vampire Apr 29 '16 at 14:50
0

If you have different versions of a library with otherwise equal coordinates, Gradles conflict resolution mechanism comes into play.

The default resolution strategy is to use the newest requested version of the library. You will not get multiple versions of the same library in your dependendcy graph.

If you really need different versions of the same library at runtime you would have to either do some ClassLoader magic which definitely is possible or do some shading for one of the libraries or both.

Regarding conflict resolution, Gradle has built-in the newest strategy that is default and a fail strategy that fails if different versions are in the dependency graph and you have to explicitly resolve version conflicts in your build files.

Vampire
  • 35,631
  • 4
  • 76
  • 102
0

Worse case is when the same class appears in multiple jars. This is more insidious - look at the metrics jars from Codahale and Dropwizard with incompatible versions of the same class in the two jars. The gradle classpath-hell plugin can detect this horror.

John Lonergan
  • 305
  • 3
  • 6