From d34f1768b3c2ec09f8d2a08d1269d7e868607121 Mon Sep 17 00:00:00 2001 From: academey Date: Thu, 24 Jul 2025 08:54:21 +0900 Subject: [PATCH] Exclude spring-boot-devtools from AOT processing and native images in Maven Previously, spring-boot-devtools was only excluded from native images built with Gradle but not with Maven. This inconsistency meant that Maven builds would include devtools in the AOT processing classpath and in the native image, causing build failures. This commit: - Applies DEVTOOLS_EXCLUDE_FILTER to the classpath during both main and test AOT processing in Maven plugin - Adds exclusions for devtools and docker-compose in the native-maven-plugin configuration within spring-boot-starter-parent - Ensures devtools is completely excluded from native images, not just from AOT processing The exclusion happens automatically without requiring any user configuration, ensuring that devtools is properly excluded from native images while maintaining its functionality during development. Fixes gh-32853 Signed-off-by: academey --- .../springframework/boot/maven/AotTests.java | 35 +++++++++++ .../projects/aot-exclude-devtools/pom.xml | 48 +++++++++++++++ .../main/java/org/test/SampleApplication.java | 29 +++++++++ .../aot-test-exclude-devtools/pom.xml | 60 +++++++++++++++++++ .../main/java/org/test/SampleApplication.java | 29 +++++++++ .../java/org/test/SampleApplicationTests.java | 29 +++++++++ .../boot/maven/ProcessAotMojo.java | 2 +- .../boot/maven/ProcessTestAotMojo.java | 2 +- .../spring-boot-starter-parent/build.gradle | 20 +++++++ 9 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-exclude-devtools/pom.xml create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-exclude-devtools/src/main/java/org/test/SampleApplication.java create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-exclude-devtools/pom.xml create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-exclude-devtools/src/main/java/org/test/SampleApplication.java create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-exclude-devtools/src/test/java/org/test/SampleApplicationTests.java diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AotTests.java b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AotTests.java index a96ace27fc82..8fd4c68c252c 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AotTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AotTests.java @@ -195,4 +195,39 @@ protected String buildLog(File project) { return contentOf(new File(project, "target/build.log")); } + @TestTemplate + void whenAotRunsWithDevtoolsInClasspathItIsExcluded(MavenBuild mavenBuild) { + mavenBuild.project("aot-exclude-devtools").goals("package").execute((project) -> { + // The test passes if the build completes successfully. + // If devtools were included, the AOT processing would fail because devtools + // uses features (like class proxies) that are not compatible with native + // images. + Path aotDirectory = project.toPath().resolve("target/spring-aot/main"); + assertThat(aotDirectory).exists(); + // Verify that source files were generated, indicating successful AOT + // processing + Path sourcesDirectory = aotDirectory.resolve("sources"); + assertThat(sourcesDirectory).exists(); + assertThat(collectRelativePaths(sourcesDirectory)).isNotEmpty(); + }); + } + + @TestTemplate + void whenTestAotRunsWithDevtoolsInClasspathItIsExcluded(MavenBuild mavenBuild) { + mavenBuild.project("aot-test-exclude-devtools").goals("process-test-classes").execute((project) -> { + // The test passes if the build completes successfully. + // If devtools were included, the test AOT processing would fail because + // devtools + // uses features (like class proxies) that are not compatible with native + // images. + Path aotDirectory = project.toPath().resolve("target/spring-aot/test"); + assertThat(aotDirectory).exists(); + // Verify that source files were generated, indicating successful AOT + // processing + Path sourcesDirectory = aotDirectory.resolve("sources"); + assertThat(sourcesDirectory).exists(); + assertThat(collectRelativePaths(sourcesDirectory)).isNotEmpty(); + }); + } + } diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-exclude-devtools/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-exclude-devtools/pom.xml new file mode 100644 index 000000000000..27c15bf381df --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-exclude-devtools/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aot-exclude-devtools + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + process-aot + + + + + + + + + org.springframework.boot + spring-boot + @project.version@ + + + org.springframework.boot + spring-boot-devtools + @project.version@ + true + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-exclude-devtools/src/main/java/org/test/SampleApplication.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-exclude-devtools/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..e26ea880dc7a --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-exclude-devtools/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-present the original author or authors. + * + * 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 + * + * https://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 org.test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleApplication.class, args); + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-exclude-devtools/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-exclude-devtools/pom.xml new file mode 100644 index 000000000000..69276c03b23a --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-exclude-devtools/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aot-test-exclude-devtools + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + process-test-aot + + + + + + + + + org.springframework.boot + spring-boot + @project.version@ + + + org.springframework.boot + spring-boot-devtools + @project.version@ + true + + + org.springframework.boot + spring-boot-test + @project.version@ + test + + + org.junit.jupiter + junit-jupiter + @junit-jupiter.version@ + test + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-exclude-devtools/src/main/java/org/test/SampleApplication.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-exclude-devtools/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..e26ea880dc7a --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-exclude-devtools/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-present the original author or authors. + * + * 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 + * + * https://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 org.test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleApplication.class, args); + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-exclude-devtools/src/test/java/org/test/SampleApplicationTests.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-exclude-devtools/src/test/java/org/test/SampleApplicationTests.java new file mode 100644 index 000000000000..973696313bf5 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-exclude-devtools/src/test/java/org/test/SampleApplicationTests.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-present the original author or authors. + * + * 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 + * + * https://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 org.test; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SampleApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ProcessAotMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ProcessAotMojo.java index c6c940e4e2b7..524594db73ed 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ProcessAotMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ProcessAotMojo.java @@ -123,7 +123,7 @@ private String[] getAotArguments(String applicationClass) { private URL[] getClassPath() throws Exception { File[] directories = new File[] { this.classesDirectory, this.generatedClasses }; - return getClassPath(directories, new ExcludeTestScopeArtifactFilter()); + return getClassPath(directories, new ExcludeTestScopeArtifactFilter(), DEVTOOLS_EXCLUDE_FILTER); } private RunArguments resolveArguments() { diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ProcessTestAotMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ProcessTestAotMojo.java index 8cf523bdf114..6b3cf7929eb1 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ProcessTestAotMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ProcessTestAotMojo.java @@ -148,7 +148,7 @@ private String[] getAotArguments() { protected URL[] getClassPath(boolean includeJUnitPlatformLauncher) throws Exception { File[] directories = new File[] { this.testClassesDirectory, this.generatedTestClasses, this.classesDirectory, this.generatedClasses }; - URL[] classPath = getClassPath(directories); + URL[] classPath = getClassPath(directories, DEVTOOLS_EXCLUDE_FILTER); if (!includeJUnitPlatformLauncher || this.project.getArtifactMap() .containsKey(JUNIT_PLATFORM_GROUP_ID + ":" + JUNIT_PLATFORM_LAUNCHER_ARTIFACT_ID)) { return classPath; diff --git a/starter/spring-boot-starter-parent/build.gradle b/starter/spring-boot-starter-parent/build.gradle index d3baf7914acb..6b94f8ab6e35 100644 --- a/starter/spring-boot-starter-parent/build.gradle +++ b/starter/spring-boot-starter-parent/build.gradle @@ -298,6 +298,16 @@ publishing.publications.withType(MavenPublication) { configuration { delegate.classesDirectory('${project.build.outputDirectory}') delegate.requiredVersion('22.3') + exclusions { + exclusion { + delegate.groupId('org.springframework.boot') + delegate.artifactId('spring-boot-devtools') + } + exclusion { + delegate.groupId('org.springframework.boot') + delegate.artifactId('spring-boot-docker-compose') + } + } } executions { execution { @@ -342,6 +352,16 @@ publishing.publications.withType(MavenPublication) { configuration { delegate.classesDirectory('${project.build.outputDirectory}') delegate.requiredVersion('22.3') + exclusions { + exclusion { + delegate.groupId('org.springframework.boot') + delegate.artifactId('spring-boot-devtools') + } + exclusion { + delegate.groupId('org.springframework.boot') + delegate.artifactId('spring-boot-docker-compose') + } + } } executions { execution {