Chapter 5. Dependency management
This chapter covers
- Understanding automated dependency management
- Declaring and organizing dependencies
- Targeting various types of repositories
- Understanding and tweaking the local cache
- Dependency reporting and version conflict resolution
In chapter 3, you learned how to declare a dependency on the Servlet API to implement web components for the To Do application. Gradle’s DSL configuration closures make it easy to declare dependencies and the repositories to retrieve them from. First, you define what libraries your build depends on with the dependencies script. Second, you tell your build the origin of these dependencies using the repositories closure. With this information in place, Gradle automatically resolves the dependencies, downloads them to your machine if needed, stores them in a local cache, and uses them for the build.
This chapter covers Gradle’s powerful support for dependency management. We’ll take a close look at key DSL configuration elements for grouping dependencies and targeting different types of repositories.
Almost all JVM-based software projects depend on external libraries to reuse existing functionality. For example, if you’re working on a web-based project, there’s a high likelihood that you rely on one of the popular open source frameworks like Spring MVC or Play to improve developer productivity. Libraries in Java get distributed in the form of a JAR file. The JAR file specification doesn’t require you to indicate the version of the library. However, it’s common practice to attach a version number to the JAR filename to identify a specific release (for example, spring-web-3.1.3.RELEASE.jar). You’ve seen small projects grow big very quickly, along with the number of third-party libraries and modules your project depends on. Organizing and managing your JAR files is critical.
The open source project Cargo (http://cargo.codehaus.org/) provides versatile support for web application deployment to a variety of Servlet containers and application servers. Cargo supports two implementations you can use in your project. On the one hand, you can utilize a Java API, which gives you fine-grained access to each and every aspect of configuring Cargo. On the other hand, you can choose to execute a set of preconfigured Ant tasks that wrap the Java API. Because Gradle provides excellent integration with Ant, our examples will be based on the Cargo Ant tasks.
Let’s revisit figure 5.1 and see how the components change in the context of a Gradle use case. In chapter 3 you learned that dependency management for a project is configured with the help of two DSL configuration blocks: dependencies and repositories. The names of the configuration blocks directly map to methods of the interface Project. For your use case, you’re going to use Maven Central because it doesn’t require any additional setup. Figure 5.4 shows that dependency definitions are provided through Gradle’s DSL in the build.gradle file. The dependency manager will evaluate this configuration at runtime, download the required artifacts from a central repository, and store them in your local cache. You’re not using a local repository, so it’s not shown in the figure.
In chapter 3, you saw that plugins can introduce configurations to define the scope for a dependency. The Java plugin brings in a variety of standard configurations to define which bucket of the Java build lifecycle a dependency should apply to. For example, dependencies required for compiling production source code are added with the compile configuration. In the build of your web application, you used the compile configuration to declare a dependency on the Apache Commons Lang library. To get a better understanding of how configurations are stored, configured, and accessed, let’s look at responsible interfaces in Gradle’s API.
Chapter 3 gave you a first taste of how to tell your project that an external library is needed for it to function correctly. The DSL configuration block dependencies is used to assign one or more dependencies to a configuration. External dependencies are not the only dependencies you can declare for your project. Table 5.1 gives you an overview of the various types of dependencies. In this book we’ll discuss and apply many of these options. Some of the dependency types are explained in this chapter, but others will make more sense in the context of another chapter. The table references each of the use cases.
In this chapter we’ll cover external module dependencies and file dependencies, but first let’s see how dependency support is represented in Gradle’s API.
Gradle puts a special emphasis on supporting existing repository infrastructures. You’ve already seen how to use Maven Central in your build. By using a single method call, mavenCentral(), you configured your build to target the most popular Java binary repository. Apart from the preconfigured repository support, you can also assign an arbitrary URL of a Maven or Ivy repository and configure it to use authentication if needed. Alternatively, a simple file system repository can be used to resolve dependencies. If metadata is found for a dependency, it will be downloaded from the repository as well. Table 5.2 shows the different types of repositories and what section to go to next to learn more about it.
Feel free to jump to the section that describes the repository you want to use in your project. In the next section, we’ll look at Gradle’s API support for defining and configuring repositories before we apply each of them to practical examples.
So far we’ve discussed how to declare dependencies and configure various types of repositories to resolve those artifacts. Gradle automatically determines whether a dependency is needed for the task you want to execute, downloads the artifacts from the repositories, and stores them in the local cache. Any subsequent build will try to reuse these artifacts. In this section, we’ll dig deeper by analyzing the cache structure, identifying how the cache works under the hood and how to tweak its behavior.
Version conflicts can be a hard nut to crack. If your project deals with many dependencies and you choose to use automatic resolution for transitive dependencies, version conflicts are almost inevitable. Gradle’s default strategy to resolve those conflicts is to pick the newest version of a dependency. The dependency report is an invaluable tool for finding out which version was selected for the dependencies you requested. In the following section, I’ll show how to troubleshoot version conflict and tweak Gradle’s dependency resolution strategy to your specific use case.
Most projects, be they open source projects or an enterprise product, are not completely self-contained. They depend on external libraries or components built by other projects. While you can manage those dependencies yourself, the manual approach doesn’t fulfill the requirements of modern software development. The more complex a project becomes, the harder it is to figure out the relationships between dependencies, resolve potential version conflicts, or even know why you need a specific dependency.
Gradle provides powerful out-of-the-box dependency management. You learned how to declare different types of dependencies, group them with the help of configurations, and target various types of repositories to download them. The local cache is an integral part of Gradle’s dependency management infrastructure and is responsible for high-performance and reliable builds. We analyzed its structure and discussed its essential features. Knowing how to troubleshoot dependency version conflicts and fine-tune the cache is key to a stable and reliable build. You used Gradle’s dependency reporting to get a good understanding of the resolved dependency graph, as well as why a specific version of a dependency was selected and where it came from. I showed strategies for changing the default resolution strategy and cache behavior, as well as appropriate situations that make them necessary.