diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractAotMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractAotMojo.java index 1158a2d748..c89fb6764f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractAotMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractAotMojo.java @@ -147,7 +147,7 @@ public abstract class AbstractAotMojo extends AbstractDependencyFilterMojo { JavaCompilerPluginConfiguration compilerConfiguration = new JavaCompilerPluginConfiguration(this.project); List options = new ArrayList<>(); options.add("-cp"); - options.add(ClasspathBuilder.build(classPath)); + options.add(ClasspathBuilder.forURLs(classPath).build().argument()); options.add("-d"); options.add(outputDirectory.toPath().toAbsolutePath().toString()); String releaseVersion = compilerConfiguration.getReleaseVersion(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java index c0bdcf7da1..e838bced47 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java @@ -39,6 +39,7 @@ import org.apache.maven.project.MavenProject; import org.apache.maven.toolchain.ToolchainManager; import org.springframework.boot.loader.tools.FileUtils; +import org.springframework.boot.maven.ClasspathBuilder.Classpath; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -345,12 +346,13 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { private void addClasspath(List args) throws MojoExecutionException { try { - String classpath = ClasspathBuilder.build(getClassPathUrls()); + Classpath classpath = ClasspathBuilder.forURLs(getClassPathUrls()).build(); if (getLog().isDebugEnabled()) { - getLog().debug("Classpath for forked process: " + classpath); + getLog().debug("Classpath for forked process: " + + classpath.elements().map(Object::toString).collect(Collectors.joining(File.separator))); } args.add("-cp"); - args.add(classpath); + args.add(classpath.argument()); } catch (Exception ex) { throw new MojoExecutionException("Could not build classpath", ex); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArgFile.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArgFile.java deleted file mode 100644 index c262fda002..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArgFile.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2012-2025 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.springframework.boot.maven; - -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.charset.UnsupportedCharsetException; -import java.nio.file.Files; -import java.nio.file.Path; - -/** - * - * Utility class that represents `argument` as a file. Mostly used to avoid `Path too long - * on ...` on Windows. - * - * @author Moritz Halbritter - * @author Dmytro Nosan - */ -final class ArgFile { - - private final Path path; - - private ArgFile(Path path) { - this.path = path.toAbsolutePath(); - } - - /** - * Creates a new {@code ArgFile} with the given content. - * @param content the content to write to the argument file - * @return a new {@code ArgFile} - * @throws IOException if an I/O error occurs - */ - static ArgFile create(CharSequence content) throws IOException { - Path tempFile = Files.createTempFile("spring-boot-", ".argfile"); - tempFile.toFile().deleteOnExit(); - ArgFile argFile = new ArgFile(tempFile); - argFile.write(content); - return argFile; - } - - private void write(CharSequence content) throws IOException { - Files.writeString(this.path, "\"" + escape(content) + "\"", getCharset()); - } - - private Charset getCharset() { - String nativeEncoding = System.getProperty("native.encoding"); - if (nativeEncoding == null) { - return Charset.defaultCharset(); - } - try { - return Charset.forName(nativeEncoding); - } - catch (UnsupportedCharsetException ex) { - return Charset.defaultCharset(); - } - } - - private String escape(CharSequence content) { - return content.toString().replace("\\", "\\\\"); - } - - @Override - public String toString() { - return this.path.toString(); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ClasspathBuilder.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ClasspathBuilder.java index a4ad501998..7505a4db6b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ClasspathBuilder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ClasspathBuilder.java @@ -20,7 +20,18 @@ import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -31,9 +42,12 @@ import org.springframework.util.StringUtils; * @author Stephane Nicoll * @author Dmytro Nosan */ -final class ClasspathBuilder { +class ClasspathBuilder { - private ClasspathBuilder() { + private final List urls; + + protected ClasspathBuilder(List urls) { + this.urls = urls; } /** @@ -43,47 +57,121 @@ final class ClasspathBuilder { * @return the classpath; on Windows, the path to an argument file is returned, * prefixed with '@' */ - static String build(URL... urls) { - if (ObjectUtils.isEmpty(urls)) { - return ""; - } - if (urls.length == 1) { - return toFile(urls[0]).toString(); - } - StringBuilder builder = new StringBuilder(); - for (URL url : urls) { - if (!builder.isEmpty()) { - builder.append(File.pathSeparator); - } - builder.append(toFile(url)); - } - String classpath = builder.toString(); - if (runsOnWindows()) { - try { - return "@" + ArgFile.create(classpath); - } - catch (IOException ex) { - return classpath; - } - } - return classpath; + static ClasspathBuilder forURLs(List urls) { + return new ClasspathBuilder(new ArrayList<>(urls)); } - private static File toFile(URL url) { + /** + * Builds a classpath string or an argument file representing the classpath, depending + * on the operating system. + * @param urls an array of {@link URL} representing the elements of the classpath + * @return the classpath; on Windows, the path to an argument file is returned, + * prefixed with '@' + */ + static ClasspathBuilder forURLs(URL... urls) { + return new ClasspathBuilder(Arrays.asList(urls)); + } + + Classpath build() { + if (ObjectUtils.isEmpty(this.urls)) { + return new Classpath("", Collections.emptyList()); + } + if (this.urls.size() == 1) { + Path file = toFile(this.urls.get(0)); + return new Classpath(file.toString(), List.of(file)); + } + List files = this.urls.stream().map(ClasspathBuilder::toFile).toList(); + String argument = files.stream().map(Object::toString).collect(Collectors.joining(File.pathSeparator)); + if (needsClasspathArgFile()) { + argument = createArgFile(argument); + } + return new Classpath(argument, files); + } + + protected boolean needsClasspathArgFile() { + String os = System.getProperty("os.name"); + if (!StringUtils.hasText(os)) { + return false; + } + // Windows limits the maximum command length, so we use an argfile + return os.toLowerCase(Locale.ROOT).contains("win"); + } + + /** + * Create a temporary file with the given {@code} classpath. Return a suitable + * argument to load the file, that is the full path prefixed by {@code @}. + * @param classpath the classpath to use + * @return a suitable argument for the classpath using a file + */ + private String createArgFile(String classpath) { try { - return new File(url.toURI()); + return "@" + writeClasspathToFile(classpath); + } + catch (IOException ex) { + return classpath; + } + } + + private Path writeClasspathToFile(CharSequence classpath) throws IOException { + Path tempFile = Files.createTempFile("spring-boot-", ".argfile"); + tempFile.toFile().deleteOnExit(); + Files.writeString(tempFile, "\"" + escape(classpath) + "\"", getCharset()); + return tempFile; + } + + private static Charset getCharset() { + String nativeEncoding = System.getProperty("native.encoding"); + if (nativeEncoding == null) { + return Charset.defaultCharset(); + } + try { + return Charset.forName(nativeEncoding); + } + catch (UnsupportedCharsetException ex) { + return Charset.defaultCharset(); + } + } + + private static String escape(CharSequence content) { + return content.toString().replace("\\", "\\\\"); + } + + private static Path toFile(URL url) { + try { + return Paths.get(url.toURI()); } catch (URISyntaxException ex) { throw new IllegalArgumentException(ex); } } - private static boolean runsOnWindows() { - String os = System.getProperty("os.name"); - if (!StringUtils.hasText(os)) { - return false; + static final class Classpath { + + private final String argument; + + private final List elements; + + private Classpath(String argument, List elements) { + this.argument = argument; + this.elements = elements; } - return os.toLowerCase(Locale.ROOT).contains("win"); + + /** + * Return the {@code -cp} argument value. + * @return the argument to use + */ + String argument() { + return this.argument; + } + + /** + * Return the classpath elements. + * @return the JAR files to use + */ + Stream elements() { + return this.elements.stream(); + } + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CommandLineBuilder.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CommandLineBuilder.java index 00b023fb25..1e09773bd4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CommandLineBuilder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CommandLineBuilder.java @@ -82,7 +82,7 @@ final class CommandLineBuilder { } if (!this.classpathElements.isEmpty()) { commandLine.add("-cp"); - commandLine.add(ClasspathBuilder.build(this.classpathElements.toArray(URL[]::new))); + commandLine.add(ClasspathBuilder.forURLs(this.classpathElements).build().argument()); } commandLine.add(this.mainClass); if (!this.arguments.isEmpty()) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArgFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArgFileTests.java deleted file mode 100644 index 2c83f86187..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArgFileTests.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2012-2025 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.springframework.boot.maven; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Paths; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ArgFile}. - * - * @author Moritz Halbritter - * @author Dmytro Nosan - */ -class ArgFileTests { - - @Test - void argFileEscapesContent() throws IOException { - ArgFile file = ArgFile.create("some \\ content"); - assertThat(Paths.get(file.toString())).content(StandardCharsets.UTF_8).isEqualTo("\"some \\\\ content\""); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ClasspathBuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ClasspathBuilderTests.java index 8216c21881..18927066a0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ClasspathBuilderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ClasspathBuilderTests.java @@ -17,41 +17,38 @@ package org.springframework.boot.maven; import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; +import org.springframework.boot.maven.ClasspathBuilder.Classpath; + import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link ClasspathBuilder}. * * @author Dmytro Nosan + * @author Stephane Nicoll */ class ClasspathBuilderTests { - @Test - void buildWithEmptyClassPath() { - assertThat(ClasspathBuilder.build()).isEmpty(); - } - - @Test - void buildWithSingleClassPathURL(@TempDir Path tempDir) throws Exception { - Path file = tempDir.resolve("test.jar"); - assertThat(ClasspathBuilder.build(file.toUri().toURL())).isEqualTo(file.toString()); - } - @Test @DisabledOnOs(OS.WINDOWS) void buildWithMultipleClassPathURLs(@TempDir Path tempDir) throws Exception { Path file = tempDir.resolve("test.jar"); Path file1 = tempDir.resolve("test1.jar"); - assertThat(ClasspathBuilder.build(file.toUri().toURL(), file1.toUri().toURL())) + assertThat(ClasspathBuilder.forURLs(file.toUri().toURL(), file1.toUri().toURL()).build().argument()) .isEqualTo(file + File.pathSeparator + file1); } @@ -60,10 +57,101 @@ class ClasspathBuilderTests { void buildWithMultipleClassPathURLsOnWindows(@TempDir Path tempDir) throws Exception { Path file = tempDir.resolve("test.jar"); Path file1 = tempDir.resolve("test1.jar"); - String classpath = ClasspathBuilder.build(file.toUri().toURL(), file1.toUri().toURL()); + String classpath = ClasspathBuilder.forURLs(file.toUri().toURL(), file1.toUri().toURL()).build().argument(); assertThat(classpath).startsWith("@"); assertThat(Paths.get(classpath.substring(1))) .hasContent("\"" + (file + File.pathSeparator + file1).replace("\\", "\\\\") + "\""); } + @Nested + class WindowsTests { + + @Test + void buildWithEmptyClassPath() throws MalformedURLException { + Classpath classpath = classPathBuilder().build(); + assertThat(classpath.argument()).isEmpty(); + assertThat(classpath.elements()).isEmpty(); + } + + @Test + void buildWithSingleClassPathURL(@TempDir Path tempDir) throws Exception { + Path file = tempDir.resolve("test.jar"); + Classpath classpath = classPathBuilder(file).build(); + assertThat(classpath.argument()).isEqualTo(file.toString()); + assertThat(classpath.elements()).singleElement().isEqualTo(file); + } + + @Test + void buildWithMultipleClassPathURLs(@TempDir Path tempDir) throws Exception { + Path file = tempDir.resolve("test.jar"); + Path file2 = tempDir.resolve("test2.jar"); + Classpath classpath = classPathBuilder(file, file2).build(); + assertThat(classpath.argument()).startsWith("@"); + assertThat(Paths.get(classpath.argument().substring(1))) + .hasContent("\"" + (file + File.pathSeparator + file2).replace("\\", "\\\\") + "\""); + } + + private ClasspathBuilder classPathBuilder(Path... files) throws MalformedURLException { + return new TestClasspathBuilder(true, files); + } + + } + + @Nested + class UnixTests { + + @Test + void buildWithEmptyClassPath() throws MalformedURLException { + Classpath classpath = classPathBuilder().build(); + assertThat(classpath.argument()).isEmpty(); + assertThat(classpath.elements()).isEmpty(); + } + + @Test + void buildWithSingleClassPathURL(@TempDir Path tempDir) throws Exception { + Path file = tempDir.resolve("test.jar"); + Classpath classpath = classPathBuilder(file).build(); + assertThat(classpath.argument()).isEqualTo(file.toString()); + assertThat(classpath.elements()).singleElement().isEqualTo(file); + } + + @Test + void buildWithMultipleClassPathURLs(@TempDir Path tempDir) throws Exception { + Path file = tempDir.resolve("test.jar"); + Path file2 = tempDir.resolve("test2.jar"); + Classpath classpath = classPathBuilder(file, file2).build(); + assertThat(classpath.argument()).doesNotStartWith("@") + .isEqualTo((file + File.pathSeparator + file2).replace("\\", "\\\\")); + } + + private ClasspathBuilder classPathBuilder(Path... files) throws MalformedURLException { + return new TestClasspathBuilder(false, files); + } + + } + + private static class TestClasspathBuilder extends ClasspathBuilder { + + private final boolean needsClasspathArgFile; + + protected TestClasspathBuilder(boolean needsClasspathArgFile, Path... files) throws MalformedURLException { + super(toURLs(files)); + this.needsClasspathArgFile = needsClasspathArgFile; + } + + private static List toURLs(Path... files) throws MalformedURLException { + List urls = new ArrayList<>(); + for (Path file : files) { + urls.add(file.toUri().toURL()); + } + return urls; + } + + @Override + protected boolean needsClasspathArgFile() { + return this.needsClasspathArgFile; + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CommandLineBuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CommandLineBuilderTests.java index 4d5e245be4..3c14218937 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CommandLineBuilderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CommandLineBuilderTests.java @@ -119,11 +119,9 @@ class CommandLineBuilderTests { @Test void buildAndRunWithLongClassPath() throws IOException, InterruptedException { - StringBuilder classPath = new StringBuilder(ManagementFactory.getRuntimeMXBean().getClassPath()); - while (classPath.length() < 35000) { - classPath.append(File.pathSeparator).append(classPath); - } - URL[] urls = Arrays.stream(classPath.toString().split(File.pathSeparator)).map(this::toURL).toArray(URL[]::new); + URL[] urls = Arrays.stream(ManagementFactory.getRuntimeMXBean().getClassPath().split(File.pathSeparator)) + .map(this::toURL) + .toArray(URL[]::new); List command = CommandLineBuilder.forMainClass(ClassWithMainMethod.class.getName()) .withClasspath(urls) .build();