使用 Maven 处理依赖地狱的系统方法

Posted

技术标签:

【中文标题】使用 Maven 处理依赖地狱的系统方法【英文标题】:Systematic approach with Maven to deal with dependency hell 【发布时间】:2016-02-27 16:35:34 【问题描述】:

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

我的问题是:我如何系统地解决 Jar hell 的问题?

我了解类加载器以及 maven 如何在重复的 Jas 之间进行选择,但我仍然不知道解决问题的实际步骤。

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

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

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

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

【问题讨论】:

我不使用 IDEA,但是 Eclipse 有一个可以按名称过滤的图形依赖项视图,因此很容易看到特定依赖项是从哪里引入的。至少,通过 grep 管道 dependency:tree IntelliJ 也有图形依赖视图 - plugins.jetbrains.com/plugin/7179 要检查依赖版本,请使用 IDE 的图形依赖工具 - Eclipse 有一个很棒的工具,打开 pom.xml 时检查依赖层次选项卡 - 并通过所需的依赖进行过滤,这非常简单。 【参考方案1】:

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

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

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

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

mvn dependency:tree -Dincludes=<groupId>

注意:您还可以以groupId:artifactId:type:version 的形式向includes 选项提供更多信息,或使用*:artifactId 等通配符。

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

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

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

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

你可能指的是带阴影的罐子:

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

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

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

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

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

你可以ban a dependency(甚至传递),在很多情况下真的很方便 您可以fail in case of SNAPSHOT used,在发布配置文件中很方便,例如。

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

【讨论】:

感谢您提供非常详细的回答。我尝试过使用github.com/basepom/duplicate-finder-maven-plugin/wiki,结果非常惊人:) 我忘了提到,duplicate-finder-plugin 主要用于检测“阴影 jar”——它检查类文件的 SHA 总和并报告差异。 @A_Di-Matteo,您还应该提到许多项目提供了 BOM POM 以避免到处出现$spring.version @Michael-O 好点,我确实忘记添加这个信息,我会尽快修复它【参考方案2】:

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

【讨论】:

我不确定如何从 maven.apache.org/plugins/maven-shade-plugin 获取重复类的列表另一方面,duplicate-finder-plugin github.com/basepom/duplicate-finder-maven-plugin/wiki 可以很好地完成这项工作。【参考方案3】:

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

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

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

在这个Maven - Manage Dependencies blog post 你可以找到一个关于依赖管理的好教程。

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

在这个Maven Best Practices blog post你可以找到:

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

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

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

【讨论】:

【参考方案4】:

使用Maven Helper plugin 排除旧版本的依赖项,轻松解决所有冲突。

【讨论】:

Maven Helper 插件真的很有帮助,特别是对于那些喜欢 GUI 而不是 CLI 的人。我不太确定排除旧版本的依赖项是长期的最佳方式。如果我理解正确,它可能会在某些传递依赖项未被使用时很有用。 你可以fork并开始维护更多的东西,或者排除旧的依赖,或者做一些魔术,这样你就可以拥有多个版本。 我更喜欢使用更少的魔法:),在当前状态下它已经足够复杂了,更多的魔法将使它更难理解。当然,在某些特定情况下它可能是唯一的方法。 是的。也许我可以添加一个功能来向dependencyManagement 添加一个条目,而不是排除... 直到您发表最后一条评论,我才意识到您是该插件的作者 :)

以上是关于使用 Maven 处理依赖地狱的系统方法的主要内容,如果未能解决你的问题,请参考以下文章

UNIX系统下删除老旧Maven依赖包的方法

promise处理回调地狱

在 OSGi 包中添加第三方 Maven 依赖项的最佳方法

maven项目关系依赖

如何在使用 maven 构建的战争中包含系统依赖项

Maven 多依赖处理