Overriding Dependency Versions with Spring Boot

Engineering | Dave Syer | April 13, 2016 | ...

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
...

Get the Spring newsletter

Stay connected with the Spring newsletter

Subscribe

Get ahead

VMware offers training and certification to turbo-charge your progress.

Learn more

Get support

Tanzu Spring offers support and binaries for OpenJDK™, Spring, and Apache Tomcat® in one simple subscription.

Learn more

Upcoming events

Check out all the upcoming events in the Spring community.

View all