Customizing dependency resolution rules

For each dependency we define in our build file, there is a dependency resolution rule. This rule is executed when the dependency needs to be resolved. We can customize this rule in our build file, so we can change certain parts of the rule before the dependency is actually resolved. Gradle allows us to change the dependency group, name, and version with a customized resolution rule. This way, we can even completely replace dependencies with other dependencies or force a particular version.

Dependency resolution rule details are implemented in the org.gradle.api.artifacts.DependencyResolveDetails class. Inside the resolutionStrategy configuration block, we use the eachDependency method to customize a resolution rule. This method accepts a closure, and the closure argument is an instance of DependencyResolveDetails. We use the useVersion and useTarget methods of DependencyResolveDetails to change either the version or the complete group, name, and version for a requested dependency.

Let's change our previous example build file and define a customized resolution rule for the org.slf4j:slf4j-api dependency so that the version 1.7.7 is always used. In the next example build file, we will see how to achieve this:

apply plugin: 'java'

repositories.jcenter()

configurations {
  runtime {
    resolutionStrategy {
      failOnVersionConflict()

      // Customize dependency resolve rules.
      eachDependency { DependencyResolveDetails details ->
        def requestedModule = details.requested

        // Force version for 
        // org.slf4j:slf4j-api dependency.
        if (requestedModule.group == 'org.slf4j'
          && requestedModule.name == 'slf4j-api') {

          // Force version 1.7.7.
          details.useVersion '1.7.7'
        }
      }
    }
  }
}

dependencies {
  compile 'org.slf4j:slf4j-api:1.7.7'

  runtime 'ch.qos.logback:logback-classic:1.1.2'
}

This mechanism is very powerful. Besides forcing a particular version, we can use the dependency resolution rules to replace a complete dependency with another. Let's suppose we have a dependency in our project and this dependency has a transitive dependency on the Log4j logging framework. We don't want this dependency, and instead want to use the log4j-over-slf4j bridge. This bridge contains alternative implementations for Log4j classes, so we can use an SLF4J API implementation. The log4j-over-slf4j bridge is defined by the org.slf4j:log4j-over-slf4j:1.7.7 dependency. We use the useTarget method of the resolution rule details to set a new target. The method accepts both string notations and map notations for dependencies.

The following example build file contains the dependency resolution rule to replace a dependency on the Log4j to the log4j-over-slf4j bridge:

apply plugin: 'java'

repositories.jcenter()

configurations {
  runtime {
    resolutionStrategy {
      eachDependency { DependencyResolveDetails details ->
        def requestedModule = details.requested

        // Change resolve rule for log4j:log4j
        // (transitive) dependency.
        if (requestedModule.group == 'log4j'
          && requestedModule.name == 'log4j') {

          // Replace log4j:log4j:<version> with
          // org.slf4j:log4j-over-slf4j:1.7.7.
          details.useTarget group: 'org.slf4j',
                  name: 'log4j-over-slf4j',
                  version: '1.7.7'
          // Alternative syntax:
          // useTarget 'org.slf4j:log4j-over-slf4j:1.7.7'
        }
      }
    }
  }
}

dependencies {
  compile 'org.slf4j:slf4j-api:1.7.7'

  // In real life this is probably a transitive
  // dependency from a dependency we need in our project.
  // We put it here as an example to show we
  // can completely replace a dependency with
  // another.
  runtime 'log4j:log4j:1.2.17'

  runtime 'ch.qos.logback:logback-classic:1.1.2'
}

We can verify that the Log4j dependency is replaced with the dependencies task from the command line. This is shown in the following code:

$ gradle -q dependencies --configuration runtime

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

runtime - Runtime classpath for source set 'main'.
+--- org.slf4j:slf4j-api:1.7.7
+--- log4j:log4j:1.2.17 -> org.slf4j:log4j-over-slf4j:1.7.7
|    --- org.slf4j:slf4j-api:1.7.7
--- ch.qos.logback:logback-classic:1.1.2
    +--- ch.qos.logback:logback-core:1.1.2
    --- org.slf4j:slf4j-api:1.7.6 -> 1.7.7

(*) - dependencies omitted (listed previously)

Notice the log4j:log4j:1.2.17 → org.slf4j:log4j-over-slf4j:1.7.7 line, which visually shows the replacement of the dependency with a new dependency.

Custom dependency resolution rules also allow us to define a custom version scheme. For example, in our organization, we can define that if the version of a dependency is set to the fixed value, the actual version is fetched from a central location on the corporate intranet. This way, all projects in the organization can share the same version for dependencies.

In the next example build file, we will implement a custom version scheme. If the version attribute is omitted or has the fixed value, then the version information is fetched from a predefined list of versions. The following code shows this:

apply plugin: 'java'

repositories.jcenter()

configurations {
  runtime {
    resolutionStrategy {
      eachDependency { DependencyResolveDetails details ->
        def requestedModule = details.requested

        // If version is not set or version 
        // has value 'fixed' set
        // version based on external definition.
        if (!requestedModule.version
          || requestedModule.version == 'fixed') {
          def moduleVersion = findModuleVersion(requestedModule.name)
          details.useVersion moduleVersion
        }
      }
    }
  }
}

dependencies {
  // Version is not defined for this dependency,
  // is resolved via custom dependency resolve rule.
  compile 'org.slf4j:slf4j-api'

  // Version is set to 'fixed', so version can
  // be resolved via custom dependency resolve rule.
  runtime 'ch.qos.logback:logback-classic:fixed'
}

/**
* Find version for given module name. In real life
* this could be part of a company Gradle plugin
* or intranet resource with version information.
*
* @param name Module descriptor name
* @return Version for given module name
*/
def findModuleVersion(String name) {
  ['slf4j-api': '1.7.7', 'logback-classic': '1.1.2']
  .find { it.key == name}
  .value
}

It is interesting to see what the output of the dependencies task is when we run it from the command line:

$ gradle -q dependencies --configuration runtime

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

runtime - Runtime classpath for source set 'main'.
+--- org.slf4j:slf4j-api: -> 1.7.7
--- ch.qos.logback:logback-classic: -> 1.1.2
    +--- ch.qos.logback:logback-core:1.1.2
    --- org.slf4j:slf4j-api:1.7.6 -> 1.7.7

(*) - dependencies omitted (listed previously)

In the output, we clearly see how the org.slf4j:slf4j-api dependency without a version is now using the version 1.7.7. The fixed version of the ch.qos.logback:logback-classic dependency is resolved to the version 1.1.2.

Using client modules

Instead of relying on the module descriptor found in the repository for our external module dependency, we can define the metadata for the module in our build file as a client module. Remember from Chapter 1, Defining Dependencies, that with a client module, we define the module descriptor in our build file and still get the artifacts from the repository.

Let's use a client module in the following example build file. We redefine the transitive dependencies for the logback-classic dependency and use the version 1.7.7 for the slf4j-api dependency. The following code shows this:

apply plugin: 'java'

repositories.jcenter()

configurations {
  runtime {
    resolutionStrategy {
      failOnVersionConflict()
    }
  }
}

dependencies {
  compile 'org.slf4j:slf4j-api:1.7.7'

  // Use a client module to redefine the transitive
  // dependencies for the logback-classic.
  runtime module('ch.qos.logback:logback-classic:1.1.2') {
    dependency 'ch.qos.logback:logback-core:1.1.2'

    // Force the correct version of
    // the slf4j-api dependency/
    dependency 'org.slf4j:slf4j-api:1.7.7'
  }
}

We invoke the dependencies task from the command line to check whether the correct dependencies are used:

$ gradle -q dependencies --configuration runtime

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

runtime - Runtime classpath for source set 'main'.
+--- org.slf4j:slf4j-api:1.7.7
--- ch.qos.logback:logback-classic:1.1.2
    +--- org.slf4j:slf4j-api:1.7.7
    --- ch.qos.logback:logback-core:1.1.2

(*) - dependencies omitted (listed previously)

We see in the output that the dependency on org.slf4j:slf4j-api is now 1.7.7 and we don't have version conflict anymore.

..................Content has been hidden....................

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