springspring-bootbuild.gradledependency-management

Is there a spring.dependency-management plugin supported way to require minimum transitive dependency versions without strictly setting them?


I'm using the spring dependency management plugin, which I know uses some internal variables to control strict dependency versions.

For instance, with spring boot version 3.2.11 and plugin version 1.1.6, xmlunit2.version is strictly set to 2.9.1, meaning org.xmlunit:xmlunit-core will deterministically be 2.9.1.

However, in a case where xmlunit-core has a CVE under version 2.10.0, it's possible to override this variable in the build.gradle file like so:

ext {
  set("xmlunit2.version", "2.10.0")
}

This successfully overrides the version, but creates a problem for the future. Let's say someday we upgrade springboot to a new version 3.2.99 that includes a safe xmlunit version 2.99.0. If we don't remember to remove the line in the ext block, we'll forever override the dependency management plugin's xmlunit2.version 2.99.0 with 2.10.0.

As a related and unsolved question elegantly put it, "This means extra coordination effort because we need to document this in our backlog, and even with good documentation on our part there is a risk that we forget to do it, which means the dependency will eventually be outdated - which is the exact opposite of what we wanted to achieve in the first place."

Normal gradle dependency management has a fix for this, where in the build.gradle you can specify a minimum version:

dependencies {
  constraints{
    implementation 'org.xmlunit:xmlunit-core:2.10.0'
  }
}

This sets a required minimum version, but isn't a strict version, so if another package introduces xmlunit-core:2.99.0, we aren't stuck using 2.10.0.

Unfortunately, spring dependency management plugin overrides constraints. And since the xmlunit2.version is just a string containing "2.99.0", writing "2.10.0" over it in the ext{} block is losing the information that 2.99.0 is the recommended version. So setting xmlunit2.version to "2.10.0+" or "[2.10.0,[" or whatever doesn't do anything, because the manager is never getting any indication that there should be a particular higher version to try.

(It should be noted that I do not have the option to drop the dependency management plugin and defer to gradle's normal dependency management strategies, as much as I'd like to. Use of the plugin is ubiquitous across our org, but resolution of these kinds of issues has historically been poorly handled by adding direct dependencies or creating technical debt by setting strict versions in the ext block)

Is there a plugin supported way to set a minimum required version without overriding the manager's higher version?

I envision a deterministic solution. Something like

ext {
  set("xmlunit2.requireVersion", "2.10.0")
}

which would not overwrite xmlunit2.version=2.99.0 and would produce a dependency tree like

|    \--- org.xmlunit:xmlunit-core:2.10.0 -> 2.99.0 (c)

Solution

  • The solution is in fact to just bite the bullet and ditch the spring.dependency-management plugin. Here's a talk where the guy who wrote it begs people to switch to using gradle platforms (gradle version 5+) because it's just better.

    The switch is super simple. Just remove the io.spring.dependency-management plugin from the top of the build.gradle, and then replace

    dependencyManagement {
        imports {
            mavenBom "org.springframework.boot:spring-boot-dependencies:${springBootVersion}" // or whatever BOM you use
        }
    }
    

    with

    dependencies {
        implementation platform("org.springframework.boot:spring-boot-dependencies:${springBootVersion}") // or whatever BOM you use
    }
    

    at the top of the dependencies block.

    Doing this maintains the same recommendations as the plugin did (derived from the BOM), but makes them softer recommendations, able to be overridden by explicit versioning in the build.gradle file. (If you want to instead override even what's in the build.gradle, use enforcedPlatform instead of platform).

    This absolves the need for any of the ext{} stuff (those variables are gone now) and allows you to instead set all your minimum versions in the constraints{} block.

    The one loss in this transition is that the ext{} overrides of the version variables was nice for version managing an entire related group of modules at once (e.g. ext{set('logback.version','1.4.14')} would set versions for ['logback-access', 'logback-classic', 'logback-core'] in one line). For big groups like slf4j, that's a lot of individual overrides in the constraint block. If you're dealing with a lot of modules in the same group that should be on the same version together, you can do so with some light conditional logic in the configurations block.

    configurations.configureEach {
        resolutionStrategy.eachDependency { DependencyResolveDetails details ->
            if (details.requested.group == 'ch.qos.logback' && details.requested.version < '1.4.14') {
                details.useVersion '1.4.14'
            }
        }
    }
    

    Not beautiful, but pretty simple.

    https://docs.spring.io/spring-boot/gradle-plugin/managing-dependencies.html#managing-dependencies.gradle-bom-support