I am developing two projects.
Project 1 is a spring-boot application based on gradle for dependency management. This application defines extension-points. If - at runtime - an extension is found on the classpath, this extension is being called from the main application under certain circumstances.
Project 2 is such an extension. This extension should only provide low-level functionality. So basically, I need spring annotations and an EntityManager within the application but I would like to prevent the full spring-boot dependencies to be present on the compile-path.
The obvious (and not satisfactory) solution is to define a compileonly-dependency on a specific version of, say, spring-context. This is somewhat dangerous, as the spring-boot version may progress and it may be easy to forget to adjust the spring version.
Providing a compileOnly dependency to spring-boot-starter (or even the main project) is out of the question.
So, is there a clever trick to tell gralde to use "the spring-version coming with spring-boot-xxx"?
Sometimes you are within a forrest and not seeing the trees...
Thanks to the comment of #emrekgn I looked for BOM/Gradle/Spring and found... the spring boot dependency-management plugin.
Adding this to your gradle file will allow you to include dependencies matching to the spring boot version you are using:
plugins {
id 'org.springframework.boot' version '2.6.2'
}
apply plugin: 'io.spring.dependency-management'
Obviously, you have to match the boot-version to your needs.
Related
if you have a build.gradle file with the line implementation 'org.springframework.boot:spring-boot-starter-web', how do you choose the version of the jar it downloads so that you get the latest one? I've seen a project where it is a 2.2.4 release, but in another project I've seen the same line with a 2.2.5 release.
Since you dropped the name Spring Boot, I assume the project has been generated Spring Initializr. A project generated with the Initializr has two plugins applied:
org.springframework.boot (Reference Documentation)
io.spring.dependency-management (Reference Documentation)
io.spring.dependency-management is Spring's opinionated way to provide Maven-like dependency management to Gradle builds. It allows to declare dependency versions once and then omit the version when declaring the actual dependency.
The org.springframework.boot plugin does the following:
When you apply the io.spring.dependency-management plugin, Spring Boot’s plugin will automatically import the spring-boot-dependencies bom from the version of Spring Boot that you are using. This provides a similar dependency management experience to the one that’s enjoyed by Maven users. For example, it allows you to omit version numbers when declaring dependencies that are managed in the bom. To make use of this functionality, simply declare dependencies in the usual way but omit the version number.
(From: Managing Dependencies)
What does that mean in practice?
When you generate a project for Spring Boot 2.1.14, your build.gradle will look similar to this:
plugins {
id 'org.springframework.boot' version '2.1.14.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
The org.springframework.boot plugin instructs the io.spring.dependency-management to apply the bill of materials (BOM) of Spring Boot 2.1.14. The BOM declares the following version for spring-boot-starter-web:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.14.RELEASE</version>
</dependency>
(From: Maven Central)
And this combination allows to declare the dependency to spring-boot-starter-web in the build.gradle without providing an actual version:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
If you would change the version of the org.springframework.boot Gradle plugin, then a different version that matches the Spring Boot versions would be applied.
You may ask, why this tremendous effort?
We take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss.
That's why.
One possible solution is to use lockfiles and a version of +, or a combination of major.minor.+ or major.+
implementation 'org.springframework.boot:spring-boot-starter-web:+'
For more information on dependency locking: https://docs.gradle.org/current/userguide/dependency_locking.html
Another approach, and one I'm quite pleased with where available, is using a bill of materials, which specifies versions for a lot of dependencies, by introducing constraints. So where a dependency is used, with no version specified, as in your example, it will get the version the BOM brings in. So for the dependency below, if it is present in the BOM, it will match
implementation 'org.springframework.boot:spring-boot-starter-web
You're also free to override versions manually, by still specifying the version, should you choose to. And a BOM is like any other dependency, so you can use a mixture of lockfiles and BOMs.
Here's gradle documentation on bill of materials: https://docs.gradle.org/current/userguide/platforms.html
I have a very simple springboot application for testing purposes.
Here my build.gradle:
plugins {
id 'org.springframework.boot' version '2.1.0.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'
}
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mandas:docker-client:2.0.2'
}
org.mandas:docker-client:2.0.2 has a transitive dependency on org.glassfish.jersey.core:jersey-client:2.28. However gradle then pulls version 2.27 instead of 2.28.
If I run ./gradlew dependencyInsight --dependency org.glassfish.jersey.core:jersey-client I get the following output:
org.glassfish.jersey.core:jersey-client:2.27 (selected by rule)
...
org.glassfish.jersey.core:jersey-client:2.27
\--- org.glassfish.jersey.connectors:jersey-apache-connector:2.27
\--- org.mandas:docker-client:2.0.2 (requested org.glassfish.jersey.connectors:jersey-apache-connector:2.28)
\--- compileClasspath
org.glassfish.jersey.core:jersey-client:2.28 -> 2.27
\--- org.mandas:docker-client:2.0.2
\--- compileClasspath
It seems that spring-boot-starter-web somehow has a dependency on org.glassfish.jersey.core:jersey-client:2.27. However, if I print all my dependencies with ./gradlew dependencies I do not see a dependency on org.glassfish.jersey.core:jersey-client:2.27 from spring-boot-starter-web.
However, searching a bit around the web, i found another way in tracking down a dependency:
grep -r "2.27" ~/.gradle/caches/modules-2/files-2.1/*
Like this i was able to track down where version 2.27 was introduced. It seems to be declared in the following poms:
spring-boot-dependencies-2.1.0.RELEASE.pom
spring-boot-autoconfigure-2.1.0.RELEASE.pom
My question now is manifold:
First of all, why does spring-boot-starter-web depend on jersey? I was always under the impression that if we want to explicitly use jersey over the spring implementation we would include spring-boot-starter-jersey.
Why can't I see that spring-boot-starter-web depends on org.glassfish.jersey.core:jersey-client:2.27 when running ./gradlew dependencies. Obviously there must be a dependeny on it somewhere as it downgrades the version.
Why is version 2.28 downgraded to version 2.27? How can I know which policy is applied by spring boot in order to make a choice for a specific version.
The application is running perfectly fine, but now as i got a version conflict, how should i best handle this? Is it a viable option to just use v2.28 instead of v2.27. I think this also refers to my first answer on why spring-boot is actually using jersey.
I know these are multiple questions, however I think it is better to ask them in one question instead of spreading them over multiple ones, as they are all related to the same context.
BTW: This is not only happening with org.glassfish.jersey.core:jersey-client. Exactly the same thing applies to org.apache.httpcomponents:httpclient.
Thanks for your help!
First of all, why does spring-boot-starter-web depend on jersey? I was always under the impression that if we want to explicitly use jersey over the spring implementation we would include spring-boot-starter-jersey.
It doesn't. Rather, it depends on Tomcat. You are correct that you would need the jersey starter for auto-configuring that.
Why can't I see that spring-boot-starter-web depends on org.glassfish.jersey.core:jersey-client:2.27 when running ./gradlew dependencies. Obviously there must be a dependeny on it somewhere as it downgrades the version.
Because it doesn't. More on that below.
Why is version 2.28 downgraded to version 2.27? How can I know which policy is applied by spring boot in order to make a choice for a specific version.
This is the underlying problem. I will explain it below.
The application is running perfectly fine, but now as i got a version conflict, how should i best handle this? Is it a viable option to just use v2.28 instead of v2.27. I think this also refers to my first answer on why spring-boot is actually using jersey.
It depends. In my experience, your dependencies could break both by upgrading and downgrading a transitive dependency compared to that they have been built and tested against, even if it is just a minor version (I am looking at you, SnakeYAML!) So you really just have to give it a shot. Usually it is safer to upgrade than to downgrade, but sometimes it will still cause problems.
Here's the deal with the Jersey downgrade.
The Spring Dependency Management plugin is used to control the versions of your dependencies, both the direct and the transitive ones.
When you apply both the dependency management plugin and the Spring Boot plugin, the latter will apply its default versions, which comes from the Spring Boot BOM. You can check which dependencies are managed and in what versions by running gradle dependencyManagement.
The idea with all this is that you get a set of dependencies that are known to work well with each other. If you like a different version of one of the managed dependencies, you will have to configure it using the dependencyManagement extension (as documented here).
This is why your Jersey dependency gets downgraded.
I personally don't use the Spring dependency management plugin as I like the way you work with dependencies in plain Gradle. So I usually just do something like this:
plugins {
id 'org.springframework.boot' version '2.1.0.RELEASE'
id 'java'
}
dependencies {
implementation platform("org.springframework.boot:spring-boot-dependencies:2.1.0.RELEASE")
}
In this case, it will by default use the dependencies from the Spring Boot BOM, but not downgrade them if anyone needs a newer version. But it will upgrade them if needed, and you also don't have to specify a version yourself if you don't want to, in which case it will use the one from the BOM.
I am using Gradle 5's BOM (Bill of Materials) feature. This is how I describe it for my JUnit 5 dependencies:
testImplementation(enforcedPlatform("org.junit:junit-bom:5.4.0")) // JUnit 5 BOM
testImplementation("org.junit.jupiter:junit-jupiter-api")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testImplementation("org.junit.jupiter:junit-jupiter-params")
My assumption is that providing the BOM will resolve the versions of the dependencies to 5.4.0. However, they get resolved to 5.1.1. I am not sure why. (I also request enforcedPlatform() to lock the specified version)
Inspecting JUnit 5's BOM we see that all org.junit.jupiter dependencies are listed with version 5.4.0 (resolving to 5.1.1 in the project) and all org.junit.platform dependencies are listed with version 1.4.0 which resolve correctly in the project.
I am not sure what I am missing and was hoping to get some help here. Thanks!
EDIT:
I used Sormuras response and moved all BOMs at the top of the dependencies {} block but was still not getting version 5.4.0. Then I suspected it might be coming from the Gradle Spring Dependency Management plugin that I use, so when I commented it out, I got version JUnit 5.4.0. How do I disable JUnit coming from the Gradle Spring Dependency Management plugin?
FINALLY:
I decided to use the Spring Boot Dependencies BOM directly and remove the Gradle plugin:
implementation(platform("org.springframework.boot:spring-boot-dependencies:2.0.5.RELEASE"))
I imagine the plugin was created for those version of Gradle before Gradle 5 where you couldn't use a BOM file. Now with the BOM support I can directly include it. This way my version of JUnit is as I have specified it in the enforcedPlatform() block.
I accepted Sam Brannen's answer below because he explains well how the issue occurs and what solves it and I think it's relevant for those who use older versions of Gradle.
How do I disable JUnit coming from the Gradle Spring Dependency Management plugin?
For starters, if you are using the dependency management plugin from Spring, you should not be importing the junit-bom since that results in duplicate (and potentially conflicting) management of those dependencies.
Aside from that, whenever you use the dependency management plugin from Spring and want to override a managed version, you have to do it by overriding the exact name of the version defined in the BOM used by the plugin.
This is documented in Spring Boot for Gradle and for Maven.
For Spring Boot the name of the JUnit Jupiter version is "junit-jupiter.version". You can find the names of all managed versions for Spring Boot 2.1.2 here.
So, in Gradle you would override it as follows.
ext['junit-jupiter.version'] = '5.4.0'.
You can see that I have done exactly that here.
With Maven you would override it as follows.
<properties>
<junit-jupiter.version>5.4.0</junit-jupiter.version>
</properties>
Further background information here: https://docs.spring.io/platform/docs/current/reference/html/getting-started-overriding-versions.html
JUnit 5.4.0 simplified its artifacts, and now delivered a single artifact for Jupiter - org.junit:junit-jupiter. I.e., you should simplify your Gradle file too:
testImplementation(enforcedPlatform("org.junit:junit-bom:5.4.0")) // JUnit 5 BOM
testImplementation("org.junit.jupiter:junit-jupiter")
Ensure to include JUnit's BOM before other BOMs that also refer to JUnit. First BOM wins and locks version of all later artifacts.
See this issue for a similar setup using Maven and Spring Boot: https://github.com/sormuras/junit-platform-maven-plugin/issues/29#issuecomment-456958188
Introduction
So I noticed the following line in the gradle file of the jhipster project:
annotationProcessor ("org.springframework.boot:spring-boot-configuration-processor") {
exclude group: 'com.vaadin.external.google', module: 'android-json'
}
https://github.com/jhipster/jhipster-sample-app-gradle/blob/9e9c3db8f3bedba4b1efd85ecb6ff3f12a5f596a/build.gradle#L230
We also used the same configuration in Maven for another project to solve the following problem: Maven transient dependency (library/jar vaadin json) is not being excluded
Questions
And now I have the following questions:
What does the spring-boot-configuration-processor dependency do?
Why is it necessary to sometimes exclude dependencies from the processor?
Why doesn't the processor necessarily appear in the mvn-dependency tree?
Why are exclusions used with processor in situations where it's very difficult to exclude a dependency?
spring-boot-configuration-processor is an annotation processor that generates metadata about classes in your application that are annotated with #ConfigurationProperties. This metadata is used by your IDE (Eclipse, IntelliJ, or NetBeans) to provide auto-completion and documentation for the properties when editing application.properties and application.yaml files. You can learn a bit more about it in the relevant section of Spring Boot's reference documentation.
Since Spring Boot 1.5.10, the exclusion is no longer necessary as com.vaadin.external.google:android-json is no longer a dependency of spring-boot-configuration-processor.
What does the spring-boot-configuration-processor dependency do?
It scans the libraries in the build and sees what properties they use so as to inform the IDE
Why is it necessary to sometimes exclude dependencies from the processor?
Maven libraries can clash sometimes - the one you reference was excluded by JHipster because it led to errors when on the classpath together with another library in JHipster's dependencies
Why doesn't the processor necessarily appear in the mvn dependency:tree?
It does for me on the jhipster-sample-app. Presumably you're referring to the comment on the linked issue stating that the android-json library isn't in the tree. I've asked there about that.
Why are exclusions used with processor in situations where it's very difficult to exclude a dependency?
This is a dependency clash issue like any other really, it just happens that the processor is bringing in the key dependency (or rather was, as #Andy Wilkinson points out com.vaadin.external.google:android-json is no longer used by the processor)
im using spring boot in a recommended way, that is by adding
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
and then adding dependencies i need, like:
compile('org.springframework.boot:spring-boot-starter-web')
That dependency pulls some predefined version of tomcat that will host my microservice.
but what happens when there is a security fix for tomcat released? does spring team track all the security issues in all the project they use and bump spring-boot version when new fix is released? or do i have to track it by myself and control dependencies (like tomcat) manually instead of using 'the spring-boot way'?
Whenever we release a new version of Spring Boot, we update the managed dependency versions to the latest appropriate release of that dependency. Appropriate means that we won't, for example, move to a new major or minor version of a dependency in a maintenance release of Spring Boot.
Generally speaking, a new version of a managed dependency (even if it contains a security fix) won't trigger the release of a new version of Spring Boot. It's impossible for us to know exactly how a dependency is being used and if the fix is relevant to all, some, or even any of Spring Boot's users.
This means that you do need to keep track of security vulnerabilities yourself. If a vulnerability affects you and Spring Boot has not yet updated its managed version then you can easily override that version in your build script. For example, if you are using Gradle:
ext['tomcat.version']='8.0.36'
Or Maven:
<properties>
<tomcat.version>8.0.36</tomcat.version>
</properties>