Resolving version conflicts

Our previous examples were simple and only contained one dependency with some transitive dependencies. When we add more dependencies to our project, or have a multimodule project where each project has a lot of dependencies, then it can happen that the same dependency or transitive dependency is included multiple times. Gradle detects this and makes sure that the dependency is only downloaded once. We will see more about the advanced dependency cache management in Gradle later.

The trouble begins when the same dependency is included multiple times but with different versions. Which version of the dependency should be used? This is where Gradle's resolution strategy comes into play. The next table shows the resolution strategies that Gradle has:

Name

Description

Newest

The newest version of a conflicting dependency is used. This is the default strategy used by Gradle. If the versions of the conflicting dependency are backward compatible, this works fine.

Fail

The build process fails when there is a version conflict with dependencies. We must explicitly add code to our build file that will resolve the version conflict. We will see later in this chapter how we can customize the resolution strategy to solve version conflicts explicitly.

Using the newest version

Let's see what happens if we have a version conflict and use the default resolution strategy of Gradle. Gradle will use the newest version of the dependency that has a version conflict. To accomplish this, perform the following steps:

  1. In the next build file, we define a dependency on slf4j-api in the compile configuration and on logback-classic in the runtime configuration:
    apply plugin: 'java'
    
    repositories.jcenter()
    
    dependencies {
      // Define dependency on SLF4J API for
      // compiling source files.
      compile 'org.slf4j:slf4j-api:1.7.7'
    
      // Define implementation Logback classic
      // of SLF4J API in runtime configuration.
      // This has a transitive dependency on
      // org.slf4j:slf4j-api:1.7.6, which is a version
      // conflict with org.slf4j:slf4j-api:1.7.7
      runtime 'ch.qos.logback:logback-classic:1.1.2'
    }
  2. We run the dependencies task to see which versions of the dependencies are used. The following output shows that the org.slf4j:slf4j-api:1.7.6 transitive dependency of logback-classic is changed, so the version 1.7.7 is used, which is defined in the compile configuration:
    $ 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)
    

    Notice the line org.slf4j:slf4j-api:1.7.6 → 1.7.7, where it visually shows that the version is increased for this dependency from 1.7.6 to 1.7.7.

  3. The dependencies task shows a hierarchical tree view of the dependencies and transitive dependencies. To get a view from a specific dependency, and to see how it got in the dependency graph, we use the dependencyInsight task. With this task, we can see how the dependency is resolved and whether any conflict resolution has happened.
  4. We must use the following two options when we invoke the dependencyInsight task from the command line:
    1. We specify the configuration of the dependency with the --configuration option.
    2. Then, we use the --dependency option to specify the name of the dependency.
  5. The name of the dependency doesn't have to be the full name; we can even use part of the name. For example, we can use org.slf4j:slf4j-api, slf4j-api, and slf4j to gain insight into a dependency.
  6. We execute the dependencyInsight task to see more information about the slf4j-api dependency in our example build file:
    $ gradle -q dependencyInsight --configuration runtime --dependency slf4j-api
    org.slf4j:slf4j-api:1.7.7 (conflict resolution)
    --- runtime
    
    org.slf4j:slf4j-api:1.7.6 -> 1.7.7
    --- ch.qos.logback:logback-classic:1.1.2
        --- runtime
    

    In the output, we see that the org.slf4j:slf4j-api:1.7.7 dependency is resolved for the runtime configuration and that conflict resolution has happened for the dependency. In the next lines, we will see that the org.slf4j:slf4j-api:1.7.6 transitive dependency has its version increased from 1.7.6 to 1.7.7. The dependencyInsight task already tells us more about the dependency resolution that is applied. We will probably start with a broad overview using the dependencies task, and if we want to get more information about a particular dependency, we will use the dependencyInsight task.

  7. There is another task that we can use that will combine both the dependencies and dependencyInsight tasks. The htmlDependencyReport task is part of the project-report plugin. With this task, we get an HTML report showing all dependencies, and we can click on dependencies to get more insight. To use the task, we first add the project-report plugin to our example project file. The following code shows this:
    apply plugin: 'java'
    apply plugin: 'project-report'
    
    repositories.jcenter()
    
    dependencies {
      compile 'org.slf4j:slf4j-api:1.7.7'
    
      runtime 'ch.qos.logback:logback-classic:1.1.2'
    }
  8. We execute the htmlDependencyReport task for this build file. The following code shows this:
    $ gradle htmlDependencyReport
    :htmlDependencyReport
    
    BUILD SUCCESSFUL
    
    Total time: 1.645 secs
    $
    
  9. After the task is executed, new files are created in build/reports/project/dependencies/.
  10. When we open the index.html file in a web browser, we can see the name of our project. If we had a multimodule project, we would see all project names here. We can click on the name and get an overview of all configurations. In the next screenshot, we see an overview of all the configuration in our project:
    Using the newest version
  11. When we click on the runtime configuration link, all dependencies are shown. We can see that there is a version conflict because the org.sfl4j:slf4j-api:1.7.6 dependency is orange in color. This view is what we also see when the dependencies task from the command line is invoked:
    Using the newest version
  12. To get the dependency insight view, we click on the org.sfl4j:slf4j-api:1.7.6 → 1.7.7 link. A pop-up window is opened in our web browser, and we see the following screenshot:
    Using the newest version

Now, we see what we normally see if we run the dependencyInsight task from the command line.

The htmlDependencyReport is very useful to get a graphical and interactive view of the dependencies in our project. It is also easy to get more details about a dependency by just clicking on it in the generated HTML reports.

Failing on version conflict

If the default Gradle resolution strategy of using the newest version of a (transitive) dependency is not solving the problem, we can choose to let the build fail if there is a version conflict. To run the build successfully again, we must explicitly solve the version conflict in our build file.

In the following example build file, we configure the resolution strategy for the runtime configuration to fail if there is a version conflict. The resolutionStrategy method accepts a configuration closure where we invoke the failOnVersionConflict method. The following code shows this:

apply plugin: 'java'

repositories.jcenter()

configurations {
  runtime {
    resolutionStrategy {
      // If there is a version conflict,
      // then the build must fail.
      failOnVersionConflict()
    }
  }

  // Alternatively we could apply
  // this to all configurations:
  // all {
  //     resolutionStrategy {
  //         failOnVersionConflict()
  //     }
  // }
}

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

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

The build is now configured to fail on a version conflict. We know from the previous examples in this chapter that there is a version conflict on slf4j-api. We now execute the dependencies task to see what happens:

$ gradle -q dependencies

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

runtime - Runtime classpath for source set 'main'.

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':dependencies'.
> Could not resolve all dependencies for configuration ':runtime'.
  > A conflict was found between the following modules:
      - org.slf4j:slf4j-api:1.7.7
      - org.slf4j:slf4j-api:1.7.6

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

We see that the build has failed this time. In the output, we see why. There is a conflict between the org.slf4j:slf4j-api:1.7.7 and org.slf4j:slf4j-api:1.7.6 modules.

Forcing a version

We can force Gradle to use a specific version for a dependency in our project. The dependency can also be transitive. We use the configuration closure for a dependency and set the force property with the value true. This instructs the Gradle dependency resolution process to always use the specified version for this dependency, even when the dependency is a transitive dependency in the dependency graph.

In our example build file, we have a version conflict. We can fix this by forcing Gradle to use the version 1.7.7 for the org.slf4j:slf4j-api dependency. The following example build file applies the force property:

apply plugin: 'java'

repositories.jcenter()

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

dependencies {
  compile 'org.slf4j:slf4j-api:1.7.7', {
    // Force Gradle to use this version
    // for this dependency (even transtive).
    force = true
  }

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

Let's run the dependencies task to see whether the version conflict is now resolved:

$ 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)

We have resolved the version conflict, and the build is now successful again. We can also see in the output that for the org.slf4j:slf4j-api:1.7.6 transitive dependency, the version is now the version 1.7.7.

Instead of setting the force property in the dependency configuration, we can also force a version for a dependency as part of the resolutionStrategy method in the configurations configuration block. We use the force method to add a dependency with a forced version. Alternatively, we can use the forcedModules property to define all forced dependencies. This might be a better solution because we can have multiple dependencies with a forced version and put them all together in the resolutionStrategy configuration closure for a more readable and maintainable build file.

In the next example build file, we will force the version of the org.slf4j:slf4j-api dependency to be 1.7.7, but this time as part of the resolutionStrategy configuration:

apply plugin: 'java'

repositories.jcenter()

configurations {
  runtime {
    resolutionStrategy {
      failOnVersionConflict()

      // Make sure version 1.7.7 is used for
      // (transitive) dependency org.slf4j:slf4j-api.
      force 'org.slf4j:slf4j-api:1.7.7'

      // Alternate syntax is to define the
      // forced module collection.
      // forcedModules = ['org.slf4j:slf4j-api:1.7.7']
    }
  }
}

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

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

When we execute the dependencies task from the command line, we see that the version 1.7.7 is used for all org.slf4j:slf4j-api dependencies:

$ 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)
..................Content has been hidden....................

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