Gradle能否以任何方式帮助解决jar包地狱问题?

7

这里是Java 8。

假设旧版本的widget库的Maven坐标为widgetmakers:widget:1.0.4,其中定义了以下类:

public class Widget {
    private String meow;

    // constructor, getters, setters, etc.
}

多年过去了。这个 widget 库的维护者决定一个 Widget 不应该 meow,而是应该 bark。因此,发布了一个新版本,它的 Maven 坐标为 widgetmakers:widget:2.0.0 并且 Widget 的样子如下:

public class Widget {
    private Bark bark;

    // constructor, getters, setters, etc.
}

现在我要构建我的应用程序myapp。为了使用所有依赖项的最新稳定版本,我在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'
    )
}

现在假设这个(虚构的)fizzbuzz库总是依赖于widget库的1.x版本,其中Widgetmeow
现在,我在编译classpath上指定了2个版本的widget
1. widgetmakers:widget:1.0.4fizzbuzz库作为其依赖项拉入;以及 2. 我直接引用的widgetmakers:widget:2.0.0 显然,取决于哪个版本的Widget被首先加载,我们将得到一个Widget#meow或一个Widget#bark
Gradle是否提供任何帮助我解决这个问题的工具?有没有办法拉入同一类的多个版本,并配置fizzbuzz类使用旧版本的Widget,而我的类使用新版本呢?如果没有,我能想到的唯一解决方案是:
1. 我可以通过某种阴影和/或fatjar-based的解决方案来实现,例如将所有依赖项作为myapp/bin下的包拉入,然后给它们不同的版本前缀。尽管我没有找到明确的解决方案,但我确定有可行的解决方案(但完全是hacky/nasty的)。或者... 2. 仔细检查我的整个依赖图,并确保所有传递依赖项不会发生冲突。对我来说,这意味着要么向fizzbuzz维护者提交一个拉取请求,将其升级到最新的widget版本,要么,可悲的是,降级myapp以使用旧的widget版本。
但Gradle(到目前为止)对我来说一直是神奇的。所以我问:有没有Gradle魔法可以帮助我?

我唯一能想到的正确使用同一库的两个不同版本的方法是在需要时自己或库引用每个版本。如果不这样做,那么你将不得不依赖于Gradle提供的版本。如果是错误的版本,你也会遇到类/方法未找到异常。Gradle可以找到缺失的依赖项并拉取它们,但通过混淆2个库来手动破坏Gradle的依赖关系解析机制在长期内不会有任何好处。 - We are Borg
感谢@WeareBorg (+1)。**(1)** 当你说“在必要的时候,由你或库引用每个版本”时,你能详细解释一下吗?我已经是JVM开发者10多年了,对这种策略/方法不熟悉!你能举个例子说明一下你的意思吗?还有**(2)**,在这种情况下Gradle的默认行为是什么?如果不改变,Gradle最终会检索/解析哪个版本的widget库?再次感谢! - smeeb
1
  1. 我的意思很简单,但我知道Maven的语法,你可以排除那些你不需要的特定版本的库,这样其他版本就会自动被拉取。
  2. 对于Gradle来说,因为它会添加classpath,所以它将是列表中的第一个;对于Maven来说,它将是最新的一个。
- We are Borg
1
Maven Shade插件可以帮助解决这种情况,尽管它也有其局限性。但在这些情况下的基本问题是库作者没有遵守公共API中所做的承诺,没有技术解决方案可以解决这个问题。 - biziclop
4个回答

7

我不太了解Gradle的具体情况,因为我更熟悉Maven,但是这个问题比较普遍。你基本上有两个选项(而且都很麻烦):

  1. 类加载器魔法。在某种程度上,你需要说服构建系统加载库的两个版本(祝你好运),然后在运行时使用一个具有旧版本的类加载器来加载使用旧版本的类。我做过这个,但是非常麻烦。(像OSGI这样的工具可能会减轻一些痛苦)
  2. 包阴影。重新打包使用旧版本库B的库A,使得B实际上在A内部,但是有一个特定于B的包前缀。这是常见的做法,例如Spring发布了自己的asm版本。在Maven方面,maven-shade-plugin可以做到这一点,可能还有一个Gradle的等效版本。或者你可以使用Jar操作的800磅大猩猩ProGuard

之前已经走过所有这些路,所以我肯定会先尝试阴影方法,如果不行,就尝试 OSGI,最后才考虑手动类加载器魔法作为最后的办法。 - biziclop
@biziclop 是的,我同意那是最不痛苦的方式。 - Sean Patrick Floyd
似乎是解决棘手问题的最佳答案。 - cjstehno

2
Gradle只会使用您的依赖项设置类路径,它不提供自己的运行时来封装依赖项及其传递依赖项。在运行时活动的版本将根据类加载规则来确定,我认为是类路径顺序中包含该类的第一个jar文件的版本。OSGI提供了可以处理此类情况的运行时环境,即将要推出的模块系统也是如此。
编辑:Bjorn是正确的,它将尝试解决不同版本之间的冲突;它将基于其策略编译类路径,因此您在文件中放置依赖项的顺序并不重要。但是,仍然只能得到一个类名对应的类,它无法解决OP的问题。

1
这是错误的。Gradle将根据所选择的冲突解决策略来解决冲突。默认情况下,它将使用所有版本请求的最新版本,除非您以其他方式配置Gradle,例如出错或使用特定的固定版本等。只要坐标相等(除了版本),它就不会将两个版本都添加到类路径中。 - Vampire

0

最糟糕的情况是相同的类出现在多个JAR文件中。这更具有欺骗性——比较Codahale和Dropwizard的度量JAR文件,这两个JAR文件中的相同类的版本不兼容。 Gradle的classpath-hell插件可以检测到这种恐怖。


0
如果您有不同版本的库,但坐标相等,则Gradle的冲突解决机制会发挥作用。
默认的解决策略是使用所请求的最新版本的库。在您的依赖图中,您将不会得到多个相同库的版本。
如果您真的需要运行时不同版本的同一库,则必须执行一些ClassLoader魔术(这肯定是可能的),或者为其中一个库或两个库进行一些阴影处理。
关于冲突解决,Gradle内置了最新策略和失败策略。后者在依赖图中存在不同版本并且您必须明确解决版本冲突时会失败。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接