编译的时候可以运行,运行时出问题
在当今java项目自动化构建的场景当中,依赖管理已经成为了项目构建自动化工具中的一个主要部分,但是在过去并总是这样。
回想以前那个很爽的时候,你的项目依赖性管理只需要将jar包导入到lib文件夹中,然后将其加入到你的版本控制系统中。
或者可以采取更冒险的方式,你可以写一个从外部源中下载想要的版本的库文件的脚本,还要向神仙保佑外部的URL不会改变。
用手动的方式来完成所有的这些将成为一个令人怯步而且麻烦的过程,尤其是考虑到透明的依赖(依赖的依赖),也许只有在某个库文件的代码执行的时候才会出现。
这会造成在运行过程中发生某些意向不到的麻烦,即使是在编译过程能够修复的问题也会发现和解析出有透明依赖。
幸运的是,在如今的项目构建自动化工具中这种情形不再是主要问题(这句话翻得有问题)。
项目的模块化变得越来越受欢迎,内部项目和外部依赖的需求也增加了,结果所有常用的项目自动化构建工具针对这种挑战进行加强了并且支持依赖性管理;不论是out of box还是通过插件。
为了更加方便开发者,它们都是用相同的语法来定义依赖性,同时也可以从相同的公共的资料库(public artifact repositories)中抽取依赖性关系(比如Maven Central)。
但是和这些公共的资料库一样方便的同时,它们也为项目带来了复杂性;透明依赖的冲突,而且增加了对于远程库的依赖。
如何定义依赖性
用于定义依赖性最常用的语法是加入group-id,artifact-id和需要的版本号到构建脚本的依赖性部分。项目构建工具会试图解析这些依赖性,通过在它们的局部环境中和远程定义的库中。
在下面的例子中,将会向你展示如何Google的公共资源库Guava的依赖性。在开始之前,你得知道它的group-id(com.google.guava),它的artifact-id(guava),和你感兴趣的库的版本号,
(在这里以最近的版本“15.0”为例)。手上有了这些信息之后,你可以往构建脚本的依赖性部分加入这些依赖。
以下是在Maven,Gradle和Ant Ivy中的示例:
Maven
Gradle
Ant+Ivy(ivy.xml,或者在build.xml中)
现在你可以知道之前的参数是如何在这三个例子中使用的,在Maven中可能稍显累赘一点。
在构建工具中,property的名称可能会有些许不同,但大体上还是类似的。
如何使用版本范围
What if you don’t want to depend on a specific version of a library, but know that any version in a specific range will do? Or if you know that any version except ‘X’ will work? This is where version ranges can come in handy.
Instead of just a single version number, you can also specify a range, using a notation very similar to how you would define an interval in mathematics: parenthesis ( ) work as exclusive markers and brackets [ ] as inclusive markers. Using a range allows the tool to pick a suitable version for the build. Continuing the example with Guava, if you know that any version between 12.0 and up to 15.0 (excluded) would work, you could define it as “[12.0,15.0)”. You can also leave it open-ended like “[12.0,)”, which adds a requirement for any version from 12.0 and up, similarly “(,12.0]” would be any version up to and including 12.0. Or if you want anything higher than 12.0, except for version 13.0, you could specify “[12.0,13.0),(13.0,)” as the version.
But why use ranges at all? It’s an easy and convenient way to get a newer version of the library without having to change your build script; however, it also sets you up for potential trouble, should the author of the library opt to change functionality or the API that you’re relying on! Another caveat about using ranges is that if the version numbering is inconsistent or doesn’t follow some standard, things might not go as expected. Using ranges on artifacts with qualifiers in the version string (like SNAPSHOT, ALPHA, BETA etc) also doesn’t always go as expected, as the range definition only supports numerical intervals and the build tool might pick a beta version because it has a higher number than the release version.
Besides ranges and specific versions, dependencies can also be resolved using dynamic versions by using the keywords ‘LATEST’ or ‘RELEASE’ instead of the version number. Using those will make the build tool inquire the artifact repository about which version is the latest (or latest release) and use that version. The same caveats apply here as with version ranges though–any changes to the API or functionality might break the world.
Transitive dependencies and dependency conflicts
Let’s go back in time to the “good old days” again. Here, you had full control and an overview over which libraries were used, since it was immediately visible which libraries were present in the lib-folder. But with declarative dependencies remotely available, and the transitive dependencies that are automatically included as well, this easy overview of which libraries are in use have become somewhat obscured. Luckily, most build tools have a plugin or an option to list the entire dependency tree:
- Maven:
mvn dependency:tree -Dverbose
- Gradle:
gradle -q dependencies
- Ivy:
<report conf="compile" />
Ivy’s report option allows you to generate the reports in various different formats, the default being an HTML and graphml report; so not as straight-forward as the console output you get from Maven and Gradle.
But what happens if there are conflicts in the dependencies? What if Library A and Library B both depend on Library C, but require different versions of it? This is where things start to get a bit tricky, and where build tools’ dependency management implementations diverge.
Assuming we have a project dependency structure that looks something like this:
-
Project
- Module A
- com.google.guava:guava:[11.0,12.99]
- Module B
- com.google.guava:guava:[13.0,)
- Module A
Trying to build the above project with Maven will result in an error because the dependencies could not be resolved, since no version of Guava can satisfy both ranges. But if you use an equivalent Gradle build script and build it with Gradle, it will pick the highest version of Guava available in either of the ranges; which in this case means version ’15.0’. Changing the dependency of Module B, so its range is ‘[12.0,)’, Maven will now pick version ‘12.0.1’, which satisfies both ranges; Gradle still picks version ’15.0’.
Ivy and Gradle acts very similar in these scenarios, which isn’t that surprising considering Gradle originally used Ivy as their underlying dependency management implementation until they implemented their own dependency resolution engine.
The usage of ranges isn’t that widely-used though, and the more common use case is to just have the simple version number listed in the dependency. Even in this simple case, Maven, Gradle and Ivy again act vastly different when resolving dependencies!
Maven utilizes a “nearest definition” strategy, in which the closest version to the root is the version it’ll use throughout the entire build! In the case of the structure above, Module A and Module B both depend on Guava, and they are both found at the same depth; but since Module A is listed in the project before Module B, the dependency for Guava used there will be the version used for the entire build, even if the latter relies on a higher version!
Due to this, a common approach for having better control over which version of conflicting dependencies is used is to add a dependency to the wanted version in the parent pom file. Since this is the first pom to be parsed, the dependencies listed there will always be nearest, thus Maven should use that version and ignore every other version mentioned in the dependency graph.
As opposed to Maven, both Gradle and Ivy by default resolve dependency conflicts with a simple strategy: they just select the highest version :) If this strategy doesn’t suit your needs, you can select a different strategy: for instance, force the build to fail should a dependency conflict arise, or forcing a specific version of the dependency to be used, overriding any version otherwise defined as part of the dependency graph.
Final words
While the above was just a short introduction to some of the great and not so great things about dependency management, these are some of the things to keep in mind when dealing with it, and a good excuse to start reading up on the documentation for your build tool to see exactly what’s happening behind the screen.
- Maven: Introduction to the Dependency Mechanism is a good place to start:http://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html
-
Gradle: a good starting point is Chapter 50 of its user guide:http://www.gradle.org/docs/current/userguide/dependency_management.html
Ant + Ivy: A slightly obfuscated path to the documentation can be found here:http://ant.apache.org/ivy/history/latest-milestone/ivyfile/dependency.html
Even though keeping binary dependencies in your VCS is somewhat frowned upon today, the idea of having complete control over which libraries you include is still a good idea. No matter which build tools you prefer (or are required to use), it’s always a good idea to know how they handle your dependencies; an old version of a library might introduce bugs, strangeness or, in the worst case, security risks to your production system! Keeping a handle on this can potentially save you a lot of headaches down the road.