Gradle java project dependencies

Declaring dependencies

Before looking at dependency declarations themselves, the concept of dependency configuration needs to be defined.

What are dependency configurations

Every dependency declared for a Gradle project applies to a specific scope. For example some dependencies should be used for compiling source code whereas others only need to be available at runtime. Gradle represents the scope of a dependency with the help of a Configuration. Every configuration can be identified by a unique name.

Many Gradle plugins add pre-defined configurations to your project. The Java plugin, for example, adds configurations to represent the various classpaths it needs for source code compilation, executing tests and the like. See the Java plugin chapter for an example.

dependency management configurations

For more examples on the usage of configurations to navigate, inspect and post-process metadata and artifacts of assigned dependencies, have a look at the resolution result APIs.

Configuration inheritance and composition

A configuration can extend other configurations to form an inheritance hierarchy. Child configurations inherit the whole set of dependencies declared for any of its superconfigurations.

Configuration inheritance is heavily used by Gradle core plugins like the Java plugin. For example the testImplementation configuration extends the implementation configuration. The configuration hierarchy has a practical purpose: compiling tests requires the dependencies of the source code under test on top of the dependencies needed write the test class. A Java project that uses JUnit to write and execute test code also needs Guava if its classes are imported in the production source code.

Читайте также:  Замена типов данных python

dependency management configuration inheritance

Under the covers the testImplementation and implementation configurations form an inheritance hierarchy by calling the method Configuration.extendsFrom(org.gradle.api.artifacts.Configuration[]). A configuration can extend any other configuration irrespective of its definition in the build script or a plugin.

Let’s say you wanted to write a suite of smoke tests. Each smoke test makes a HTTP call to verify a web service endpoint. As the underlying test framework the project already uses JUnit. You can define a new configuration named smokeTest that extends from the testImplementation configuration to reuse the existing test framework dependency.

val smokeTest by configurations.creating < extendsFrom(configurations.testImplementation.get()) >dependencies
configurations < smokeTest.extendsFrom testImplementation >dependencies

Resolvable and consumable configurations

Configurations are a fundamental part of dependency resolution in Gradle. In the context of dependency resolution, it is useful to distinguish between a consumer and a producer. Along these lines, configurations have at least 3 different roles:

  1. to declare dependencies
  2. as a consumer, to resolve a set of dependencies to files
  3. as a producer, to expose artifacts and their dependencies for consumption by other projects (such consumable configurations usually represent the variants the producer offers to its consumers)

For example, to express that an application app depends on library lib , at least one configuration is required:

// declare a "configuration" named "someConfiguration" val someConfiguration by configurations.creating dependencies < // add a project dependency to the "someConfiguration" configuration someConfiguration(project(":lib")) >
configurations < // declare a "configuration" named "someConfiguration" someConfiguration >dependencies < // add a project dependency to the "someConfiguration" configuration someConfiguration project(":lib") >

Configurations can inherit dependencies from other configurations by extending from them. Now, notice that the code above doesn’t tell us anything about the intended consumer of this configuration. In particular, it doesn’t tell us how the configuration is meant to be used. Let’s say that lib is a Java library: it might expose different things, such as its API, implementation, or test fixtures. It might be necessary to change how we resolve the dependencies of app depending upon the task we’re performing (compiling against the API of lib , executing the application, compiling tests, etc.). To address this problem, you’ll often find companion configurations, which are meant to unambiguously declare the usage:

configurations < // declare a configuration that is going to resolve the compile classpath of the application compileClasspath < extendsFrom(someConfiguration) >// declare a configuration that is going to resolve the runtime classpath of the application runtimeClasspath < extendsFrom(someConfiguration) >>

At this point, we have 3 different configurations with different roles:

  • someConfiguration declares the dependencies of my application. It’s just a bucket that can hold a list of dependencies.
  • compileClasspath and runtimeClasspath are configurations meant to be resolved: when resolved they should contain the compile classpath, and the runtime classpath of the application respectively.

This distinction is represented by the canBeResolved flag in the Configuration type. A configuration that can be resolved is a configuration for which we can compute a dependency graph, because it contains all the necessary information for resolution to happen. That is to say we’re going to compute a dependency graph, resolve the components in the graph, and eventually get artifacts. A configuration which has canBeResolved set to false is not meant to be resolved. Such a configuration is there only to declare dependencies. The reason is that depending on the usage (compile classpath, runtime classpath), it can resolve to different graphs. It is an error to try to resolve a configuration which has canBeResolved set to false . To some extent, this is similar to an abstract class ( canBeResolved =false) which is not supposed to be instantiated, and a concrete class extending the abstract class ( canBeResolved =true). A resolvable configuration will extend at least one non-resolvable configuration (and may extend more than one).

On the other end, at the library project side (the producer), we also use configurations to represent what can be consumed. For example, the library may expose an API or a runtime, and we would attach artifacts to either one, the other, or both. Typically, to compile against lib , we need the API of lib , but we don’t need its runtime dependencies. So the lib project will expose an apiElements configuration, which is aimed at consumers looking for its API. Such a configuration is consumable, but is not meant to be resolved. This is expressed via the canBeConsumed flag of a Configuration :

configurations < // A configuration meant for consumers that need the API of this component create("exposedApi") < // This configuration is an "outgoing" configuration, it's not meant to be resolved isCanBeResolved = false // As an outgoing configuration, explain that consumers may want to consume it assert(isCanBeConsumed) >// A configuration meant for consumers that need the implementation of this component create("exposedRuntime") < isCanBeResolved = false assert(isCanBeConsumed) >>
configurations < // A configuration meant for consumers that need the API of this component exposedApi < // This configuration is an "outgoing" configuration, it's not meant to be resolved canBeResolved = false // As an outgoing configuration, explain that consumers may want to consume it assert canBeConsumed >// A configuration meant for consumers that need the implementation of this component exposedRuntime < canBeResolved = false assert canBeConsumed >>

In short, a configuration’s role is determined by the canBeResolved and canBeConsumed flag combinations:

Resolve for certain usage

For backwards compatibility, both flags have a default value of true , but as a plugin author, you should always determine the right values for those flags, or you might accidentally introduce resolution errors.

Choosing the right configuration for dependencies

The choice of the configuration where you declare a dependency is important. However there is no fixed rule into which configuration a dependency must go. It mostly depends on the way the configurations are organised, which is most often a property of the applied plugin(s).

For example, in the java plugin, the created configuration are documented and should serve as the basis for determining where to declare a dependency, based on its role for your code.

As a recommendation, plugins should clearly document the way their configurations are linked together and should strive as much as possible to isolate their roles.

Defining custom configurations

You can define configurations yourself, so-called custom configurations. A custom configuration is useful for separating the scope of dependencies needed for a dedicated purpose.

Let’s say you wanted to declare a dependency on the Jasper Ant task for the purpose of pre-compiling JSP files that should not end up in the classpath for compiling your source code. It’s fairly simple to achieve that goal by introducing a custom configuration and using it in a task.

val jasper by configurations.creating repositories < mavenCentral() >dependencies < jasper("org.apache.tomcat.embed:tomcat-embed-jasper:9.0.2") >tasks.register("preCompileJsps") < val jasperClasspath = jasper.asPath val projectLayout = layout doLast < ant.withGroovyBuilder < "taskdef"("classname" to "org.apache.jasper.JspC", "name" to "jasper", "classpath" to jasperClasspath) "jasper"("validateXml" to false, "uriroot" to projectLayout.projectDirectory.file("src/main/webapp").asFile, "outputDir" to projectLayout.buildDirectory.file("compiled-jsps").get().asFile) >> >
configurations < jasper >repositories < mavenCentral() >dependencies < jasper 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.2' >tasks.register('preCompileJsps') < def jasperClasspath = configurations.jasper.asPath def projectLayout = layout doLast < ant.taskdef(classname: 'org.apache.jasper.JspC', name: 'jasper', classpath: jasperClasspath) ant.jasper(validateXml: false, uriroot: projectLayout.projectDirectory.file('src/main/webapp').asFile, outputDir: projectLayout.buildDirectory.file("compiled-jsps").get().asFile) >>

You can manage project configurations with a configurations object. Configurations have a name and can extend each other. To learn more about this API have a look at ConfigurationContainer.

Different kinds of dependencies

Module dependencies

Module dependencies are the most common dependencies. They refer to a module in a repository.

dependencies < runtimeOnly(group = "org.springframework", name = "spring-core", version = "2.5") runtimeOnly("org.springframework:spring-aop:2.5") runtimeOnly("org.hibernate:hibernate:3.0.5") < isTransitive = true >runtimeOnly(group = "org.hibernate", name = "hibernate", version = "3.0.5") < isTransitive = true >>
dependencies < runtimeOnly group: 'org.springframework', name: 'spring-core', version: '2.5' runtimeOnly 'org.springframework:spring-core:2.5', 'org.springframework:spring-aop:2.5' runtimeOnly( [group: 'org.springframework', name: 'spring-core', version: '2.5'], [group: 'org.springframework', name: 'spring-aop', version: '2.5'] ) runtimeOnly('org.hibernate:hibernate:3.0.5') < transitive = true >runtimeOnly group: 'org.hibernate', name: 'hibernate', version: '3.0.5', transitive: true runtimeOnly(group: 'org.hibernate', name: 'hibernate', version: '3.0.5') < transitive = true >>

See the DependencyHandler class in the API documentation for more examples and a complete reference.

Gradle provides different notations for module dependencies. There is a string notation and a map notation. A module dependency has an API which allows further configuration. Have a look at ExternalModuleDependency to learn all about the API. This API provides properties and configuration methods. Via the string notation you can define a subset of the properties. With the map notation you can define all properties. To have access to the complete API, either with the map or with the string notation, you can assign a single dependency to a configuration together with a closure.

If you declare a module dependency, Gradle looks for a module metadata file ( .module , .pom or ivy.xml ) in the repositories. If such a module metadata file exists, it is parsed and the artifacts of this module (e.g. hibernate-3.0.5.jar ) as well as its dependencies (e.g. cglib ) are downloaded. If no such module metadata file exists, as of Gradle 6.0, you need to configure metadata sources definitions to look for an artifact file called hibernate-3.0.5.jar directly.

In Maven, a module can have one and only one artifact.

In Gradle and Ivy, a module can have multiple artifacts. Each artifact can have a different set of dependencies.

File dependencies

Projects sometimes do not rely on a binary repository product e.g. JFrog Artifactory or Sonatype Nexus for hosting and resolving external dependencies. It’s common practice to host those dependencies on a shared drive or check them into version control alongside the project source code. Those dependencies are referred to as file dependencies, the reason being that they represent a file without any metadata (like information about transitive dependencies, the origin or its author) attached to them.

dependency management file dependencies

The following example resolves file dependencies from the directories ant , libs and tools .

Источник

Dependency Management

Software projects rarely work in isolation. Projects often rely on reusable functionality from libraries. Some projects organize unrelated functionality into separate parts of a modular system. Dependency management is an automated technique for declaring, resolving and using functionality required by a project.

For an overview of dependency management terms, see Dependency Management Terminology.

Dependency Management in Gradle

dependency management resolution

Gradle has built-in support for dependency management.

We’ll explore the main concepts with the help of the theoretical but common project to define terms:

  • This project builds Java source code.
  • Some of the Java source files import classes from Google Guava, a open-source library.
  • This project uses JUnit for testing.

This project uses Guava and JUnit as dependencies. Gradle fetches dependencies from repositories. You can declare repositories to tell Gradle where to fetch dependencies. Repositories offer dependencies in multiple formats. For information about the formats supported by Gradle, see dependency types.

A build script developer can declare dependencies for different scopes. For example, Gradle could use one scope to compile source code and another to execute tests. Gradle calls the scope of a dependency a configuration.

During a build, Gradle locates the dependencies needed for the requested tasks. The dependencies might need to be downloaded from a remote repository, retrieved from a local directory, or (in a multi-project setting) built from another project. This process is called dependency resolution. For more information, see How Gradle downloads dependencies.

Gradle stores resolved dependencies in a local cache called the dependency cache. Subsequent builds use the cache to avoid unnecessary network calls.

Metadata describes dependencies. Some examples of metadata include:

  • coordinates for finding the dependency in a repository
  • information about the project that created the dependency
  • the authors of the dependency
  • other dependencies required for a dependency to work properly, known as transitive dependencies

Projects with tens or hundreds of declared dependencies can be difficult to debug. Gradle provides tooling to visualize and analyze a project’s dependency graph. You can use a build scan or built-in tasks. For more information, see Viewing and Debugging Dependencies.

Источник

Оцените статью