Gradle 6.0围绕着依赖性管理进行了许多改进,我们在一系列的博文中介绍了这些改进。 在这篇文章中,我们通过性能的概念,探讨了对classpath上不兼容的依赖的检测。
为了说明这个概念,我们将看看Java应用程序和库的日志状态。除了Java核心库提供的java.util.logging (JUL)之外,还有一些日志库可供开发人员使用,例如。
日志问题
有了这样一个庞大的产品,不同的库使用不同的日志API也就不足为奇了。 再加上有时质量不高的元数据,最终在一个应用程序的运行时classpath上出现多个日志实现是很常见的。
这导致了,除其他外,这个著名的Slf4J警告。
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:.../slf4j-log4j12-1.7.29.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:.../logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
虽然我们可以说 "这只是一个警告",但它向我们展示了一个可能发生问题的地方。 我们的一个用户,Netflix的工程团队,告诉了我们以下的故事:由于选择了错误的Slf4J日志绑定,他们系统中的日志文件最终出现在了错误的位置--导致磁盘被填满,最终导致系统崩溃。 他们肯定不是唯一有这样经历的人。
另一个例子:Slf4J文档明确指出,如果你为一个特定的集成混合使用桥接和委托JAR,你会遇到StackOverflowError 。
Log4J 2增加了自己的复杂性,因为它也提供了桥接选项,包括Slf4J桥接。
如果我们看一下不同的库,它们的交互方式,以及它们所提供的选项,我们会得到下面的图表。
要根据你选择的日志框架找出正确的库组合,需要研究你使用的框架和你不使用的框架的兼容性说明,因为它们可以在你没有注意到的情况下通过相互依赖关系被包含进来。 此外,这些要求目前都没有被提供给构建工具,这意味着这些工具不能帮助开发者,让他们知道无效的配置,甚至不能根据选择的日志栈来挑选正确的组合。
自动检测无效的日志设置
有了这个概念 性能的概念,就有可能向构建工具提供信息,这样它至少可以检测出无效的设置。
性能本质上是一个软件模块所提供的功能的标识符。多个模块可以提供相同能力的实现,这使得Gradle可以检测不同的模块是否有冲突。 当它们在依赖关系图上时,这可以直接应用于相同日志API的两个不同实现。
有不同的方法来提供能力信息:直接通过组件的元数据,手动添加到构建中,或者通过插件来提供。
我们已经为日志领域创建了这样一个插件,它为上述的日志库捕获了所有需要的信息,并允许你解决冲突。 dev.jacomet.logging-capabilities Gradle插件将确保你在运行时不会对无效的日志配置感到惊讶,因为你的项目将在构建时报告问题
让我们回顾一下它所处理的一些情况。
Slf4J和它的多重绑定
由于Slf4J警告说多个绑定是有问题的,它有效地创建了一个排他性的实现关系。任何实现slf4j-api ,以提供一个绑定的模块不能与另一个这样的实现住在classpath上。
为了检测我们在一个给定的依赖图中有多个Slf4J绑定,该插件将能力dev.jacomet.logging:slf4j-impl:1.0 给所有下列模块。logback-classic,slf4j-simple,slf4j-log4j12,slf4j-jdk14,log4j-slf4j-impl 和slf4j-jcl 。
有了这些信息,在任何已解决的依赖关系图中出现两个Slf4J绑定是非法的,在构建时强制执行以前只在运行时报告的Slf4J。
下面是一个有这种故障的构建输出示例。
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':doIt'.
> Could not resolve all files for configuration ':runtimeClasspath'.
> Could not resolve org.slf4j:slf4j-simple:1.7.27.
Required by:
project :
> Module 'org.slf4j:slf4j-simple' has been rejected:
Cannot select module with conflict on capability 'dev.jacomet.logging:slf4j-impl:1.0' also provided by [ch.qos.logback:logback-classic:1.2.3(runtime)]
> Could not resolve ch.qos.logback:logback-classic:1.2.3.
Required by:
project :
> Module 'ch.qos.logback:logback-classic' has been rejected:
Cannot select module with conflict on capability 'dev.jacomet.logging:slf4j-impl:1.0' also provided by [org.slf4j:slf4j-simple:1.7.27(runtime)]
Log4J还是其他的日志解决方案?
Log4J的第一个版本是在2001年,当时JUL还没有出现,自从2012年1.2.17 ,就再也没有发布过了。
鉴于此,作为开发者,你可能会更倾向于选择一个更新的解决方案,比如Slf4J绑定或Log4J 2,来作为你的日志框架,但你可能会使用针对Log4J API开发的过渡性依赖。
为了应用你的选择,你需要注意到一些潜在的冲突。
log4j:log4j需要由Slf4J的 或Log4J 2的 替换。log4j-over-slf4jlog4j-1.2-api- 这两个替换本身是排他的
- 如果你使用
log4j-over-slf4j,就不能使用slfj-log4j12
再一次,dev.jacomet.logging-capabilities 插件负责为你声明必要的能力。
- 它将把
dev.jacomet.logging:slf4j-vs-log4j的能力添加到log4j-over-slf4j和slfj-log4j12 - 它将把
dev.jacomet.logging:slf4j-vs-log4j2-log4j的能力添加到log4j:log4j,log4j-over-slf4j和 。log4j-1.2-api
这就保证了你不会在任何已解决的依赖关系图中混合不兼容的桥接和Log4J的实现。
一个全面的解决方案
正如你在增强图上看到的,还有许多其他有问题的模块组合。
- 与Log4J的替换类似,JUL也可以用Slf4J或Log4J 2替换
- 对于Apache Commons Logging,Log4J2的集成需要
commons-logging,而Slf4J的集成则取代了它 - ...
对于所有这些潜在的冲突,插件dev.jacomet.logging-capabilities ,注册了必要的能力来检测所有无效的组合。请到插件文档中查看这些能力及其作用的全面清单。
该插件利用Gradle组件元数据规则来添加能力信息。
前往插件的代码,看看它是如何为所有配置了规则的模块添加能力dev.jacomet.logging:slf4j-impl:1.0 。
同样地,规则也被添加到上图所识别的所有可能的冲突中。
在发布时加强日志生态系统
有了Gradle Module Metadata,上一节提出的概念可以应用于日志库的发布元数据。 这将使使用自定义ComponentMetadataRules或像上面那样的插件变得过时,因为这些信息将由库的作者编码到库的发布元数据中。
作为一个库的作者,可以按照Gradle文档中所示,添加能力进行发布。
识别要声明的能力
工作的一个重要部分是确定这些共享能力的坐标。理想情况下,这个选择将由提供可扩展系统的原始库做出。然后,第三方实现者将能够在他们的实现中符合能力声明。
我不希望我的构建出现问题,我希望Gradle能修复它!"。
我们已经看到了能力是如何被用来检测冲突的,并在出现这种冲突时使构建失败。 但如果我们不能修复检测到的冲突,光靠这一点是无法帮助我们的。 为此,Gradle提供了能力解决策略。
dev.jacomet.logging-capabilities 插件已经设置了这样的解决策略,并提供了简单的结构来选择和激活它们。你可以声明性地表达你的日志选择,该插件确保用相关的能力解决和替换规则来增强你的构建,从而使classpath上只出现必要的日志库。
以下内容将确保Log4J 2被用作记录器的实现。
plugins {
`java-library`
id("dev.jacomet.logging-capabilities")
}
loggingCapabilities {
enforceLog4J2()
}
它将。
- 配置Log4J 2来桥接Slf4J,如果图中有Slf4J桥接的话
- 配置JUL与Log4J 2的桥接。
- 仅在需要时配置
commons-logging的桥接。 - 仅在需要时用
log4j-1.2-api替换log4j
但是它不会添加Log4J 2的依赖关系,这些依赖关系要作为依赖关系来添加--直接或过境。
关于可以表达的选择的完整概述,请前往插件文档。
Gradle提供了一个API来指示如何解决能力冲突。
插件使用它来告诉Gradle,如果在dev.jacomet.logging:slf4j-impl 上发生冲突,引擎必须为测试运行时classpath选择模块org.slf4j:slf4j-simple:1.7.25 。冲突解决逻辑检查了可用的候选模块并执行了条件选择。
请注意,在上面的例子中,如果没有能力冲突,可以使用另一个Slf4J实现。 然而,如果你打算使用slf4j-simple ,你很可能已经在你的构建中声明了这个依赖性。 而这个插件需要它才能正常工作。
总结
我们已经看到,能力是Gradle提供的一个建模概念,用来表达不同库之间的互斥性。如日志用例所示,当在依赖图中发现某个特性的冲突实现时,它们使Gradle的依赖解析失败。与对齐类似,使用Gradle模块元数据的能力使库作者能够分享更多关于何时以及在何种组合中使用他们的库的知识。有了这些信息,Gradle为构建者提供了API,使他们能够在自己的构建中声明性地解决冲突,而无需进行破解。
能力概念可以解决的用例超出了这里展示的日志用例。改变了坐标的库,以多种格式存在的库(如cglib 和cglib-nodep ),或者仅仅是有不同的功能集,都可以利用这个概念来表达在classpath上有超过一个模块的存在应该被视为一个错误。