Aligning dependency versions
Dependency version alignment allows different modules belonging to the same logical group (a platform) to have identical versions in a dependency graph.
Handling inconsistent module versions
Gradle supports aligning versions of modules which belong to the same "platform".
It is often preferable, for example, that the API and implementation modules of a component are using the same version.
However, because of the game of transitive dependency resolution, it is possible that different modules belonging to the same platform end up using different versions.
For example, your project may depend on the jackson-databind
and vert.x
libraries, as illustrated below:
dependencies {
// a dependency on Jackson Databind
implementation("com.fasterxml.jackson.core:jackson-databind:2.8.9")
// and a dependency on vert.x
implementation("io.vertx:vertx-core:3.5.3")
}
dependencies {
// a dependency on Jackson Databind
implementation 'com.fasterxml.jackson.core:jackson-databind:2.8.9'
// and a dependency on vert.x
implementation 'io.vertx:vertx-core:3.5.3'
}
Because vert.x
depends on jackson-core
, we would actually resolve the following dependency versions:
-
jackson-core
version2.9.5
(brought byvertx-core
) -
jackson-databind
version2.9.5
(by conflict resolution) -
jackson-annotation
version2.9.0
(dependency ofjackson-databind:2.9.5
)
It’s easy to end up with a set of versions which do not work well together. To fix this, Gradle supports dependency version alignment, which is supported by the concept of platforms. A platform represents a set of modules which "work well together". Either because they are actually published as a whole (when one of the members of the platform is published, all other modules are also published with the same version), or because someone tested the modules and indicates that they work well together (typically, the Spring Platform).
Aligning versions natively with Gradle
Gradle natively supports alignment of modules produced by Gradle. This is a direct consequence of the transitivity of dependency constraints. So if you have a multi-project build, and you wish that consumers get the same version of all your modules, Gradle provides a simple way to do this using the Java Platform Plugin.
For example, if you have a project that consists of 3 modules:
-
lib
-
utils
-
core
, depending onlib
andutils
And a consumer that declares the following dependencies:
-
core
version 1.0 -
lib
version 1.1
Then by default resolution would select core:1.0
and lib:1.1
, because lib
has no dependency on core
.
We can fix this by adding a new module in our project, a platform, that will add constraints on all the modules of your project:
plugins {
`java-platform`
}
dependencies {
// The platform declares constraints on all components that
// require alignment
constraints {
api(project(":core"))
api(project(":lib"))
api(project(":utils"))
}
}
plugins {
id 'java-platform'
}
dependencies {
// The platform declares constraints on all components that
// require alignment
constraints {
api(project(":core"))
api(project(":lib"))
api(project(":utils"))
}
}
Once this is done, we need to make sure that all modules now depend on the platform, like this:
dependencies {
// Each project has a dependency on the platform
api(platform(project(":platform")))
// And any additional dependency required
implementation(project(":lib"))
implementation(project(":utils"))
}
dependencies {
// Each project has a dependency on the platform
api(platform(project(":platform")))
// And any additional dependency required
implementation(project(":lib"))
implementation(project(":utils"))
}
It is important that the platform contains a constraint on all the components, but also that each component has a dependency on the platform. By doing this, whenever Gradle will add a dependency to a module of the platform on the graph, it will also include constraints on the other modules of the platform. This means that if we see another module belonging to the same platform, we will automatically upgrade to the same version.
In our example, it means that we first see core:1.0
, which brings a platform 1.0
with constraints on lib:1.0
and lib:1.0
.
Then we add lib:1.1
which has a dependency on platform:1.1
.
By conflict resolution, we select the 1.1
platform, which has a constraint on core:1.1
.
Then we conflict resolve between core:1.0
and core:1.1
, which means that core
and lib
are now aligned properly.
This behavior is enforced for published components only if you use Gradle Module Metadata. |
Aligning versions of modules not published with Gradle
Whenever the publisher doesn’t use Gradle, like in our Jackson example, we can explain to Gradle that all Jackson modules "belong to" the same platform and benefit from the same behavior as with native alignment. There are two options to express that a set of modules belong to a platform:
-
A platform is published as a BOM and can be used: For example,
com.fasterxml.jackson:jackson-bom
can be used as platform. The information missing to Gradle in that case is that the platform should be added to the dependencies if one of its members is used. -
No existing platform can be used. Instead, a virtual platform should be created by Gradle: In this case, Gradle builds up the platform itself based on all the members that are used.
To provide the missing information to Gradle, you can define component metadata rules as explained in the following.
Align versions of modules using a published BOM
abstract class JacksonBomAlignmentRule: ComponentMetadataRule {
override fun execute(ctx: ComponentMetadataContext) {
ctx.details.run {
if (id.group.startsWith("com.fasterxml.jackson")) {
// declare that Jackson modules belong to the platform defined by the Jackson BOM
belongsTo("com.fasterxml.jackson:jackson-bom:${id.version}", false)
}
}
}
}
abstract class JacksonBomAlignmentRule implements ComponentMetadataRule {
void execute(ComponentMetadataContext ctx) {
ctx.details.with {
if (id.group.startsWith("com.fasterxml.jackson")) {
// declare that Jackson modules belong to the platform defined by the Jackson BOM
belongsTo("com.fasterxml.jackson:jackson-bom:${id.version}", false)
}
}
}
}
By using the belongsTo
with false
(not virtual), we declare that all modules belong to the same published platform.
In this case, the platform is com.fasterxml.jackson:jackson-bom
and Gradle will look for it, as for any other module, in the declared repositories.
dependencies {
components.all<JacksonBomAlignmentRule>()
}
dependencies {
components.all(JacksonBomAlignmentRule)
}
Using the rule, the versions in the example above align to whatever the selected version of com.fasterxml.jackson:jackson-bom
defines.
In this case, com.fasterxml.jackson:jackson-bom:2.9.5
will be selected as 2.9.5
is the highest version of a module selected.
In that BOM, the following versions are defined and will be used:
jackson-core:2.9.5
,
jackson-databind:2.9.5
and
jackson-annotation:2.9.0
.
The lower versions of jackson-annotation
here might be the desired result as it is what the BOM recommends.
This behavior is working reliable since Gradle 6.1. Effectively, it is similar to a component metadata rule that adds a platform dependency to all members of the platform using withDependencies .
|
Align versions of modules without a published platform
abstract class JacksonAlignmentRule: ComponentMetadataRule {
override fun execute(ctx: ComponentMetadataContext) {
ctx.details.run {
if (id.group.startsWith("com.fasterxml.jackson")) {
// declare that Jackson modules all belong to the Jackson virtual platform
belongsTo("com.fasterxml.jackson:jackson-virtual-platform:${id.version}")
}
}
}
}
abstract class JacksonAlignmentRule implements ComponentMetadataRule {
void execute(ComponentMetadataContext ctx) {
ctx.details.with {
if (id.group.startsWith("com.fasterxml.jackson")) {
// declare that Jackson modules all belong to the Jackson virtual platform
belongsTo("com.fasterxml.jackson:jackson-virtual-platform:${id.version}")
}
}
}
}
By using the belongsTo
keyword without further parameter (platform is virtual), we declare that all modules belong to the same virtual platform, which is treated specially by the engine.
A virtual platform will not be retrieved from a repository.
The identifier, in this case com.fasterxml.jackson:jackson-virtual-platform
, is something you as the build author define yourself.
The "content" of the platform is then created by Gradle on the fly by collecting all belongsTo
statements pointing at the same virtual platform.
dependencies {
components.all<JacksonAlignmentRule>()
}
dependencies {
components.all(JacksonAlignmentRule)
}
Using the rule, all versions in the example above would align to 2.9.5
.
In this case, also jackson-annotation:2.9.5
will be taken, as that is how we defined our local virtual platform.
For both published and virtual platforms, Gradle lets you override the version choice of the platform itself by specifying an enforced dependency on the platform:
dependencies {
// Forcefully downgrade the virtual Jackson platform to 2.8.9
implementation(enforcedPlatform("com.fasterxml.jackson:jackson-virtual-platform:2.8.9"))
}
dependencies {
// Forcefully downgrade the virtual Jackson platform to 2.8.9
implementation enforcedPlatform('com.fasterxml.jackson:jackson-virtual-platform:2.8.9')
}