How do you parameterize the Spring Boot Gradle plugin?
Asked Answered
C

4

7

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'
Chiasma answered 16/10, 2020 at 13:14 Comment(4)
Hi @Berin, can you elaborate a little bit more on the common build script and project build script part? I have had some experience related to this in the past and I may be able to help you out.Triumphant
@PRC_noob, I provided the entire script to you in my question. I also provided the script for one of our simpler projects to build. What specific questions do you have that the code doesn't answer?Chiasma
For completeness, what version of Gradle are you using? Also, versions of Maven and Java? This info may not resolve the problem, but will assist those trying to investigate the issue.Arbour
Java 11, Gradle 6.6.1. Version of Maven is largely irrelevant to my question. I'm sick of dealing with the way it resolves dependencies (usually to the wrong version).Chiasma
G
0

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 .

Giannini answered 21/10, 2020 at 1:14 Comment(5)
If you look at my central build, you see all the things I want to make the same across every repository. Some things are easily forgotten like the hack required to be able to push a built service to the repository, and others must have consistency like our coverage requirements.Chiasma
Using pluginManagement in settings.gradle will resolve the Spring plugin, but then I can't fix the deploy bug (that Spring should really fix) centrally due to the way gradle resolves dependencies. A solution for one side breaks something else, and that is my greatest frustration with the process.Chiasma
So I tried just specifying the version as "${springBootVersion}" hand had that value in the global gradle.properties. It worked as long as I didn't use the pluginManagement feature. So this seems to be the closest answer to what I need.Chiasma
Please consider accepting my answer if it was what you were after, (seems the bounty magically disappeared.Giannini
For that I am sorry, I have been in meetings non-stop. There was one more possible answer I wanted to try, but ran out of time.Chiasma
B
0

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'
Brake answered 16/10, 2020 at 14:49 Comment(2)
I have a separate repository for every microservice. There are roughly 40 of them in our application. So the sub-project concept does not fit my problem. Also if apply plugin: 'org.springframework.boot' worked, I would use it. Unfortunately Gradle can't find the plugin unless I use the plugins DSL approach.Chiasma
Updated my question to provide more context on my specific problem.Chiasma
G
0

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 .

Giannini answered 21/10, 2020 at 1:14 Comment(5)
If you look at my central build, you see all the things I want to make the same across every repository. Some things are easily forgotten like the hack required to be able to push a built service to the repository, and others must have consistency like our coverage requirements.Chiasma
Using pluginManagement in settings.gradle will resolve the Spring plugin, but then I can't fix the deploy bug (that Spring should really fix) centrally due to the way gradle resolves dependencies. A solution for one side breaks something else, and that is my greatest frustration with the process.Chiasma
So I tried just specifying the version as "${springBootVersion}" hand had that value in the global gradle.properties. It worked as long as I didn't use the pluginManagement feature. So this seems to be the closest answer to what I need.Chiasma
Please consider accepting my answer if it was what you were after, (seems the bounty magically disappeared.Giannini
For that I am sorry, I have been in meetings non-stop. There was one more possible answer I wanted to try, but ran out of time.Chiasma
A
0

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

Arbour answered 21/10, 2020 at 14:15 Comment(1)
I ran out of time before I could attempt this. Before I go down this path, it looks like the buildscript block is defined in each project?Chiasma
K
0

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'
Kirbie answered 22/10, 2020 at 14:22 Comment(2)
When I tried this approach, I must have been missing something in the translation. I couldn't get it to work. I have Gradle 6.6.1 if that makes a difference. Is this in my included file or each build file? Also, where do I specify springBootVersion?Chiasma
Added an example springBootVersion definition @BerinLoritschKirbie

© 2022 - 2024 — McMap. All rights reserved.