From a6b80831f0d39c58a396cad7d0a1453a75d4a524 Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Mon, 17 Feb 2025 13:08:59 +0200 Subject: [PATCH 1/2] Use ArgFile for classpath argument on Windows This commit uses @argfile syntax for classpath argument on Windows OS to avoid creating a command-line that is too long. See gh-44305 Signed-off-by: Dmytro Nosan --- .../boot/maven/AbstractAotMojo.java | 6 +- .../boot/maven/AbstractRunMojo.java | 72 +-------------- .../springframework/boot/maven/ArgFile.java | 81 +++++++++++++++++ .../boot/maven/ClasspathBuilder.java | 89 +++++++++++++++++++ .../boot/maven/CommandLineBuilder.java | 30 +------ ...actRunMojoTests.java => ArgFileTests.java} | 14 +-- .../boot/maven/ClasspathBuilderTests.java | 69 ++++++++++++++ .../boot/maven/CommandLineBuilderTests.java | 70 ++++++++++++++- 8 files changed, 322 insertions(+), 109 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArgFile.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ClasspathBuilder.java rename spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/{AbstractRunMojoTests.java => ArgFileTests.java} (72%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ClasspathBuilderTests.java 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 8c6c9599ae3..1158a2d7486 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -46,8 +46,6 @@ import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter; import org.apache.maven.toolchain.ToolchainManager; -import org.springframework.boot.maven.CommandLineBuilder.ClasspathBuilder; - /** * Abstract base class for AOT processing MOJOs. * @@ -149,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(Arrays.asList(classPath))); + options.add(ClasspathBuilder.build(classPath)); 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 71eb7569f5c..c0bdcf7da1c 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * 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. @@ -20,15 +20,10 @@ import java.io.File; import java.io.IOException; import java.net.MalformedURLException; 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.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -46,7 +41,6 @@ import org.apache.maven.toolchain.ToolchainManager; import org.springframework.boot.loader.tools.FileUtils; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; /** * Base class to run a Spring Boot application. @@ -351,45 +345,18 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { private void addClasspath(List args) throws MojoExecutionException { try { - StringBuilder classpath = new StringBuilder(); - for (URL ele : getClassPathUrls()) { - if (!classpath.isEmpty()) { - classpath.append(File.pathSeparator); - } - classpath.append(new File(ele.toURI())); - } + String classpath = ClasspathBuilder.build(getClassPathUrls()); if (getLog().isDebugEnabled()) { getLog().debug("Classpath for forked process: " + classpath); } args.add("-cp"); - if (needsClasspathArgFile()) { - args.add("@" + ArgFile.create(classpath).path()); - } - else { - args.add(classpath.toString()); - } + args.add(classpath); } catch (Exception ex) { throw new MojoExecutionException("Could not build classpath", ex); } } - private boolean needsClasspathArgFile() { - // Windows limits the maximum command length, so we use an argfile there - return runsOnWindows(); - } - - private boolean runsOnWindows() { - String os = System.getProperty("os.name"); - if (!StringUtils.hasLength(os)) { - if (getLog().isWarnEnabled()) { - getLog().warn("System property os.name is not set"); - } - return false; - } - return os.toLowerCase(Locale.ROOT).contains("win"); - } - protected URL[] getClassPathUrls() throws MojoExecutionException { try { List urls = new ArrayList<>(); @@ -468,37 +435,4 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { } - record ArgFile(Path path) { - - 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("\\", "\\\\"); - } - - 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; - } - - } - } 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 new file mode 100644 index 00000000000..c262fda002b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArgFile.java @@ -0,0 +1,81 @@ +/* + * 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 new file mode 100644 index 00000000000..a4ad501998c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ClasspathBuilder.java @@ -0,0 +1,89 @@ +/* + * 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.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Locale; + +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Helper class to build the -cp (classpath) argument of a java process. + * + * @author Stephane Nicoll + * @author Dmytro Nosan + */ +final class ClasspathBuilder { + + private ClasspathBuilder() { + } + + /** + * 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 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; + } + + private static File toFile(URL url) { + try { + return new File(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; + } + return os.toLowerCase(Locale.ROOT).contains("win"); + } + +} 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 ba5a45ce866..00b023fb252 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * 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. @@ -16,8 +16,6 @@ package org.springframework.boot.maven; -import java.io.File; -import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; @@ -84,7 +82,7 @@ final class CommandLineBuilder { } if (!this.classpathElements.isEmpty()) { commandLine.add("-cp"); - commandLine.add(ClasspathBuilder.build(this.classpathElements)); + commandLine.add(ClasspathBuilder.build(this.classpathElements.toArray(URL[]::new))); } commandLine.add(this.mainClass); if (!this.arguments.isEmpty()) { @@ -93,30 +91,6 @@ final class CommandLineBuilder { return commandLine; } - static class ClasspathBuilder { - - static String build(List classpathElements) { - StringBuilder classpath = new StringBuilder(); - for (URL element : classpathElements) { - if (!classpath.isEmpty()) { - classpath.append(File.pathSeparator); - } - classpath.append(toFile(element)); - } - return classpath.toString(); - } - - private static File toFile(URL element) { - try { - return new File(element.toURI()); - } - catch (URISyntaxException ex) { - throw new IllegalArgumentException(ex); - } - } - - } - /** * Format System properties. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/AbstractRunMojoTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArgFileTests.java similarity index 72% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/AbstractRunMojoTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArgFileTests.java index ba5669ee356..2c83f86187c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/AbstractRunMojoTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArgFileTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * 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. @@ -18,24 +18,24 @@ 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 org.springframework.boot.maven.AbstractRunMojo.ArgFile; - import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link AbstractRunMojo}. + * Tests for {@link ArgFile}. * * @author Moritz Halbritter + * @author Dmytro Nosan */ -class AbstractRunMojoTests { +class ArgFileTests { @Test - void argfileEscapesContent() throws IOException { + void argFileEscapesContent() throws IOException { ArgFile file = ArgFile.create("some \\ content"); - assertThat(file.path()).content(StandardCharsets.UTF_8).isEqualTo("\"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 new file mode 100644 index 00000000000..8216c218811 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ClasspathBuilderTests.java @@ -0,0 +1,69 @@ +/* + * 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.File; +import java.nio.file.Path; +import java.nio.file.Paths; + +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 static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ClasspathBuilder}. + * + * @author Dmytro Nosan + */ +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())) + .isEqualTo(file + File.pathSeparator + file1); + } + + @Test + @EnabledOnOs(OS.WINDOWS) + 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()); + assertThat(classpath).startsWith("@"); + assertThat(Paths.get(classpath.substring(1))) + .hasContent("\"" + (file + File.pathSeparator + file1).replace("\\", "\\\\") + "\""); + } + +} 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 aedd17bd48b..4d5e245be44 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -16,10 +16,25 @@ package org.springframework.boot.maven; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.management.ManagementFactory; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; import java.util.Map; 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.loader.tools.JavaExecutable; import org.springframework.boot.maven.sample.ClassWithMainMethod; import static org.assertj.core.api.Assertions.assertThat; @@ -76,4 +91,57 @@ class CommandLineBuilderTests { .containsExactly(CLASS_NAME, "--test", "--another"); } + @Test + @DisabledOnOs(OS.WINDOWS) + void buildWithClassPath(@TempDir Path tempDir) throws Exception { + Path file = tempDir.resolve("test.jar"); + Path file1 = tempDir.resolve("test1.jar"); + assertThat(CommandLineBuilder.forMainClass(CLASS_NAME) + .withClasspath(file.toUri().toURL(), file1.toUri().toURL()) + .build()).containsExactly("-cp", file + File.pathSeparator + file1, CLASS_NAME); + } + + @Test + @EnabledOnOs(OS.WINDOWS) + void buildWithClassPathOnWindows(@TempDir Path tempDir) throws Exception { + Path file = tempDir.resolve("test.jar"); + Path file1 = tempDir.resolve("test1.jar"); + List args = CommandLineBuilder.forMainClass(CLASS_NAME) + .withClasspath(file.toUri().toURL(), file1.toUri().toURL()) + .build(); + assertThat(args).hasSize(3); + assertThat(args.get(0)).isEqualTo("-cp"); + assertThat(args.get(1)).startsWith("@"); + assertThat(args.get(2)).isEqualTo(CLASS_NAME); + assertThat(Paths.get(args.get(1).substring(1))) + .hasContent("\"" + (file + File.pathSeparator + file1).replace("\\", "\\\\") + "\""); + } + + @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); + List command = CommandLineBuilder.forMainClass(ClassWithMainMethod.class.getName()) + .withClasspath(urls) + .build(); + ProcessBuilder pb = new JavaExecutable().processBuilder(command.toArray(new String[0])); + Process process = pb.start(); + assertThat(process.waitFor()).isEqualTo(0); + try (InputStream inputStream = process.getInputStream()) { + assertThat(inputStream).hasContent("Hello World"); + } + } + + private URL toURL(String path) { + try { + return Paths.get(path).toUri().toURL(); + } + catch (MalformedURLException ex) { + throw new RuntimeException(ex); + } + } + } From cd8c12da0bc63920f59cae07db9412042da223c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 18 Feb 2025 15:12:57 +0100 Subject: [PATCH 2/2] Polish "Use ArgFile for classpath argument on Windows" See gh-44305 --- .../boot/maven/AbstractAotMojo.java | 2 +- .../boot/maven/AbstractRunMojo.java | 8 +- .../springframework/boot/maven/ArgFile.java | 81 --------- .../boot/maven/ClasspathBuilder.java | 154 ++++++++++++++---- .../boot/maven/CommandLineBuilder.java | 2 +- .../boot/maven/ArgFileTests.java | 41 ----- .../boot/maven/ClasspathBuilderTests.java | 114 +++++++++++-- .../boot/maven/CommandLineBuilderTests.java | 8 +- 8 files changed, 232 insertions(+), 178 deletions(-) delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArgFile.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArgFileTests.java 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 1158a2d7486..c89fb6764f4 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 c0bdcf7da1c..e838bced47d 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 c262fda002b..00000000000 --- 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 a4ad501998c..7505a4db6b9 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 00b023fb252..1e09773bd45 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 2c83f86187c..00000000000 --- 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 8216c218811..18927066a02 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 4d5e245be44..3c142189375 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();