Seeking feedback on dependency management approach

Hi all,

When I first used Gradle a year ago (or so), the first thing I missed (coming from Maven) was a centralized way of defining dependency versions for a (multi-module) project, similar to Maven’s <dependencyManagement>. I initially started doing like many and using variables and referencing them from the projects’ dependencies; e.g.

def fooVersion = '1.0'
ext.libs = [
  someDependency: "com.example:some-dependency:$fooVersion",
  otherDependency: dependencies.create("com.example:other-dependency:$fooVersion") {
      exclude module: 'slf4j-simple'
    },
  yetAnotherDep: [ 'com.example:yet-another:2.0', 'com.example:some-optional-dep:3.0' ]
]

dependencies {
  compile libs.someDependency
  test    libs.otherDependency
  runtime libs.yetAnotherDep
}

For a few months, I’ve started using a new (to me) approach that has the advantage of applying to the buildscript too, and managing versions of transitive dependencies (and easy replacement of dependencies). I’d like to know what you think about it.

I create a file called dependency-management.gradle where I’ll put all my versions and rules (see below), and I apply it to all projects and their buildscripts with the following snippet in my root build.gradle:

buildscript {
  apply from: "$rootDir/dependency-management.gradle", to: it
  dependencies {
    classpath 'net.ltgt.gradle:gradle-errorprone-plugin'
  }
}
subprojects*.buildscript {
  apply from: "$rootDir/dependency-management.gradle", to: it
}
allprojects {
  apply from: "$rootDir/dependency-management.gradle"
}

Dependencies are then declared without version in my projects (even if they had a version, it would be overwritten), and that also applies to classpath dependencies in buildscript (as you can see above)

Here’s what my dependency-management.gradle looks like:

// Note: this script is applied both to Project and ScriptHandler.
// Beware then to only use properties common to both objects,
// specifically 'configurations', 'dependencies', and 'repositories'.

repositories {
  mavenCentral()
}

dependencies.modules {
  module('javax.ws.rs:jsr311-api') {
    replacedBy('org.jboss.resteasy:jaxrs-api')
  }
}

configurations.all {
  exclude group: 'javax.ws.rs', module: 'jsr311-api' // conflicts with org.jboss.resteasy:jaxrs-api

  resolutionStrategy {
    force 'net.ltgt.gradle:gradle-errorprone-plugin:0.0.6'

    // All artifacts in a given groupId and whose artifactId has a given prefix: "foo.bar:baz-*" or "foo.bar:baz_*"
    // (note: matches "foo.bar:baz" and "foo.bar:baz-quux")
    def artifactPrefixInGroup = [
      'com.google.errorprone:error_prone':  '2.0.2',
    ]
    // All artifacts in a given groupId: "foo.bar:*"
    def allArtifactsInGroup = [
      'com.google.guava':         '18.0',
      'org.slf4j':                '1.7.12',
      'commons-logging':          [ group: 'org.slf4j', name: 'jcl-over-slf4j']
     ]
    // All artifacts whose groupId has a given prefix: "foo.bar.*:*"
    // (note: matches both "foo.bar:baz" and "foo.bar.baz:quux")
    def allArtifactsInGroupPrefix = [
      'com.google.inject':        '4.0',
    ]

    def selectReplacement
    selectReplacement = { group, name ->
      def selector = group + ":" + name
      def replacement = artifactPrefixInGroup.findResult {
        if (selector == it.key || selector.startsWith(it.key + "-") || selector.startsWith(it.key + "_")) return it.value
      }
      if (!replacement) {
        replacement = allArtifactsInGroup[group]
        if (!replacement) {
          replacement = allArtifactsInGroupPrefix.findResult {
            if (group == it.key || group.startsWith(it.key + ".")) return it.value
          }
        }
      }
      if (replacement instanceof Map) {
        replacement = selectReplacement(replacement.group, replacement.name)
        assert replacement : "Recursively selecting a replacement for ${group}:${name} returned null"
      } else if (replacement instanceof String) {
        // replacement is a version, keep the same 'group' and 'name'
        replacement = [
            group:    group,
            name:     name,
            version:  replacement
        ]
      }
      return replacement
    }

    eachDependency { DependencyResolveDetails details ->
      def replacement = selectReplacement(details.requested.group, details.requested.name)
      if (replacement) {
        details.useTarget(replacement)
      }
    }
  }
}

See here how I ensure that:

  • net.ltgt.gradle:gradle-errorprone-plugin will have version 0.0.6
  • all error prone dependencies (in group com.google.errorprone with name error_prone or error_prone_annotations for example) will have the same version
  • all Guice dependencies (in groups com.google.inject and com.google.inject.extensions) will have the same version
  • similarly, all Slf4j dependencies will have the same version
  • all dependencies on commons-logging:<whatever> will be replaced with a dependency on org.slf4,:jcl-over-slf4j (whose version is then –recursively– resolved to 1.7.12)

I also used global exclude and replacement rules, all centralized in the same file.

You might also want to look at the dependency management plugin from the folks at SpringSource.

Or the dependency recommender plugin from the folks at Netflix.

Oh, I didn’t know about the dependency-management-plugin! At the time I started, the nebula-dependency-recommender wasn’t compatible with the latest version (2.2.1 at the time).

Being plugins, none will manage buildscript dependencies (not a big deal though). More importantly, none provide a DSL for replacing a dependency with another (e.g. commons-logging:commons-logging with org.slf4j:jlc-over-slf4j); that still has to be done using a dependency resolve rule (in the case of commons-logging and jlc-over-slf4j, I suppose a module replacement rule would work too).

Yes, that is correct.