Chapter 3. Resolving Dependencies

In Chapter 1, Defining Dependencies, you learned how to add dependencies to your projects. We have seen different ways of specifying dependencies, such as module or project dependencies. In the previous chapter, we explored how to define the repositories that host our dependencies. Gradle will use this information to do the actual dependency resolution. In this chapter, we will see how Gradle resolves dependencies.

Gradle has a different way of resolving version conflicts than other build tools, so we will take a good look at what happens when a dependency is resolved. We will see how we can customize the resolution process in Gradle so that we can have the exact dependencies we want and have reliable and repeatable builds.

Understanding dependency resolution

Gradle will use the information in the repositories and dependencies configuration blocks to gather and download all dependencies. This process is also called dependency resolution. The following steps are taken by Gradle to resolve dependencies:

  1. The module descriptor file for a dependency is searched in the defined repositories. The order of the repository definitions is used for searching. So, repositories defined before other repositories are searched first, and so on. If a POM or Ivy descriptor file is found, it is used. If no descriptor file is found, then the artifact file for the dependency is searched. If either the descriptor file or the artifact file is found, then Gradle knows this repository can be used to download the dependencies.
    • If a POM descriptor file is found with a parent POM descriptor file, then the parent POM is resolved by Gradle.
    • A dynamic version, like 4.1.+, is resolved to the highest available static version in the repository. For example if the repository contains versions 4.1.0 and 4.1.1 then the 4.1.1 version is used.
  2. Gradle will determine which repository is the best to use based on the following criteria:
    • Module descriptor files, like POM and Ivy descriptor files, are preferred over artifact file only.
    • Dependencies found in earlier repositories are preferred over later repositories.
    • If a dynamic version like 2.+ is used, than a higher static version is preferred over a lower static version.
  3. The artifacts for the module dependency are downloaded from the repository that is chosen by Gradle. This means that artifacts are not downloaded from a different repository than where the descriptor file or artifact file for the defined dependency are found.

If a dependency is defined with a static version, and Gradle finds a module descriptor file for this dependency in a repository, then the search for this dependency is complete, and other repositories will not be used for the search. The process cannot come up with a better repository candidate, so the dependency resolution is finished for the dependency.

Configuring transitive dependencies

Most of the time dependencies in our project are also dependent on other libraries. So, the dependencies have dependencies of their own. These are so-called transitive dependencies. Gradle must be able to resolve the transitive dependencies as well.

In the following example build file, we define the logback-classic module dependency with the version 1.1.2 and the group name ch.qos.logback:

apply plugin: 'java'

repositories.jcenter()

dependencies {
  // Dependency definition for Logback classic
  // library, used as implementation for SLF4J API.
  compile 'ch.qos.logback:logback-classic:1.1.2'
}

When we run the Gradle dependencies task, we can see that our defined dependency for logback-classic depends on ch.qos.logback:logback-core:1.1.2 and org.slf4j:slf4j-api:1.7.6. The following code shows this:

$ gradle -q dependencies --configuration compile
------------------------------------------------------------
Root project
------------------------------------------------------------

compile - Compile classpath for source set 'main'.
--- ch.qos.logback:logback-classic:1.1.2
  +--- ch.qos.logback:logback-core:1.1.2
  --- org.slf4j:slf4j-api:1.
7.6

Disabling transitive dependencies

If we don't want to have transitive dependencies in our project, we must reconfigure the dependency or configuration. With Gradle, we have different ways to disable transitive behavior for dependencies. First, we can add a configuration closure to our dependency definition, use the transitive property, and set the value to false. By default, all dependencies are treated as transitive, as we saw in our example build file.

In the following example build file, we specify that we want to treat or use the logback-classic dependency as nontransitive:

apply plugin: 'java'

repositories.jcenter()

dependencies {
  // Dependency definition for Logback classic.
  compile 'ch.qos.logback:logback-classic:1.1.2', {
    // We don't want to have the transitive dependencies.
    transitive = false
  }
}

If we run the dependencies task again, we can see in the output that the transitive dependencies are no longer resolved:

$ gradle -q dependencies --configuration compile

------------------------------------------------------------
Root project
------------------------------------------------------------

compile - Compile classpath for source set 'main'.
--- ch.qos.logback:logback-classic:1.1.2

We can also disable transitive dependencies for a dependency configuration as a whole. So, this means that any dependencies defined with the configuration will not have transitive dependencies. Single dependencies within the configuration can use the transitive property in the configuration closure to enable transitive behavior again for that dependency. To accomplish this, perform the following steps:

  1. First, we will disable transitive dependencies for the compile configuration in the next example build file:
    apply plugin: 'java'
    
    repositories.jcenter()
    
    configurations {
      // Disable transitive dependencies for
      // all dependencies defined in this
      // configuration.
      // Configurations extended
      // from the compile configuration will not
      // inherit this transitive property value.
      compile.transitive = false
    }
    
    dependencies {
      // Dependency definition for Logback classic
      compile 'ch.qos.logback:logback-classic:1.1.2'
    }
  2. Next, we will execute the dependencies task and see that transitive dependencies are no longer resolved:
    $ gradle -q dependencies --configuration compile
    
    ------------------------------------------------------------
    Root project
    ------------------------------------------------------------
    
    compile - Compile classpath for source set 'main'.
    --- ch.qos.logback:logback-classic:1.1.2

Excluding transitive dependencies

We can also have more fine-grained control of transitive dependencies. We can exclude certain transitive dependencies in our dependency definition. This way, we can choose to use only certain transitive dependencies and leave others out. To define which transitive dependencies we want to exclude, we use the exclude method in the configuration closure of our dependency.

Let's see how we can include the logback-core transitive dependency but remove the slf4j-api dependency. We use the exclude method in the configuration closure. The exclude method takes Map as an argument with one or both of the keys: module and group. In the following build file, we include the logback-core transitive dependency:

apply plugin: 'java'

repositories.jcenter()

dependencies {
  // Dependency definition for Logback classic
  compile('ch.qos.logback:logback-classic:1.1.2') {
    // Exclude slf4j-api transitive dependency.
    exclude module: 'slf4j-api'
    // Alternative syntax:
    // Exclude all modules in the group org.slf4j:
    // exclude group: 'org.slf4j'
    // Or specify both group and module name:
    // exclude group: 'org.slf4j', module: 'slf4j-api'
  }
}

We execute the dependencies task to see whether our configuration definition has the desired effect:

$ gradle -q dependencies --configuration compile

------------------------------------------------------------
Root project
------------------------------------------------------------

compile - Compile classpath for source set 'main'.
--- ch.qos.logback:logback-classic:1.1.2
    --- ch.qos.logback:logback-core:1.1.2

Notice that in the output, the transitive dependency, org.slf4j:slf4j-api:1.7.6, is no longer part of our transitive dependencies.

We can also set exclude rules on a configuration in addition to a single dependency. The exclude rule on a configuration will be used for all dependencies defined within the configuration. In the next example Gradle build file, we will exclude the slf4j-api module from all dependencies in the compile configuration:

apply plugin: 'java'

repositories.jcenter()

configurations {
  compile {
    // Exclude slf4j-api transitive dependency.
    exclude module: 'slf4j-api'
    // Alternative syntax:
    // Exclude all modules in the group org.slf4j:
    // exclude group: 'org.slf4j'
    // Or specify both group and module name:
    // exclude group: 'org.slf4j', module: 'slf4j-api'
  }

  // To exclude a module and/or group from all configurations
  // we can use the all method:
  // all { exclude module: 'slf4j-api' }
}

dependencies {
  // Dependency definition for Logback classic.
  compile('ch.qos.logback:logback-classic:1.1.2')
}

Any exclude rule that we add to either the configuration or the dependency is accessible again via the excludeRules property of the corresponding objects. We can use this to find out the configuration or dependency that is responsible for excluding a certain dependency. In the following example build file, we create a new task, showExcludeRules, where we loop through all configurations and dependencies and collect exclude rules. At the end of the task, we print all the information to standard output. The following code shows this:

apply plugin: 'java'

repositories.jcenter()

configurations {
  compile {
    exclude module: 'slf4j-api'
  }
}

dependencies {
  compile('ch.qos.logback:logback-classic:1.1.2') {
    exclude group: 'ch.qos.logback', module: 'logback-core'
  }
}

task showExcludeRules {
  description 'Show exclude rules for configurations and dependencies'

  doFirst {
    // Store found exclude rules.
    def excludes = []

    // Go through all configurations to find exclude rules
    // defined at configuration level and at
    // dependency level for dependencies in the configuration.
    configurations.all.each { configuration ->
      def configurationExcludes = configuration.excludeRules
      configurationExcludes.findAll().each { rule ->
        // Add found excludeRule to excludes collection.
        excludes << [type: 'container',
              id: configuration.name,
              excludes: rule]
      }

      def dependencies = configuration.allDependencies
      dependencies.all { dependency ->
        def excludeRules = dependency.excludeRules

        excludeRules.findAll().each { rule ->
          def dep = dependency
          def id = "${dep.group}:${dep.name}:${dep.version}"
          // Add found excludeRule to excludes collection.
          excludes << [type: 'dependency', id: id, excludes: rule]
        }
      }
    }

    // Printing exclude rule information for output.
    def printExcludeRule = {
      def rule = "${it.excludes.group ?: '*'}:${it.excludes.module ?: '*'}"
      println "$it.id >> $rule"
    }

    // Print formatted header for output.
    def printHeader = { header ->
      println()
      println '-' * 60
      println header
      println '-' * 60
    }

    // Group rules by organisation or dependency.
    def excludeRules = excludes.groupBy { it.type }

    printHeader 'Configurations'
    excludeRules.container.toSet().each(printExcludeRule)

    printHeader 'Dependencies'
    excludeRules.dependency.toSet().each(printExcludeRule)
  }
}

When we run the task, we get the following output:

$ gradle -q showExcludeRules

------------------------------------------------------------
Configurations
------------------------------------------------------------
compile >> *:slf4j-api

------------------------------------------------------------
Dependencies
------------------------------------------------------------
ch.qos.logback:logback-classic:1.1.2 >> ch.qos.log
back:logback-core

Using artifact-only dependencies

Finally, we can use the ext attribute for an external module dependency if we know we only want to include a single artifact from the dependency. With this attribute, no transitive dependencies are resolved because we specify that we specifically want the artifact specified by the ext attribute.

In our example, we can use the ext attribute with the jar value to resolve only the JAR artifact for the logback-classic dependency. In the next example build file, we will use the ext attribute for our logback-classic dependency:

apply plugin: 'java'

repositories.jcenter()

dependencies {
  // Dependency definition for Logback classic library
  compile 'ch.qos.logback:logback-classic:1.1.2@jar'

  // Alternative syntax:
  //compile group: 'ch.qos.logback',
  //        name: 'logback-classic',
  //        version: '1.1.2',
  //        ext: 'jar'
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.117.214