Skip to content

Commit

Permalink
Merge pull request #853 from cherylking/springBoot3Support
Browse files Browse the repository at this point in the history
Spring boot3 support
  • Loading branch information
cherylking authored Oct 21, 2023
2 parents 5650daa + 15e8ac6 commit 651a3f9
Show file tree
Hide file tree
Showing 18 changed files with 520 additions and 30 deletions.
14 changes: 12 additions & 2 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,15 @@ jobs:
run: |
mvn -V clean install -f ci.ant --batch-mode --no-transfer-progress --errors -DtrimStackTrace=false -DskipTests
mvn -V clean install -f ci.common --batch-mode --no-transfer-progress --errors -DtrimStackTrace=false -DskipTests
# Run tests that require a minimum of Java 17 or later
- name: Run tests that require a minimum of Java 17 or later
if: ${{ matrix.java == '17' || matrix.java == '21' }}
run:
./gradlew clean install check -P"test.include"="**/TestSpringBootApplication30*" -Druntime=${{ matrix.RUNTIME }} -DruntimeVersion="${{ matrix.RUNTIME_VERSION }}" --stacktrace --info
# Run tests
- name: Run tests with Gradle on Ubuntu
run:
./gradlew clean install check -Druntime=${{ matrix.RUNTIME }} -DruntimeVersion="${{ matrix.RUNTIME_VERSION }}" -P"test.exclude"="**/DevContainerTest*" --stacktrace --info
./gradlew clean install check -P"test.exclude"="**/TestSpringBootApplication30*,**/DevContainerTest*" -Druntime=${{ matrix.RUNTIME }} -DruntimeVersion="${{ matrix.RUNTIME_VERSION }}" --stacktrace --info
# Copy build reports and upload artifact if build failed
- name: Copy build/report/tests/test for upload
if: ${{ failure() }}
Expand Down Expand Up @@ -143,11 +148,16 @@ jobs:
- name: Install ci.common
working-directory: C:/ci.common
run: mvn -V clean install --batch-mode --no-transfer-progress --errors -DtrimStackTrace=false -DskipTests
# Run tests that require a minimum of Java 17 or later
- name: Run tests that require a minimum of Java 17 or later
if: ${{ matrix.java == '17' || matrix.java == '21' }}
run:
./gradlew clean install check -P"test.include"="**/TestSpringBootApplication30*" -Druntime=${{ matrix.RUNTIME }} -DruntimeVersion="${{ matrix.RUNTIME_VERSION }}" --stacktrace --info --no-daemon
# Run tests
- name: Run tests with Gradle on Windows
working-directory: C:/ci.gradle
# LibertyTest is excluded because test0_run hangs
run: ./gradlew clean install check -P"test.exclude"="**/Polling*,**/LibertyTest*,**/GenerateFeaturesTest*,**/DevContainerTest*" -Druntime=${{ matrix.RUNTIME }} -DruntimeVersion="${{ matrix.RUNTIME_VERSION }}" --stacktrace --info --no-daemon
run: ./gradlew clean install check -P"test.exclude"="**/Polling*,**/LibertyTest*,**/GenerateFeaturesTest*,**/TestSpringBootApplication30*,**/DevContainerTest*" -Druntime=${{ matrix.RUNTIME }} -DruntimeVersion="${{ matrix.RUNTIME_VERSION }}" --stacktrace --info --no-daemon
timeout-minutes: 75
# Copy build reports and upload artifact if build failed
- name: Copy build/report/tests/test for upload
Expand Down
8 changes: 5 additions & 3 deletions docs/spring-boot-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,20 @@ The `server.xml` file provided by the `configDirectory` or `serverXmlFile` param
| Feature | Description |
| ------- | ----------- |
| springBoot-1.5 | Required to support applications with Spring Boot version 1.5.x. |
| springBoot-2.0 | Required to support applications with Spring Boot version 2.0.x and above. |
| springBoot-2.0 | Required to support applications with Spring Boot version 2.x. |
| springBoot-3.0 | Required to support applications with Spring Boot version 3.x. |

The Liberty features that support the Spring Boot starters can be found [here](https://www.ibm.com/support/knowledgecenter/SSAW57_liberty/com.ibm.websphere.wlp.nd.multiplatform.doc/ae/rwlp_springboot.html). They should be enabled in the `server.xml` along with the appropriate Spring Boot feature.


### Gradle Compatibility

There is a known build conflict that Spring Boot Gradle plugin 1.5.x is incompatible with Gradle 5.x. As the Spring Boot 1.5.x plugin will not be updated to support Gradle 5.x, consider upgrading the Spring Boot plugin or downgrading Gradle.
The Spring Boot Gradle plugin 3.x requires Java 17 and a minimum of Gradle 7.5. There is a known build conflict that Spring Boot Gradle plugin 1.5.x is incompatible with Gradle 5.x. As the Spring Boot 1.5.x plugin will not be updated to support Gradle 5.x, consider upgrading the Spring Boot plugin or downgrading Gradle.

| Spring Boot version | Advised Gradle version |
| ------------------- | -------------- |
| 2.0.x | 4.x+ |
| 3.x | 7.5+ or 8.x |
| 2.x | 4.x+ |
| 1.5.x | 2.9 or 3.x (Although we observed compatibility up to 4.10, proceed at your own risk)|

Refer to the [current release Spring docs](https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/html/#introduction) to be advised on Gradle compatibility for the latest Spring Boot Gradle plugin.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package io.openliberty.tools.gradle

import io.openliberty.tools.gradle.extensions.ServerExtension
import io.openliberty.tools.gradle.tasks.AbstractServerTask
import io.openliberty.tools.gradle.tasks.AbstractLibertyTask

import org.gradle.api.Project
import org.gradle.api.tasks.TaskDependency
Expand Down Expand Up @@ -92,8 +93,9 @@ public class LibertyTasks {
}

project.deploy {
if (AbstractServerTask.findSpringBootVersion(project) != null) {
if (springBootVersion?.startsWith('2')) {
String springBootVersion = AbstractServerTask.findSpringBootVersion(project)
if (springBootVersion != null) {
if (AbstractLibertyTask.isSpringBoot2plus(springBootVersion)) {
dependsOn 'bootJar'
} else { //version 1.5.x
dependsOn 'bootRepackage'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* (C) Copyright IBM Corporation 2017, 2021.
* (C) Copyright IBM Corporation 2017, 2023.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -78,19 +78,51 @@ abstract class AbstractLibertyTask extends DefaultTask {
return version
}

protected static boolean isSpringBoot1(String springBootVersion) {
if (springBootVersion == null) {
return false
}
return springBootVersion.startsWith('1.')
}

protected static boolean isSpringBoot2plus(String springBootVersion) {
if (springBootVersion == null) {
return false
}
if (springBootVersion.contains('.')) {
String majorVersion = springBootVersion.substring(0,springBootVersion.indexOf('.'))
try {
int majorVersionNumber = Integer.parseInt(majorVersion)
return majorVersionNumber > 1
} catch (NumberFormatException e) {
}
}
return false
}

protected static String getLibertySpringBootFeature(String springBootVersion) {
if (isSpringBoot1(springBootVersion)) {
return "springBoot-1.5"
} else if (isSpringBoot2plus(springBootVersion)) {
String majorVersion = springBootVersion.substring(0,springBootVersion.indexOf('.'))
return "springBoot-"+majorVersion+".0"
}
return null
}

protected static Task findSpringBootTask(Project project, String springBootVersion) {
if (springBootVersion == null) {
return null
}
Task task
//Do not change the order of war and java
if (springBootVersion.startsWith('2.')) {
if (isSpringBoot2plus(springBootVersion)) {
if (project.plugins.hasPlugin('war')) {
task = project.bootWar
} else if (project.plugins.hasPlugin('java')) {
task = project.bootJar
}
} else if (springBootVersion.startsWith('1.')) {
} else if (isSpringBoot1(springBootVersion)) {
if (project.plugins.hasPlugin('war')) {
task = project.war
} else if (project.plugins.hasPlugin('java')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,9 @@ abstract class AbstractServerTask extends AbstractLibertyTask {
}

protected determineSpringBootBuildTask() {
if (springBootVersion ?. startsWith('2') ) {
if (isSpringBoot2plus(springBootVersion)) {
return project.bootJar
}
else if ( springBootVersion ?. startsWith('1') ) {
} else if (isSpringBoot1(springBootVersion)) {
return project.bootRepackage
}
}
Expand Down Expand Up @@ -578,7 +577,7 @@ abstract class AbstractServerTask extends AbstractLibertyTask {
}

protected String getArchiveName(Task task){
if (springBootVersion?.startsWith('1')) {
if (isSpringBoot1(springBootVersion)) {
task = project.jar
}
if (server.stripVersion){
Expand Down Expand Up @@ -662,7 +661,7 @@ abstract class AbstractServerTask extends AbstractLibertyTask {
appList.each { Object appObj ->
Node application = new Node(null, 'application')
if (appObj instanceof Task) {
if (springBootVersion?.startsWith('1')) {
if (isSpringBoot1(springBootVersion)) {
appObj = project.jar
}
application.appendNode('appsDirectory', appDir)
Expand Down
38 changes: 23 additions & 15 deletions src/main/groovy/io/openliberty/tools/gradle/tasks/DeployTask.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,10 @@ class DeployTask extends AbstractServerTask {
private String getArchiveOutputPath() {
String archiveOutputPath;

if (springBootVersion.startsWith('2.')) {
if (isSpringBoot2plus(springBootVersion)) {
archiveOutputPath = springBootTask.archivePath.getAbsolutePath()
}
else if(springBootVersion.startsWith('1.')) {
else if(isSpringBoot1(springBootVersion)) {
archiveOutputPath = springBootTask.archivePath.getAbsolutePath()
if (project.bootRepackage.classifier != null && !project.bootRepackage.classifier.isEmpty()) {
archiveOutputPath = archiveOutputPath.substring(0, archiveOutputPath.lastIndexOf(".")) + "-" + project.bootRepackage.classifier + "." + springBootTask.getArchiveExtension().get()
Expand Down Expand Up @@ -214,23 +214,31 @@ class DeployTask extends AbstractServerTask {
return targetThinAppPath;
}

private isSpringBootUtilAvailable() {
new FileNameFinder().getFileNames(new File(getInstallDir(project), "bin").getAbsolutePath(), "springBootUtility*")
private boolean isUtilityAvailable(File installDirectory, String utilityName) {
String utilFileName = isWindows ? utilityName+".bat" : utilityName;
File installUtil = new File(installDirectory, "bin/"+utilFileName);
return installUtil.exists();
}

private installSpringBootFeatureIfNeeded() {
if (isClosedLiberty() && !isSpringBootUtilAvailable()) {
File installDir = getInstallDir(project)
if (!isUtilityAvailable(installDir, "springBootUtility") && isUtilityAvailable(installDir, "featureUtility")) {
String fileSuffix = isWindows ? ".bat" : ""
File installUtil = new File(getInstallDir(project), "bin/installUtility"+fileSuffix)

def installCommand = installUtil.toString() + " install springBoot-1.5 springBoot-2.0 --acceptLicense"
def sbuff = new StringBuffer()
def proc = installCommand.execute()
proc.consumeProcessOutput(sbuff, sbuff)
proc.waitFor()
logger.info(sbuff.toString())
if (proc.exitValue()!=0) {
throw new GradleException("Error installing required spring boot support features.")
File installUtil = new File(installDir, "bin/featureUtility"+fileSuffix)

// only install springBoot feature that matches required version
String sbFeature = getLibertySpringBootFeature(springBootVersion)
if (sbFeature != null) {
logger.info("Required springBootUtility not found in Liberty installation. Installing feature "+sbFeature+" to enable it.");
def installCommand = installUtil.toString() + " installFeature " + sbFeature + " --acceptLicense"
def sbuff = new StringBuffer()
def proc = installCommand.execute()
proc.consumeProcessOutput(sbuff, sbuff)
proc.waitFor()
if (proc.exitValue()!=0) {
logger.error(sbuff.toString())
throw new GradleException("Error installing required spring boot support feature: "+sbFeature)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* (C) Copyright IBM Corporation 2023.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.openliberty.tools.gradle

import groovy.xml.QName
import org.junit.*
import org.junit.rules.TestName

public class TestSpringBootApplication30 extends AbstractIntegrationTest{
static File resourceDir = new File("build/resources/test/sample.springboot3")
static String buildFilename = "springboot_3_archive.gradle"

File buildDir;

@Rule
public TestName testName = new TestName();

@Before
public void setup() {
buildDir = new File(integTestDir, "/" + testName.getMethodName())
createDir(buildDir)
createTestProject(buildDir, resourceDir, testName.getMethodName() + '.gradle')
}

@After
public void tearDown() throws Exception {
runTasks(buildDir,'libertyStop')
}


@Test
public void test_spring_boot_apps_30() {
try {
runTasks(buildDir, 'deploy', 'libertyStart')

String webPage = new URL("http://localhost:9080").getText()
Assert.assertEquals("Did not get expected http response.","Hello!", webPage)
Assert.assertTrue('defaultServer/dropins has app deployed',
new File(buildDir, 'build/wlp/usr/servers/defaultServer/dropins').list().size() == 0)
Assert.assertTrue('no app in apps folder',
new File(buildDir, "build/wlp/usr/servers/defaultServer/apps/thin-${testName.getMethodName()}-1.0-SNAPSHOT.jar").exists() )
} catch (Exception e) {
throw new AssertionError ("Fail on task deploy.", e)
}
}

@Test
public void test_spring_boot_classifier_apps_30() {
try {
runTasks(buildDir, 'deploy', 'libertyStart')

String webPage = new URL("http://localhost:9080").getText()
Assert.assertEquals("Did not get expected http response.","Hello!", webPage)
Assert.assertTrue('defaultServer/dropins has app deployed',
new File(buildDir, 'build/wlp/usr/servers/defaultServer/dropins').list().size() == 0)
Assert.assertTrue('no app in apps folder',
new File(buildDir, "build/wlp/usr/servers/defaultServer/apps/thin-${testName.getMethodName()}-1.0-SNAPSHOT-test.jar").exists() )
} catch (Exception e) {
throw new AssertionError ("Fail on task deploy.", e)
}
}

@Test
public void test_spring_boot_classifier_apps_30_no_feature() {
try {
runTasks(buildDir, 'deploy')

Assert.assertTrue('defaultServer/dropins has app deployed',
new File(buildDir, 'build/wlp/usr/servers/defaultServer/dropins').list().size() == 0)
Assert.assertTrue('no app in apps folder',
new File(buildDir, "build/wlp/usr/servers/defaultServer/apps/thin-${testName.getMethodName()}-1.0-SNAPSHOT-test.jar").exists() )
} catch (Exception e) {
throw new AssertionError ("Fail on task deploy.", e)
}
}

@Test
public void test_spring_boot_war_apps_30() {
try {
runTasks(buildDir, 'deploy', 'libertyStart')

String webPage = new URL("http://localhost:9080").getText()
Assert.assertEquals("Did not get expected http response.","Hello!", webPage)
Assert.assertTrue('defaultServer/dropins has app deployed',
new File(buildDir, 'build/wlp/usr/servers/defaultServer/dropins').list().size() == 0)
Assert.assertTrue('no app in apps folder',
new File(buildDir, "build/wlp/usr/servers/defaultServer/apps/thin-${testName.getMethodName()}-1.0-SNAPSHOT.war").exists() )
} catch (Exception e) {
throw new AssertionError ("Fail on task deploy.", e)
}
}

@Test
public void test_spring_boot_war_classifier_apps_30() {
try {
runTasks(buildDir, 'deploy', 'libertyStart')

String webPage = new URL("http://localhost:9080").getText()
Assert.assertEquals("Did not get expected http response.","Hello!", webPage)
Assert.assertTrue('defaultServer/dropins has app deployed',
new File(buildDir, 'build/wlp/usr/servers/defaultServer/dropins').list().size() == 0)
Assert.assertTrue('no app in apps folder',
new File(buildDir, "build/wlp/usr/servers/defaultServer/apps/thin-${testName.getMethodName()}-1.0-SNAPSHOT-test.war").exists() )
} catch (Exception e) {
throw new AssertionError ("Fail on task deploy.", e)
}
}

@Test
public void test_spring_boot_dropins_30() {
try {
runTasks(buildDir, 'deploy', 'libertyStart')
String webPage = new URL("http://localhost:9080").getText()
Assert.assertEquals("Did not get expected http response.","Hello!", webPage)
Assert.assertTrue('defaultServer/dropins/spring has no app',
new File(buildDir, "build/wlp/usr/servers/defaultServer/dropins/spring/thin-${testName.getMethodName()}-1.0-SNAPSHOT.jar").exists())
Assert.assertTrue('apps folder should be empty',
new File(buildDir, "build/wlp/usr/servers/defaultServer/apps").list().size() == 0 )
} catch (Exception e) {
throw new AssertionError ("Fail on task deploy.", e)
}
}
}
1 change: 1 addition & 0 deletions src/test/resources/sample.springboot3/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//Empty
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ibm.test.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
@RequestMapping("/")
public String index() {
return "Hello!";
}
}
Loading

0 comments on commit 651a3f9

Please sign in to comment.