I am unable to force a version of a dependency using Gradle. My goal is to use version 0.20.0.RELEASE of the Spring HATEOAS library, but despite all my efforts it keeps resolving to 0.19.0.RELEASE.
I have attempted a number of strategies, both in isolation and in combination with one another. These strategies include, but are possibly not limited to, the following (note that in all cases $springHateoasVersionis defined in the gradle.properties file that resides in the directory that is the parent of the directory for the module declaring the Spring HATEOAS dependency):
#1 (in the build.gradle file for the module that declares the dependency)
apply plugin: 'io.spring.dependency-management'
dependencyManagement {
dependencies {
dependency group:'org.springframework.hateoas', name:'spring-hateoas', version:"$springHateoasVersion"
}
}
#2 (in the build.gradle file for the module that declares the dependency)
compile ("org.springframework.hateoas:spring-hateoas:$springHateoasVersion") { force = true }
#3 (in the build.gradle file of the parent directory)
subprojects {
configurations.all {
resolutionStrategy {
force "org.springframework.hateoas:spring-hateoas:$springHateoasVersion"
}
}
}
I have done my best to research this problem:
This question has an accepted answer, but doesn't seem like an exact match for the problem that I'm experiencing: How can I force Gradle to set the same version for two dependencies?
Neither of these questions seem to have accepted answers: 1) Gradle is not honoring resolutionStrategy.force, 2) Forcing a module version has no effect on generated org.eclipse.wst.common.component.
In addition to the fact that my project is broken (because I'm using the wrong version of Spring HATEOAS), I can explicitly see that Gradle is "consciously" selecting the incorrect dependency version despite all my protestations. When I run ./gradlew dependencyInsight --dependency spring-hateoas, I see the following output:
org.springframework.hateoas:spring-hateoas:0.19.0.RELEASE (selected by rule)
org.springframework.hateoas:spring-hateoas:0.20.0.RELEASE -> 0.19.0.RELEASE
\--- project :Commons
\--- compile
Despite the name, the dependencyInsight task provides surprisingly little insight into which rule caused Gradle to select the inappropriate dependency version, let alone how I might go about circumventing said rule.
I found the solution to this problem here. Of course this was the one thing I didn't try because it "didn't seem material". :-/
In order to get things working, I added the following to the build.gradle file of the parent directory (relative to the directory for the module that declared the dependency on Spring HATEOAS).
subprojects {
apply plugin: 'io.spring.dependency-management'
dependencyManagement {
applyMavenExclusions false
}
ext['spring-hateoas.version'] = "$springHateoasVersion"
}
honored by e.g.
allprojects {
repositories {
exclusiveContent {
filter {
includeGroup "com.facebook.react"
}
forRepository {
maven {
url "$rootDir/../node_modules/react-native/android"
}
}
}
}
...
}
ref to https://github.com/facebook/react-native/issues/35204#issuecomment-1304740228
Related
I know this is a common issue and I'm embarrassed to be asking it but I can't work out why I cant load the main class of my multimodule Springboot app.
Full stacktrace:
Exception in thread "main" java.lang.ClassNotFoundException: space.forloop.addon.app.Main
at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:471)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:589)
at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:151)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:398)
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:46)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:107)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:58)
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88)
A have a root-level gradle.build file
plugins {
id 'org.springframework.boot' version '2.4.1'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
}
repositories {
mavenCentral()
}
bootJar {
mainClass = 'space.forloop.addon.app.Main'
}
def javaProjects = [
'addon-sync-app',
// Removed, not important
]
javaProjects.each {
name ->
project(":$name") {
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'java'
repositories {
// Removed, not important
}
dependencies {
// Removed, not important
}
}
}
In the package module addon-sync-app I have another build.gradle file which just has:
apply plugin: 'org.springframework.boot'
dependencies {
// Removed, not important
}
Looking at the documentation of Configuring the Main Class I was sure adding:
bootJar {
mainClass = 'space.forloop.addon.app.Main'
}
To the root gradle.build file was the correct thing here, but seems not. Any thing else I might have missed?
You are using the Spring Boot plugin in the wrong way. It's such a common mistake that I think they need to document it better or just make it work out-of-the-box. Oh well.
In a Gradle multi-project, you typically define your support libraries and then one or more runnable or deployable applications.
One thing I can't tell from your description is whether you intend the root project to build that final application, or if it should rather be the addon-sync-app project. Given the name, I am assuming the latter, but it can be either one (but generally not both at the same time).
When you apply the Spring Boot plugin to a project (and it doesn't matter if it is the root project or a sub-project), and you rely on defaults, it will take that project and make it into a "fat jar", which requires a special classloader to run. This makes it unsuitable as a normal library. So when you try to depend on it in a normal way, your classes will not be found.
To fix it, you should only apply the Spring Boot plugin to the project that builds the final application jar. If that is addon-sync-app, then remove it from the root and all non-application sub-projects.
If instead, you want the root project to produce the final application, you need to create dependencies to all the required sub-projects and, just as before, remove the Spring Boot plugin from them as well.
One thing you lose when not applying the Spring Boot plugin is the automatic dependency to the BOM which defines default versions of dependencies. If you like to keep using that without creating fat jars of your libraries, there are a few different ways to handle that. I wrote a (little bit too long) answer on that here.
As per gradle documentation, you can try to add 'application' plugin.
https://docs.gradle.org/current/userguide/application_plugin.html
plugins {
id 'application'
}
application {
mainClass = 'space.forloop.addon.app.Main'
}
I have a gradle monolithic project with too many dependencies.
I'd like to explode it into many sub-projects and publish all sub-projects (build + sources + javadoc) + an extra project being the merge of all sub-projects.
This extra project should be like a virtual artifact with all my projects in a single jar like it is today because I don't want a too big change for my users.
The jar must not include dependencies (it is not an uber-jar) but the resulted pom.xml must contain the dependencies of all sub-projects (the generated pom.xml of the maven artifact must contain all dependencies).
The virtual artifact will include the merge of javadoc and sources too in order to respect Maven Central conventions.
Current state:
Project Main, generate
pom.xml
main.jar
main-sources.jar
main-javadoc.jar
Expected state:
Subproject A, generate
A-pom.xml
A.jar
A-sources.jar
A-javadoc.jar
Subproject B, generate
B-pom.xml
B.jar
B-sources.jar
B-javadoc.jar
virtal-Project Main, generate
pom.xml=A-pom.xml+B-pom.xml
main.jar=A.jar+B.jar
main-sources.jar=A-sources.jar+B-sources.jar
main-javadoc.jar=A-javadoc.jar+B-javadoc.jar
How can I manage it?
We have been in exactly the same situation for some time now. We want to publish a single artifact for our clients to depend on, although internally the product is developed through a few separate component projects. I got it done eventually (with compromises), and here is what I learned:
Merging jars is not as straightforward as it looks like because there could be things like resource files within a jar that are not
always namespace-ed. It is possible that two of your jars have a
resource file with the same name, in which case you will have to
merge the content of those files.
Javadoc is very hard to merge without accessing the original source
files because it has summary pages (index pages).
So my advice would be:
Think twice, maybe what you really want is NOT a single jar, but a single dependency for your clients? These are different. You can easily have a pom only artifact. Depending on this pom only artifact will simply translates transitively into depending on individual artifacts of your component sub projects. To your client, practically, nothing is changed. Spring Boot takes this approach. To do it, you can create an empty java-library project, make all your component projects its api dependency. You don't even need any source code in this project.
If you really want to merge into a single jar, you can try building a fat jar with customization. The customization is not to pull in 3rd party dependencies.
We use the Gradle Shadow plugin for merging jars. Its original purpose was to build a fat jar, which will include all the transitive dependencies. But it also has a special "shadow" configuration, to which you can add dependencies if you want the dependencies to be exported into POM rather than bundled. So what you need to do:
Define a non-transitive configuration (say bundler) to which you will add your sub-project as dependencies. This is going to be the target configuration for the Gradle Shadow plugin.
Define a transitive configuration (bundlerTransitive) that extends from your non-transitive one. This will be manually resolved in order to find the 3rd party dependencies
in your build.gradle, register an afterEvaluate closure, where you find the level two dependencies of the resolved transitive configuration, add them to the shadow configuration. The reason for level-two is that level one dependencies will be your sub-project artifacts.
After all the above, the artifact produced by shadowJar task is the one to be uploaded to maven. You will need to configure the shadowJar task to remove the classifier (which is shadow by default)
Here is a complete example (build.gradle) of bundling vertx-web and all its dependencies within the io.vertx group:
plugins {
id 'java'
id 'maven-publish'
id 'com.github.johnrengelman.shadow' version '5.2.0'
}
group 'org.example'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
configurations {
bundler {
transitive = false
}
bundlerTansitive {
extendsFrom bundler
transitive = true
}
}
dependencies {
bundler "io.vertx:vertx-web:4.0.0"
bundler "io.vertx:vertx-web-common:4.0.0"
bundler "io.vertx:vertx-core:4.0.0"
bundler "io.vertx:vertx-auth-common:4.0.0"
bundler "io.vertx:vertx-bridge-common:4.0.0"
}
shadowJar {
configurations = [project.configurations.bundler]
classifier ''
}
publishing {
publications {
shadow(MavenPublication) { publication ->
project.shadow.component(publication)
}
}
}
project.afterEvaluate {
// this is needed because your sub-projects might have inter-dependencies
def isBundled = { ResolvedDependency dep ->
return configurations.bundler.dependencies.any {
dep.moduleGroup == it.group && dep.moduleName == it.name
}
}
logger.lifecycle '\nBundled artifacts and their 1st level dependencies:'
// level one dependencies
configurations.bundlerTansitive.resolvedConfiguration.firstLevelModuleDependencies.forEach {
logger.lifecycle "+--- ${it.getName()}"
// level two dependencies
it.children.findAll({ ResolvedDependency dep -> !isBundled(dep) })
.forEach { ResolvedDependency dep ->
logger.lifecycle "| +--- ${dep.name}"
project.dependencies.add('shadow', [group: dep.moduleGroup, name: dep.moduleName, version: dep.moduleVersion])
}
}
logger.lifecycle '\nExported Dependencies:'
configurations.shadow.getResolvedConfiguration().getFirstLevelModuleDependencies().forEach {
project.logger.lifecycle "+--- ${it.getName()}"
}
}
For javadoc if you don't care about the index (compromise, as I said), then it is just a jar task with a copy spec:
configurations {
javadoc {
transitive = false
}
}
dependencies {
javadoc 'com.my:component-a:1.1.0:javadoc'
javadoc 'com.my:component-b:1.1.0:javadoc'
javadoc 'com.my:component-c:1.1.0:javadoc'
javadoc 'com.my:component-d:1.1.0:javadoc'
}
task javadocFatJar(type: Jar) {
archiveClassifier.set('javadoc')
from {
configurations.javadoc.collect { it.isDirectory() ? it : zipTree(it) }
}
with jar
}
This cannot be done with maven-publish directly, but one can add individual java-library modules and package each of them with sources and docs. With Gradle this would be a simple jar task, but when the artifacts are publicly available ...such transitive dependencies should better be provided by a meta package; nothing but Maven (Local/Central) dependencies, instead of embedded JARS. In this case, this would be just another module (which obviously would only build after having published the others).
And concerning the concept, that it would require any "merged" JavaDocs ...
https://central.sonatype.org/pages/requirements.html#supply-javadoc-and-sources
While they're referenced (Maven Central) in *.pom, Gradle will be able to find them.
Just use repository mavenLocal() instead of mavenCentral() for testing purposes.
We are looking to migrate from Maven to Gradle, and have worked through most of the challenges you would expect for replacing the parent POM concept. There is one sticky point that we haven't figured out yet. We need to specify the version of Spring Boot we are using globally, but I run into invalid build file problems with both of the solutions I've tried:
I tried putting the plugins { id 'org.springframework.boot' version '2.1.17.RELEASE' } declaration in the common build script. Build error, "Only Project and Settings build scripts can contain plugins {} blocks."
I tried calling the common build file to specify the springBootVersion parameter and using that in the plugins declaration. Build Error, "only buildscript {} and other plugins {} script blocks are allowed before plugins {} blocks, no other statements are allowed"
All of this would be easier if I could simply apply plugin: 'org.springframework.boot' but then Gradle can't find the plugin. All but one microservice are on a single version of Spring Boot, and we want to be able to upgrade globally if possible.
Additional Information
I have ~40 microservices plus some libraries used by those services
Separate repository for each of them, so the normal parent/child approach does not work
Maven parent POMs allowed you to publish that POM as it's own resource, and there is no 1:1 equivalent feature in Gradle
Gradle pluginManagement concept also doesn't work for us because it resolves the Spring Boot plugin but the dependency management plugin now can't be found.
My common build script is included here:
repositories {
mavenLocal()
/* Removed our internal repositories */
jcenter()
mavenCentral()
}
apply plugin: 'java'
apply plugin: 'jacoco'
apply plugin: 'maven-publish'
apply plugin: 'io.spring.dependency-management'
group = 'nedl-unified-platform'
/* Required to publish Spring Boot microservices to publish to repository */
configurations {
[apiElements, runtimeElements].each {
it.outgoing.artifacts.removeIf { it.buildDependencies.getDependencies(null).contains(jar) }
it.outgoing.artifact(bootJar)
}
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
withJavadocJar()
withSourcesJar()
}
ext {
set('springBootVersion', '2.1.17.RELEASE')
set('springCloudVersion', "Greenwich.SR6")
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
jacoco {
toolVersion = "0.8.5"
reportsDir = file("$buildDir/reports/jacoco")
}
test {
finalizedBy jacocoTestReport // report is always generated after tests run
}
jacocoTestCoverageVerification {
violationRules {
rule {
limit {
minimum = 0.2
}
}
}
}
jacocoTestReport {
dependsOn test // tests are required to run before generating the report
reports {
xml.enabled true
html.destination file("${reportsDir}/jacocoHtml")
xml.destination file("${reportsDir}/jacocoReport.xml")
}
}
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
publishing {
publications {
maven(MavenPublication) {
from components.java
}
}
repositories {
/* excluded for privacy and brevity's sake, our internal Maven repo */
}
}
And that is called by our project build script that I want to parameterize:
plugins {
id 'org.springframework.boot' version springBootVersion
}
apply from: "https://mycentral.repo/project-common/develop/build.gradle"
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
implementation 'ch.qos.logback:logback-classic'
implementation 'javax.annotation:javax.annotation-api:1.3.2'
implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359'
implementation 'org.glassfish.jaxb:jaxb-runtime:2.4.0-b180830.0438'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
version = '0.0.2-SNAPSHOT'
I think the gap here is that in maven you have the concept of a parent pom, whereas in Gradle you don't. There is no 1:1 mapping to this like you say, but you can have plugins in Gradle, and apply a plugin.
The closest thing you would have is if you developed your own Gradle plugin, which each of your projects could apply. Your custom plugin would then configure Spring Boot among whatever else is common to all your projects. This plugin would define the version of Spring Boot you want all your other projects to use.
You wouldn't get much benefit to a custom plugin if it's only concern is configuring Spring Boot, it would need to do other things as well. It can be difficult to create a Gradle plugin when you don't have allot of experience in it. You lose all the familiar syntax to the build.gradle and you literally have to write code, (there are some similarities but I have found it difficult), I would avoid it if possible.
I would suggest you start off by applying the spring boot plugin directly to one of your microservices projects, get that working, then do another. After you have done a number of them you will then be able to see what is common between them, and if it is indeed worth investing into developing a global plugin. You really need to be careful though because your global plugin has the potential to be both a blessing and curse. It may take away allot of manual work for maintainers, but if you get it wrong it will cause them grief, and then they will want to go back to maven.
I'm not sure if I understand your globally defined Spring version requirement. Unless you are using SNAPSHOT dependencies/plugins (bad don't do that), (or a black magic settings.gralde outside of your repo), you will have to put some version somewhere. As an alternative you could create your own custom task which runs on the check lifecycle which will check the version of spring (or your plugin) and print a warning if it's not the latest version, and encourage the developer to upgrade.
Extra Info
Parameterizing plugins with properties can be done putting your property in gradle.properties as springBootVersion=2.1.17.RELEASE .
I'm not sure I understood your issue perfectly but you should use the Gradle way for sharing configuration : the root project config.
Instead of including the common build script in every project, create a global project and set the configuration here.
root
|
| --- projectA
| --- projectB
| --- projectC
With the according settings.gradle
include 'projectA'
include 'projectB'
include 'projectC'
In the root build.gradle, set up the version
ext.springBootVersion = '2.1.17.RELEASE'
In subprojects using springBoot, let's say projectB, apply the plugin in the sub build.gradle
buildscript {
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
}
}
apply plugin: 'org.springframework.boot'
This example works for me, though I may not understand all of the constraints.
If we abstract the version of Spring Boot behind a fixed URI (e.g. on an internal CI/CD server), then consider this in each project/repo's build.gradle:
buildscript {
repositories {
jcenter()
}
def SPRING_BOOT_VERSION_URI = 'http://localhost:5151/api-server/spring-boot.txt'
ext.springBootVersion = new URL(SPRING_BOOT_VERSION_URI).getText().trim()
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
}
}
apply plugin: 'org.springframework.boot'
apply from: "../common/build.gradle"
I realize the original question states that the apply plugin doesn't work, but it's not clear to me if that precludes this method.
Finally, note that it is easy to expand this beyond a simple text-file to be a more formal JSON specification (tailored to the teams' needs).
If you add this to the root project, all child projects should be able to just import from the same set of Spring Boot dependencies. The magic ingredient is the allprojects block:
buildscript {
repositories {
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
ext {
springBootVersion = '2.3.4.RELEASE'
}
allprojects {
apply plugin: 'java'
apply plugin: 'io.spring.dependency-management'
dependencyManagement {
imports {
mavenBom("org.springframework.boot:spring-boot-dependencies:${springBootVersion}")
}
}
}
apply plugin: 'org.springframework.boot'
If I have module A and module B that both needs a JSON library (say GSON),
and then I have application C which includes module A & B. There is a chance I will get two different versions of the JSON library if module A and B use different versions? Or does gradle remove one of them and just use one of the versions?
What if I have even more modules, updating them to use the same version of a dependency seems like a lot of work. In this case, letting Application C decide which version to use over all modules would be nice (but not always working I guess because of backwards compatability). Anyway to achieve this?
So my questions is how to best handle dependencies which are very common in many modules. Should I have a Json wrapper that I inject in all my modules instead, letting the Application define what to use?
I guess logging could be a similar dependency
Yes. If you specifically ask for different versions of the same library in projects A and B, you might end up with different versions of the same direct dependency.
As to transient dependencies, the default behaviour is to settle on the newest version of the requested dependency. Please mind the word newest as opposed to highest version requested. This is fine as long as the versions are backward compatible with the lowest version your project actually expects.
Luckily, gradle has several built in methods to settle dependency conflicts.
I have written extensively on the subject here: http://www.devsbedevin.net/android-understanding-gradle-dependencies-and-resolving-conflicts/
TL;DR
You may choose to fail on conflicts:
configurations.all {
resolutionStrategy {
failOnVersionConflict()
}
}
Force a specific dependency:
configurations.all {
resolutionStrategy {
force 'asm:asm-all:3.3.1', 'commons-io:commons-io:1.4', 'com.parse.bolts:bolts-android:1.+'
}
}
Prefer your own modules:
configurations.all {
resolutionStrategy {
preferProjectModules()
}
}
Replace all instances of libary X with Y (either a library, a module or a project):
configurations.all {
resolutionStrategy {
dependencySubstitution {
substitute module('commons-io:commons-io:2.4') with project(':my-commons-io')
}
}
}
Exclude all transient dependencies for a specific library and add the necessary libraries by manually:
dependencies {
compile('com.android.support:appcompat-v7:23.1.0') {
transitive = false
}
}
Exclude a specific transitive dependency:
dependencies {
compile('com.android.support:appcompat-v7:23.1.0') {
exclude group: 'com.parse.bolts'
}
}
Force your project to use a specific version regardless of the actual dependency requirements:
dependencies {
compile('com.parse.bolts:bolts-android:1.+') {
force = true
}
}
Consider a Gradle build that has two modules/projects. The settings.gradle file:
include 'modA', 'modB'
Let's say that both use commons-lang3-3.6, and modA uses gson-2.8.1, whereas modB uses gson-2.2.4. One could configure this in the build.gradle file at the root:
subprojects { p ->
apply plugin: 'java'
repositories {
jcenter()
}
dependencies {
compile 'org.apache.commons:commons-lang3:3.6'
}
if (p.name == 'modA') {
dependencies {
compile 'com.google.code.gson:gson:2.8.1'
}
} else if (p.name == 'modB') {
dependencies {
compile 'com.google.code.gson:gson:2.2.4'
}
}
}
The desired config can be confirmed with:
$ gradle :modA:dependencies
$ gradle :modB:dependencies
I have a Gradle project which depends on another Gradle project. My project structure is like this:
project1
build.gradle
settings.gradle
src/
project2
build.gradle
settings.gradle
src/
in project1/build.gradle I want to add project2 as a dependency:
version '1.0-SNAPSHOT'
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
sourceSets {
main {
java {
srcDirs = [ 'src' ]
}
}
}
include ':project2'
project(':project2').projectDir = new File("../project2")
dependencies {
compile project(':project2')
}
Unfortunately, I'm always getting this error message:
Error:(21, 0) Could not find method include() for arguments [:project2] on root project 'project1' of type org.gradle.api.Project.
I'm using Gradle 3.5 and I'm getting this error both on the command line (gradle build) and in IntelliJ. I found a few StackOverflow threads about the same issue (this and this), but they were not helpful.
The Gradle multi-project documentation also doesn't mention any specific requirements which I may be missing that can cause the error.
When I leave the include call out, I get the message that the project path could not be found in the root project.
I also tried moving the dependency to a subdirectory of project1, but without success.
I wonder what I'm doing wrong and why apparently not many other people are having the same problem. I'd be grateful for hints.
Note: this is not an Android project.
As pointed out in the first comment, include actually needs to go into settings.gradle and not into build.gradle. The same applies to changing the projectDir property.
Comment 3 gave also me another idea. The project can be included in settings.gradle as follows:
includeBuild '../project2'
and in project1/build.gradle I specify the dependency as
dependencies {
compile 'group:project2:version'
}
I generally like this better, since it's less code and looks cleaner. The downside, however, is that recursive composite builds aren't possible. So if project2 itself is also a composite build, this won't work.