Engineering
Releases
News and Events

Overriding Dependency Versions with Spring Boot

This article explains some of the dependency management tricks that
can be used to create libraries and apps that depend on newer versions
of a transitive dependency than that managed by a platform like
Spring Boot or the
Spring IO Platform. The
examples below uses Reactor as an example
of such a dependency because it is nearing a major new release (2.5.0)
but existing dependency management platforms (Spring Boot 1.3.xq)
declare a dependency on older versions (2.0.7). If you wanted to write
an app that depended on a new version of Reactor through a transitive
dependency on a library, this is the situation you would be faced
with.

It is a reasonable thing to want to do this, but it should be done
with caution, because newer versions of transitive dependencies can
easily break features that rely on the older version in Spring
Boot. When you do this, and apply one of the fixes below, you are
divorcing yourself from the dependency management of Spring Boot and
saying “hey, I know what I am doing, trust me.” Unfortunately,
sometimes you need to do this in order to take advantage of new
features in third party libraries. If you don’t need the new version
of Reactor (or whatever other external transitive dependency you
need), then don’t do this, just stick to the happy path and let Spring
Boot manage the dependencies.

The real life parallel to the toy code in this article would be a
library that explicitly changed the version of something that is
listed inspring-boot-dependencies. Other Boot-based projects,
e.g. various parts of Spring Cloud, define their own *-dependencies
BOM that you can use to manage external dependencies, and for the most
part these do not require new versions of transitive dependencies that
clash with the Spring Boot ones. If they did, this is how they would
have to declare them, and this is how you could opt in to their
version of dependency management. The example that prompted this
article was
spring-cloud-cloudfoundry-deployer
which needs Reactor 2.5 through a transitive dependency on the new
Cloud Foundry Java client. Its
parent has (or should have) a *-dependencies BOM that can be used
wherever one is called for in the fixes listed below.

NOTE: All the code examples below are in the
github repository. You
should mvn install at the top level to get everything set up. The
Maven projects are all laid out as a reactor build, but this is just
for convenience. In principle all the projects could be
independently built and installed (if the relative paths of their
parents were fixed).

The Problem

We have a parent pom that has dependency management for Reactor
(2.0.7). It does this via a Maven property, i.e. in the parent pom:

	<properties>
		<reactor.version>2.0.7.RELEASE</reactor.version>
	</properties>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>io.projectreactor</groupId>
				<artifactId>reactor-core</artifactId>
				<version>${reactor.version}</version>
			</dependency>
		</dependencies>
	</dependencyManagement>

Then we have a library with this parent that wants to use a newer
version of Reactor, so it does this:

	<properties>
		<reactor.version>2.5.0.BUILD-SNAPSHOT</reactor.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>io.projectreactor</groupId>
			<artifactId>reactor-core</artifactId>
		</dependency>
	</dependencies>

Everyone is happy. Here’s a summary of the relationships between the
artifacts:

parent (manages reactor:2.0.7)
\(child)- library
   \(depends on)- reactor:2.5.0

and the actual dependency:tree from Maven (3.3):

[INFO] com.example:example-library:jar:0.0.1-SNAPSHOT
[INFO] \- io.projectreactor:reactor-core:jar:2.5.0.BUILD-SNAPSHOT:compile
[INFO]    \- org.reactivestreams:reactive-streams:jar:1.0.0:compile

Then a user wants to write an app that depends on the library and
would like to re-use the parent, let’s say for other features that we
haven’t included in the simple sample. He does that and finds that
(boo, hoo), the Reactor version is messed up. The relationships for
the app can be summarised like this:

parent
\(child)- app
  \(depends on)- library

and the Maven (3.3) dependency report looks like this:

[INFO] com.example:example-app:jar:0.0.1-SNAPSHOT
[INFO] \- com.example:example-library:jar:0.0.1-SNAPSHOT:compile
[INFO]    \- io.projectreactor:reactor-core:jar:2.0.7.RELEASE:compile
[INFO]       +- org.reactivestreams:reactive-streams:jar:1.0.0:compile
[INFO]       \- org.slf4j:slf4j-api:jar:1.7.12:compile

(i.e. it has the wrong version of Reactor). This is because the parent
has dependency management for Reactor, and there is no explicit
dependency or dependency management of Reactor in the app itself. The
parent always wins in this case and it doesn’t help to add a BOM
(using Maven 3.3 at least) with the right Reactor version - only an
explicit version of Reactor itself will fix it.

This is the same structure (with fewer levels) as a user app generated
from initializr, with a dependency on a
libary that uses Reactor 2.5.0. It has all the same problems, and the
same options for workarounds and fixes.

Workarounds and Fixes

Option 1: Explicitly manage the Reactor dependency in the app:

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>io.projectreactor</groupId>
				<artifactId>reactor-core</artifactId>
				<version>2.5.0.BUILD-SNAPSHOT</version>
			</dependency>
		</dependencies>
	</dependencyManagement>

This is ugly and lots of lines of XML. Note that it doesn’t help to
use a BOM that has this code in it with Maven 3.3 (but does with
Maven 3.4, see below).

$ cd app-1
$ ../mvn dependency:tree
...
[INFO] com.example:example-app-1:jar:0.0.1-SNAPSHOT
[INFO] \- com.example:example-library:jar:0.0.1-SNAPSHOT:compile
[INFO]    \- io.projectreactor:reactor-core:jar:2.5.0.BUILD-SNAPSHOT:compile
[INFO]       \- org.reactivestreams:reactive-streams:jar:1.0.0:compile
...

NOTE: the same amount of XML (or actually slightly less) can be used
to explicitly list the same dependencies in the <dependencies>
section of the POM.

Option 2: Explicitly manage only the Reactor version in the app via a
property:

	<properties>
		<reactor.version>2.5.0.BUILD-SNAPSHOT</reactor.version>
	</properties>

This seems fairly palatable, and it’s a simple rule to follow: if your
project or one of your dependencies needs to override the version of a
transitive dependency that is managed by the parent POM, just add a
version property for that dependency. For this rule to work the parent
POM has to define version properties for all the dependencies that it
manages (the spring-boot-starter-parent does this).

$ cd app-2
$ ../mvnw dependency:tree
...
[INFO] com.example:example-app-2:jar:0.0.1-SNAPSHOT
[INFO] \- com.example:example-library:jar:0.0.1-SNAPSHOT:compile
[INFO]    \- io.projectreactor:reactor-core:jar:2.5.0.BUILD-SNAPSHOT:compile
[INFO]       \- org.reactivestreams:reactive-streams:jar:1.0.0:compile
...

Option 3: Use a BOM with the new Reactor version and Maven 3.4.

I.e. in the app:

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>com.example</groupId>
				<artifactId>example-bom</artifactId>
				<version>0.0.1-SNAPSHOT</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

Maven 3.4 is not released yet, but you can get it from
the repo
e.g. by editing the wrapper.properties in the application
project. This strategy is nice because it fits the Maven dependency
management model quite well, but only works with a version of Maven
that isn’t released yet.

$ cd app-3
$ ./mvnw dependency:tree # N.B. Maven 3.4
...
[INFO] com.example:example-app-3:jar:0.0.1-SNAPSHOT
[INFO] \- com.example:example-library:jar:0.0.1-SNAPSHOT:compile
[INFO]    \- io.projectreactor:reactor-core:jar:2.5.0.BUILD-SNAPSHOT:compile
[INFO]       \- org.reactivestreams:reactive-streams:jar:1.0.0:compile
...

NOTE: the wrapper.properties for this project has been set up to
work at the time of writing. You might have to edit the version
label to get it to work with the latest snapshot.

Option 4: Use Gradle (instead of Maven) and a BOM with the new Reactor
version. There is no parent, since that is a Maven thing, and
dependency management with the available BOMs can be applied using
the spring.io plugin.

$ cd app-4
$ ./gradlew dependencies
...
compile - Dependencies for source set 'main'.
\--- com.example:example-library:0.0.1-SNAPSHOT
     \--- io.projectreactor:reactor-core:2.5.0.BUILD-SNAPSHOT
          \--- org.reactivestreams:reactive-streams:1.0.0
...

Option 5: Don’t use that parent, and adopt the *-dependencies model that
Spring Cloud uses. If the parent has some plugin and property
declarations that you want to re-use, copy those into a new parent,
and use that as your application parent POM. The dependency
management can be declared in a standalone BOM that can then be used
in the <dependencyManagement> section of your application POM, and
if it is declared first it will take precedence over other BOMs for
anything it declares explicitly.

The simple-parent and the bom in the sample code are an example of
splitting the parent up in this way. Then app-5 uses the
simple-parent as a parent and the bom as a BOM.

$ cd app-5
$ ../mvnw dependency:tree
...
[INFO] com.example:example-app-5:jar:0.0.1-SNAPSHOT
[INFO] \- com.example:example-library:jar:0.0.1-SNAPSHOT:compile
[INFO]    \- io.projectreactor:reactor-core:jar:2.5.0.BUILD-SNAPSHOT:compile
[INFO]       \- org.reactivestreams:reactive-streams:jar:1.0.0:compile
...
comments powered by Disqus