26

我正在努力解决如何处理 jar 依赖地狱。我有一个使用一些 aws sdk 的 Maven-IntelliJ Scala 项目。最近添加的 kinesis sdk 引入了不兼容的 Jackson 版本。

我的问题是:我如何系统地处理 Jar hell 的问题?

我了解类加载器以及 maven 如何在重复的 Jars 之间进行选择,但是对于解决该问题的实际实际步骤,我仍然一头雾水。

我目前的尝试是基于反复试验,我在这里用杰克逊的例子来概述:

  • 首先,我看到 Jackson 数据绑定 ObjectMapper 类上的实际异常是什么,在本例中为 NoSuchMethodError。然后,我查看 Jackson 文档以查看添加或删除该方法的时间。这通常很乏味,因为我手动检查每个版本的 api 文档(问题 1:有更好的方法吗?)
  • 然后,我 mvn dependency:tree用来确定我实际使用的是哪个版本的 Jackson (问题 2:是否有一种自动方式询问 maven 正在使用哪个版本的 jar,而不是梳理树输出?)
  • 最后,我比较了 mvn dependency:tree添加 Kinesis SDK 之前和之后的输出,以检测 mvn dependency:tree输出中的差异,并希望查看 Jackson 版本是否发生了变化。(问题 3:当依赖解析发生时,maven 如何使用阴影 jar 中的库?和其他一样吗?)

最后,在比较树输出之后,我尝试在 POM 中显式添加 Jackson 的最新工作版本,以触发 maven 依赖解析链中的优先级。如果最新的不起作用,我添加下一个最新的库,依此类推。

整个过程非常繁琐。除了我提出的具体问题外,我也很好奇其他人对这个问题的系统方法。有没有人有他们使用的任何资源?

4

4 回答 4

23

然后,我查看 Jackson 文档以查看添加或删除该方法的时间。这通常很乏味,因为我手动检查每个版本的 api 文档(问题 1:有更好的方法吗?)

要检查 API(破坏)兼容性,有几个工具可以自动分析 jar 并为您提供正确的信息。在这篇Stack Overflow 帖子中,有一些方便的工具很好的提示。
JAPICC似乎相当不错。

然后,我 mvn dependency:tree用来确定我实际使用的是哪个版本的 Jackson(问题 2:是否有一种自动方式询问 maven 正在使用哪个版本的 jar,而不是梳理树输出?)

maven-dependency-tree绝对是要走的路,但是您可以从一开始就过滤掉范围,只得到您真正想要的东西,使用它的includes选项如下:

mvn dependency:tree -Dincludes=<groupId>

注意:您还可以includes在表单中为选项提供更多信息groupId:artifactId:type:version或使用通配符,如*:artifactId.

这似乎是一个小提示,但在具有许多依赖项的大型项目中,缩小其输出范围是有很大帮助的。通常,简单地groupId作为过滤器就足够了,*:artifactId但如果您正在寻找特定的依赖项,这可能是最快的。

如果您对也按字母顺序排列的依赖项列表(不是树)感兴趣(在许多情况下非常方便),那么以下内容也可能会有所帮助:

mvn dependency:list -Dsort=true -DincludeGroupIds=groupId

问题3:当依赖解析发生时,maven如何使用阴影jar中的库?和其他的一样吗?

通过带阴影的罐子,您可能意味着:

  • fat jars,这也将其他罐子带入类路径。在这种情况下,它们被视为一个依赖项,是Maven 依赖中介的一个单元,其内容将成为项目类路径的一部分。通常,您不应该将 fat-jars 作为依赖项的一部分,因为您无法控制它带来的打包库。
  • 带有阴影(重命名)包的罐子。在这种情况下 - 再次 - 就 Maven 依赖中介而言没有控制:它是一个单元,一个 jar,基于其 GAVC(GroupId,ArtifactId,Version,Classifier),这使其独一无二。然后将其内容添加到项目类路径中(根据依赖范围,但由于它的包已重命名,您可能会遇到难以处理的冲突。同样,您不应该将重命名包作为项目依赖项的一部分(但通常你不能知道)。

有没有人有他们使用的任何资源?

一般来说,您应该很好地理解 Maven 如何处理依赖关系并使用它提供的资源(它的工具和机制)。下面是一些重要的点:

  • dependencyManagement绝对是这个话题的切入点:在这里你可以处理 Maven 依赖中介,影响它对传递依赖、它们的版本、它们的范围的决定。重要的一点是:您添加的dependencyManagement内容不会自动添加为依赖项。仅当项目的某个依赖项(如在文件中声明或通过传递依赖项声明)与其条目之一匹配dependencyManagement时才考虑,否则将被简单地忽略。pom.xml这是一个重要的部分,pom.xml因为它有助于管理依赖关系及其传递图,这就是为什么经常在父 pom 中使用:您只想处理一个并以集中方式处理哪个版本,例如,log4j你想在你所有的 Maven 项目中使用它,你在一个公共/共享的父 pom 中声明它,dependencyManagement并确保它会被这样使用。集中化意味着更好的治理和更好的维护。
  • dependency部分对于声明依赖项很重要:通常,您应该只在此处声明您需要的直接依赖项。一个很好的重击规则是:在此处声明为compile(默认)范围,仅在您的代码中实际使用 asimport语句(但您通常需要超出此范围,例如,运行时需要的 JDBC 驱动程序并且从未在您的代码中引用,它然后将在runtime范围内)。还要记住:声明的顺序很重要:第一个声明的依赖在与传递依赖冲突的情况下获胜,因此通过明确地重新声明依赖,您可以有效地影响依赖中介。
  • 不要滥用exclusionsin 依赖关系来处理传递依赖关系:如果可以的话,使用dependencyManagement和 order of 。dependencies滥用exclusions使维护变得更加困难,只有在确实需要时才使用它。此外,在添加时exclusions总是添加一个 XML 注释来解释原因:你的队友或/和你未来的自己会感激的。
  • 慎重使用依赖scope关系。将默认 ( compile) 范围用于编译和测试真正需要的内容(例如loga4j),test仅(且仅)用于测试中使用的内容(例如junit),注意provided目标容器已经提供的范围(例如servlet-api),runtime仅将作用域用于您在运行时需要的内容,但您永远不应该使用它进行编译(例如 JDBC 驱动程序)。不要使用system范围,因为它只会带来麻烦(例如,它没有与您的最终工件打包在一起)。
  • 不要玩版本范围,除非出于特殊原因,并且要知道指定的版本是默认的最低要求,[<version>]表达式是最强的,但你很少需要它。
  • 使用 Maven作为库系列元素的property占位符,version以确保您有一个集中的位置来对一组具有相同版本值的依赖项进行版本控制。一个经典的例子是用于多个依赖项的spring.versionor属性。hibernate.version同样,集中化意味着更好的治理和维护,这也意味着更少的头痛和更少的地狱
  • 提供时,导入 BOM作为上述要点的替代方案,并更好地处理依赖关系系列(例如jbosspom.xml ),将特定依赖关系集的管理委托给另一个文件。
  • 不要(ab)使用SNAPSHOT依赖项(或尽可能少)。如果您确实需要,请确保您永远不要使用SNAPSHOT依赖项发布:否则构建可重复性将处于高度危险之中。
  • 在进行故障排除时,请始终检查文件的完整层次结构,在检查有效时pom.xml使用help:effective-pom可能非常有用dependencyManagementdependencies并且properties就最终的依赖关系图而言。
  • 使用其他一些 Maven 插件来帮助您进行治理。在maven-dependency-plugin故障排除期间确实很有帮助,但也maven-enforcer-plugin可以提供帮助。这里有几个值得一提的例子:

以下示例将确保没有人(您、您的队友、您未来的自己)能够在compile范围内添加一个众所周知的测试库:构建将失败。它确保junit永远不会到达 PROD(与您的包装一起war,例如)

<plugin>
    <artifactId>maven-enforcer-plugin</artifactId>
    <version>1.4.1<.version>
    <executions>
        <execution>
            <id>enforce-test-scope</id>
            <phase>validate</phase>
            <goals>
                <goal>enforce</goal>
            </goals>
            <configuration>
                <rules>
                    <bannedDependencies>
                        <excludes>
                            <exclude>junit:junit:*:*:compile</exclude>
                            <exclude>org.mockito:mockito-*:*:*:compile</exclude>
                            <exclude>org.easymock:easymock*:*:*:compile</exclude>
                            <exclude>org.powermock:powermock-*:*:*:compile</exclude>
                            <exclude>org.seleniumhq.selenium:selenium-*:*:*:compile</exclude>
                            <exclude>org.springframework:spring-test:*:*:compile</exclude>
                            <exclude>org.hamcrest:hamcrest-all:*:*:compile</exclude>
                        </excludes>
                        <message>Test dependencies should be in test scope!</message>
                    </bannedDependencies>
                </rules>
                <fail>true</fail>
            </configuration>
        </execution>
    </executions>
</plugin>

看看这个插件提供的其他标准规则:如果出现错误情况,许多可能有助于破坏构建:

同样,一个共同的父 pom可以包含多个这些机制(dependencyManagement、强制插件、依赖系列的属性)并确保遵守某些规则。你可能无法涵盖所有​​可能的场景,但它肯定会降低你感知和体验的地狱程度。

于 2016-08-31T14:42:29.193 回答
7

使用Maven Helper 插件通过排除旧版本的依赖项来轻松解决所有冲突。

于 2015-11-25T01:57:50.620 回答
3

根据我的经验,我没有发现任何完全自动化的东西,但我发现以下方法对我自己非常有用和有用:

首先,我尝试对项目结构、项目之间的关系有一个清晰的映射,我通常使用 Eclipse 图形依赖项视图,它告诉我,例如,如果一个依赖项因与另一个冲突而被省略。此外,它会告诉您已解决的项目依赖项。我真诚地不使用 IntelliJ IDEA,但我相信它具有类似的功能。

通常我会尝试将非常常见的依赖项放在结构中更高的位置,并利用该<dependencyManagement>功能来处理传递依赖项的版本,最重要的是避免项目结构中的重复

在这篇Maven - Manage Dependencies 博客文章中,您可以找到一个关于依赖管理的好教程。

在向我的项目添加新的依赖项时,就像你的情况一样,我会注意它在我的项目结构中的添加位置并进行相应的更改,但在大多数情况下,依赖项管理机制能够处理这个问题。

在这篇Maven 最佳实践博客文章中,您可以找到:

Maven 的 dependencyManagement 部分允许父 pom.xml 定义可能在子项目中重用的依赖项。这样可以避免重复;如果没有 dependencyManagement 部分,每个子项目都必须定义自己的依赖项并复制依赖项的版本、范围和类型。

显然,如果您需要项目的特定版本的依赖项,您总是可以在本地指定您需要的版本,在层次结构的深处。

我同意你的观点,这可能很乏味,但依赖管理可以为你提供很好的帮助。

于 2016-08-31T08:09:31.077 回答
0

即使用相同的名称替换所有 jar,您仍然可以拥有一些具有相同完全限定名称的类。我在我的一个项目中使用了 maven shade 插件。它打印来自不同 jar 的具有相同限定名称的类。也许这可以帮助你

于 2016-09-03T13:32:09.600 回答