diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractApplicationRunMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractApplicationRunMojo.java deleted file mode 100644 index 578162b4c5..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractApplicationRunMojo.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2012-2022 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.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -import org.apache.maven.model.Resource; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; -import org.apache.maven.plugins.annotations.Parameter; -import org.apache.maven.shared.artifact.filter.collection.FilterArtifacts; - -import org.springframework.boot.loader.tools.FileUtils; - -/** - * Base class to run a spring application. - * - * @author Phillip Webb - * @author Stephane Nicoll - * @author David Liu - * @author Daniel Young - * @author Dmytro Nosan - * @since 3.0.0 - * @see RunMojo - * @see StartMojo - */ -public abstract class AbstractApplicationRunMojo extends AbstractRunMojo { - - /** - * Add maven resources to the classpath directly, this allows live in-place editing of - * resources. Duplicate resources are removed from {@code target/classes} to prevent - * them to appear twice if {@code ClassLoader.getResources()} is called. Please - * consider adding {@code spring-boot-devtools} to your project instead as it provides - * this feature and many more. - * @since 1.0.0 - */ - @Parameter(property = "spring-boot.run.addResources", defaultValue = "false") - private boolean addResources = false; - - /** - * Path to agent jars. NOTE: a forked process is required to use this feature. - * @since 2.2.0 - */ - @Parameter(property = "spring-boot.run.agents") - private File[] agents; - - /** - * Flag to say that the agent requires -noverify. - * @since 1.0.0 - */ - @Parameter(property = "spring-boot.run.noverify") - private boolean noverify = false; - - /** - * Flag to include the test classpath when running. - * @since 1.3.0 - */ - @Parameter(property = "spring-boot.run.useTestClasspath", defaultValue = "false") - private Boolean useTestClasspath; - - @Override - protected void run(File workingDirectory, String startClassName, Map environmentVariables) - throws MojoExecutionException, MojoFailureException { - List args = new ArrayList<>(); - addAgents(args); - addJvmArgs(args); - addClasspath(args); - args.add(startClassName); - addArgs(args); - run(workingDirectory, args, environmentVariables); - } - - /** - * Run with a forked VM, using the specified command line arguments. - * @param workingDirectory the working directory of the forked JVM - * @param args the arguments (JVM arguments and application arguments) - * @param environmentVariables the environment variables - * @throws MojoExecutionException in case of MOJO execution errors - * @throws MojoFailureException in case of MOJO failures - */ - protected abstract void run(File workingDirectory, List args, Map environmentVariables) - throws MojoExecutionException, MojoFailureException; - - @Override - protected URL[] getClassPathUrls() throws MojoExecutionException { - try { - List urls = new ArrayList<>(); - addUserDefinedDirectories(urls); - addResources(urls); - addProjectClasses(urls); - FilterArtifacts filters = (this.useTestClasspath ? getFilters() : getFilters(new TestArtifactFilter())); - addDependencies(urls, filters); - return urls.toArray(new URL[0]); - } - catch (IOException ex) { - throw new MojoExecutionException("Unable to build classpath", ex); - } - } - - private void addAgents(List args) { - if (this.agents != null) { - if (getLog().isInfoEnabled()) { - getLog().info("Attaching agents: " + Arrays.asList(this.agents)); - } - for (File agent : this.agents) { - args.add("-javaagent:" + agent); - } - } - if (this.noverify) { - args.add("-noverify"); - } - } - - private void addResources(List urls) throws IOException { - if (this.addResources) { - for (Resource resource : this.project.getResources()) { - File directory = new File(resource.getDirectory()); - urls.add(directory.toURI().toURL()); - FileUtils.removeDuplicatesFromOutputDirectory(this.classesDirectory, directory); - } - } - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractDependencyFilterMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractDependencyFilterMojo.java index c2074afb6b..cc0929ba64 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractDependencyFilterMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractDependencyFilterMojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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,6 +16,10 @@ package org.springframework.boot.maven; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -25,6 +29,8 @@ import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.apache.maven.shared.artifact.filter.collection.AbstractArtifactFeatureFilter; import org.apache.maven.shared.artifact.filter.collection.ArtifactFilterException; import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter; import org.apache.maven.shared.artifact.filter.collection.FilterArtifacts; @@ -38,6 +44,13 @@ import org.apache.maven.shared.artifact.filter.collection.FilterArtifacts; */ public abstract class AbstractDependencyFilterMojo extends AbstractMojo { + /** + * The Maven project. + * @since 3.0.0 + */ + @Parameter(defaultValue = "${project}", readonly = true, required = true) + protected MavenProject project; + /** * Collection of artifact definitions to include. The {@link Include} element defines * mandatory {@code groupId} and {@code artifactId} properties and an optional @@ -76,6 +89,26 @@ public abstract class AbstractDependencyFilterMojo extends AbstractMojo { this.excludeGroupIds = excludeGroupIds; } + protected List getDependencyURLs(ArtifactsFilter... additionalFilters) throws MojoExecutionException { + Set artifacts = filterDependencies(this.project.getArtifacts(), getFilters(additionalFilters)); + List urls = new ArrayList<>(); + for (Artifact artifact : artifacts) { + if (artifact.getFile() != null) { + urls.add(toURL(artifact.getFile())); + } + } + return urls; + } + + protected URL toURL(File file) { + try { + return file.toURI().toURL(); + } + catch (MalformedURLException ex) { + throw new IllegalStateException("Invalid URL for " + file, ex); + } + } + protected final Set filterDependencies(Set dependencies, FilterArtifacts filters) throws MojoExecutionException { try { @@ -124,4 +157,17 @@ public abstract class AbstractDependencyFilterMojo extends AbstractMojo { return cleaned.toString(); } + static class TestArtifactFilter extends AbstractArtifactFeatureFilter { + + TestArtifactFilter() { + super("", Artifact.SCOPE_TEST); + } + + @Override + protected String getArtifactFeature(Artifact artifact) { + return artifact.getScope(); + } + + } + } 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 20b294ec51..03f1bf75cd 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 @@ -20,6 +20,7 @@ import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -29,21 +30,19 @@ import java.util.stream.Collectors; import org.apache.maven.artifact.Artifact; import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Resource; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; -import org.apache.maven.shared.artifact.filter.collection.AbstractArtifactFeatureFilter; import org.apache.maven.shared.artifact.filter.collection.FilterArtifacts; -import org.apache.maven.toolchain.Toolchain; import org.apache.maven.toolchain.ToolchainManager; -import org.springframework.boot.loader.tools.JavaExecutable; -import org.springframework.boot.loader.tools.MainClassFinder; +import org.springframework.boot.loader.tools.FileUtils; /** - * Base class to support running a process that deals with a Spring application. + * Base class to run a Spring Boot application. * * @author Phillip Webb * @author Stephane Nicoll @@ -51,19 +50,17 @@ import org.springframework.boot.loader.tools.MainClassFinder; * @author Daniel Young * @author Dmytro Nosan * @since 1.3.0 + * @see RunMojo + * @see StartMojo */ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { - private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication"; - - private static final int EXIT_CODE_SIGINT = 130; - /** * The Maven project. * @since 1.0.0 */ @Parameter(defaultValue = "${project}", readonly = true, required = true) - protected MavenProject project; + private MavenProject project; /** * The current Maven session. This is used for toolchain manager API calls. @@ -79,6 +76,31 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { @Component private ToolchainManager toolchainManager; + /** + * Add maven resources to the classpath directly, this allows live in-place editing of + * resources. Duplicate resources are removed from {@code target/classes} to prevent + * them to appear twice if {@code ClassLoader.getResources()} is called. Please + * consider adding {@code spring-boot-devtools} to your project instead as it provides + * this feature and many more. + * @since 1.0.0 + */ + @Parameter(property = "spring-boot.run.addResources", defaultValue = "false") + private boolean addResources = false; + + /** + * Path to agent jars. + * @since 2.2.0 + */ + @Parameter(property = "spring-boot.run.agents") + private File[] agents; + + /** + * Flag to say that the agent requires -noverify. + * @since 1.0.0 + */ + @Parameter(property = "spring-boot.run.noverify") + private boolean noverify = false; + /** * Current working directory to use for the application. If not specified, basedir * will be used. @@ -157,7 +179,14 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { * @since 1.0.0 */ @Parameter(defaultValue = "${project.build.outputDirectory}", required = true) - protected File classesDirectory; + private File classesDirectory; + + /** + * Flag to include the test classpath when running. + * @since 1.3.0 + */ + @Parameter(property = "spring-boot.run.useTestClasspath", defaultValue = "false") + private Boolean useTestClasspath; /** * Skip the execution. @@ -172,29 +201,36 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { getLog().debug("skipping run as per configuration."); return; } - run((this.workingDirectory != null) ? this.workingDirectory : this.project.getBasedir(), getStartClass(), - determineEnvironmentVariables()); + String startClass = (this.mainClass != null) ? this.mainClass + : SpringBootApplicationClassFinder.findSingleClass(this.classesDirectory); + run(startClass); + } + + private void run(String startClassName) throws MojoExecutionException, MojoFailureException { + List args = new ArrayList<>(); + addAgents(args); + addJvmArgs(args); + addClasspath(args); + args.add(startClassName); + addArgs(args); + JavaProcessExecutor processExecutor = new JavaProcessExecutor(this.session, this.toolchainManager); + File workingDirectoryToUse = (this.workingDirectory != null) ? this.workingDirectory + : this.project.getBasedir(); + run(processExecutor, workingDirectoryToUse, args, determineEnvironmentVariables()); } /** - * Run with a forked VM, using the specified class name. + * Run the application. + * @param processExecutor the {@link JavaProcessExecutor} to use * @param workingDirectory the working directory of the forked JVM - * @param startClassName the name of the class to execute + * @param args the arguments (JVM arguments and application arguments) * @param environmentVariables the environment variables * @throws MojoExecutionException in case of MOJO execution errors * @throws MojoFailureException in case of MOJO failures + * @since 3.0.0 */ - protected abstract void run(File workingDirectory, String startClassName, Map environmentVariables) - throws MojoExecutionException, MojoFailureException; - - /** - * Specify if the forked process has terminated successfully, based on its exit code. - * @param exitCode the exit code of the process - * @return {@code true} if the process has terminated successfully - */ - protected boolean hasTerminatedSuccessfully(int exitCode) { - return (exitCode == 0 || exitCode == EXIT_CODE_SIGINT); - } + protected abstract void run(JavaProcessExecutor processExecutor, File workingDirectory, List args, + Map environmentVariables) throws MojoExecutionException, MojoFailureException; /** * Resolve the application arguments to use. @@ -207,16 +243,6 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { return runArguments; } - /** - * Provides access to the java binary executable, regardless of OS. - * @return the java executable - */ - protected String getJavaExecutable() { - Toolchain toolchain = this.toolchainManager.getToolchainFromBuildContext("jdk", this.session); - String javaExecutable = (toolchain != null) ? toolchain.findTool("java") : null; - return (javaExecutable != null) ? javaExecutable : new JavaExecutable().toString(); - } - /** * Resolve the environment variables to use. * @return an {@link EnvVariables} defining the environment variables @@ -225,7 +251,7 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { return new EnvVariables(this.environmentVariables); } - protected void addArgs(List args) { + private void addArgs(List args) { RunArguments applicationArguments = resolveApplicationArguments(); Collections.addAll(args, applicationArguments.asArray()); logArguments("Application argument(s): ", applicationArguments.asArray()); @@ -254,12 +280,26 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { return new RunArguments(stringBuilder.toString()); } - protected void addJvmArgs(List args) { + private void addJvmArgs(List args) { RunArguments jvmArguments = resolveJvmArguments(); Collections.addAll(args, jvmArguments.asArray()); logArguments("JVM argument(s): ", jvmArguments.asArray()); } + private void addAgents(List args) { + if (this.agents != null) { + if (getLog().isInfoEnabled()) { + getLog().info("Attaching agents: " + Arrays.asList(this.agents)); + } + for (File agent : this.agents) { + args.add("-javaagent:" + agent); + } + } + if (this.noverify) { + args.add("-noverify"); + } + } + private void addActiveProfileArgument(RunArguments arguments) { if (this.profiles.length > 0) { StringBuilder arg = new StringBuilder("--spring.profiles.active="); @@ -274,7 +314,7 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { } } - protected void addClasspath(List args) throws MojoExecutionException { + private void addClasspath(List args) throws MojoExecutionException { try { StringBuilder classpath = new StringBuilder(); for (URL ele : getClassPathUrls()) { @@ -294,26 +334,21 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { } } - protected String getStartClass() throws MojoExecutionException { - String mainClass = this.mainClass; - if (mainClass == null) { - try { - mainClass = MainClassFinder.findSingleMainClass(this.classesDirectory, - SPRING_BOOT_APPLICATION_CLASS_NAME); - } - catch (IOException ex) { - throw new MojoExecutionException(ex.getMessage(), ex); - } + protected URL[] getClassPathUrls() throws MojoExecutionException { + try { + List urls = new ArrayList<>(); + addUserDefinedDirectories(urls); + addResources(urls); + addProjectClasses(urls); + addDependencies(urls); + return urls.toArray(new URL[0]); } - if (mainClass == null) { - throw new MojoExecutionException("Unable to find a suitable main class, please add a 'mainClass' property"); + catch (IOException ex) { + throw new MojoExecutionException("Unable to build classpath", ex); } - return mainClass; } - protected abstract URL[] getClassPathUrls() throws MojoExecutionException; - - protected void addUserDefinedDirectories(List urls) throws MalformedURLException { + private void addUserDefinedDirectories(List urls) throws MalformedURLException { if (this.directories != null) { for (String directory : this.directories) { urls.add(new File(directory).toURI().toURL()); @@ -321,12 +356,22 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { } } - protected void addProjectClasses(List urls) throws MalformedURLException { + private void addResources(List urls) throws IOException { + if (this.addResources) { + for (Resource resource : this.project.getResources()) { + File directory = new File(resource.getDirectory()); + urls.add(directory.toURI().toURL()); + FileUtils.removeDuplicatesFromOutputDirectory(this.classesDirectory, directory); + } + } + } + + private void addProjectClasses(List urls) throws MalformedURLException { urls.add(this.classesDirectory.toURI().toURL()); } - protected void addDependencies(List urls, FilterArtifacts filters) - throws MalformedURLException, MojoExecutionException { + private void addDependencies(List urls) throws MalformedURLException, MojoExecutionException { + FilterArtifacts filters = (this.useTestClasspath ? getFilters() : getFilters(new TestArtifactFilter())); Set artifacts = filterDependencies(this.project.getArtifacts(), filters); for (Artifact artifact : artifacts) { if (artifact.getFile() != null) { @@ -341,19 +386,6 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { } } - static class TestArtifactFilter extends AbstractArtifactFeatureFilter { - - TestArtifactFilter() { - super("", Artifact.SCOPE_TEST); - } - - @Override - protected String getArtifactFeature(Artifact artifact) { - return artifact.getScope(); - } - - } - /** * Format System properties. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AotGenerateMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AotGenerateMojo.java index 09bf791279..2c6a66dd8c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AotGenerateMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AotGenerateMojo.java @@ -23,6 +23,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -36,14 +37,18 @@ import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; +import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.toolchain.ToolchainManager; -import org.springframework.boot.loader.tools.RunProcess; +import org.springframework.boot.maven.CommandLineBuilder.ClasspathBuilder; +import org.springframework.util.ObjectUtils; /** * Invoke the AOT engine on the application. @@ -55,10 +60,35 @@ import org.springframework.boot.loader.tools.RunProcess; @Mojo(name = "aot-generate", defaultPhase = LifecyclePhase.PREPARE_PACKAGE, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME) -public class AotGenerateMojo extends AbstractRunMojo { +public class AotGenerateMojo extends AbstractDependencyFilterMojo { private static final String AOT_PROCESSOR_CLASS_NAME = "org.springframework.boot.AotProcessor"; + /** + * The current Maven session. This is used for toolchain manager API calls. + */ + @Parameter(defaultValue = "${session}", readonly = true) + private MavenSession session; + + /** + * The toolchain manager to use to locate a custom JDK. + */ + @Component + private ToolchainManager toolchainManager; + + /** + * Directory containing the classes and resource files that should be packaged into + * the archive. + */ + @Parameter(defaultValue = "${project.build.outputDirectory}", required = true) + private File classesDirectory; + + /** + * Skip the execution. + */ + @Parameter(property = "spring-boot.aot.skip", defaultValue = "false") + private boolean skip; + /** * Directory containing the generated sources. */ @@ -77,11 +107,40 @@ public class AotGenerateMojo extends AbstractRunMojo { @Parameter(defaultValue = "${project.build.directory}/spring-aot/main/classes", required = true) private File generatedClasses; + /** + * List of JVM system properties to pass to the AOT process. + */ + @Parameter + private Map systemPropertyVariables; + + /** + * JVM arguments that should be associated with the AOT process. On command line, make + * sure to wrap multiple values between quotes. + */ + @Parameter(property = "spring-boot.aot.jvmArguments") + private String jvmArguments; + + /** + * Name of the main class to use as the source for the AOT process. If not specified + * the first compiled class found that contains a 'main' method will be used. + */ + @Parameter(property = "spring-boot.aot.main-class") + private String mainClass; + + /** + * Spring profiles to take into account for AOT processing. + */ + @Parameter + private String[] profiles; + @Override - protected void run(File workingDirectory, String startClassName, Map environmentVariables) - throws MojoExecutionException, MojoFailureException { + public void execute() throws MojoExecutionException, MojoFailureException { + if (this.skip) { + getLog().debug("skipping execution as per configuration."); + return; + } try { - generateAotAssets(workingDirectory, startClassName, environmentVariables); + generateAotAssets(); compileSourceFiles(); copyAll(this.generatedResources.toPath().resolve("META-INF/native-image"), this.classesDirectory.toPath().resolve("META-INF/native-image")); @@ -92,52 +151,39 @@ public class AotGenerateMojo extends AbstractRunMojo { } } - private void generateAotAssets(File workingDirectory, String startClassName, - Map environmentVariables) throws MojoExecutionException { - List args = new ArrayList<>(); - addJvmArgs(args); - addClasspath(args); - args.add(AOT_PROCESSOR_CLASS_NAME); - // Adding arguments that are necessary for generation - args.add(startClassName); - args.add(this.generatedSources.toString()); - args.add(this.generatedResources.toString()); - args.add(this.generatedClasses.toString()); - args.add(this.project.getGroupId()); - args.add(this.project.getArtifactId()); - addArgs(args); + private void generateAotAssets() throws MojoExecutionException { + String applicationClass = (this.mainClass != null) ? this.mainClass + : SpringBootApplicationClassFinder.findSingleClass(this.classesDirectory); + List aotArguments = new ArrayList<>(); + aotArguments.add(applicationClass); + aotArguments.add(this.generatedSources.toString()); + aotArguments.add(this.generatedResources.toString()); + aotArguments.add(this.generatedClasses.toString()); + aotArguments.add(this.project.getGroupId()); + aotArguments.add(this.project.getArtifactId()); + if (!ObjectUtils.isEmpty(this.profiles)) { + aotArguments.add("--spring.profiles.active=" + String.join(",", this.profiles)); + } + // @formatter:off + List args = CommandLineBuilder.forMainClass(AOT_PROCESSOR_CLASS_NAME) + .withSystemProperties(this.systemPropertyVariables) + .withJvmArguments(new RunArguments(this.jvmArguments).asArray()) + .withClasspath(getClassPathUrls()) + .withArguments(aotArguments.toArray(String[]::new)) + .build(); + // @formatter:on if (getLog().isDebugEnabled()) { getLog().debug("Generating AOT assets using command: " + args); } - int exitCode = forkJvm(workingDirectory, args, environmentVariables); - if (!hasTerminatedSuccessfully(exitCode)) { - throw new MojoExecutionException("AOT generation process finished with exit code: " + exitCode); - } + JavaProcessExecutor processExecutor = new JavaProcessExecutor(this.session, this.toolchainManager); + processExecutor.run(this.project.getBasedir(), args, Collections.emptyMap()); } - private int forkJvm(File workingDirectory, List args, Map environmentVariables) - throws MojoExecutionException { - try { - RunProcess runProcess = new RunProcess(workingDirectory, getJavaExecutable()); - return runProcess.run(true, args, environmentVariables); - } - catch (Exception ex) { - throw new MojoExecutionException("Could not exec java", ex); - } - } - - @Override - protected URL[] getClassPathUrls() throws MojoExecutionException { - try { - List urls = new ArrayList<>(); - addUserDefinedDirectories(urls); - addProjectClasses(urls); - addDependencies(urls, getFilters(new TestArtifactFilter())); - return urls.toArray(new URL[0]); - } - catch (IOException ex) { - throw new MojoExecutionException("Unable to build classpath", ex); - } + private URL[] getClassPathUrls() throws MojoExecutionException { + List urls = new ArrayList<>(); + urls.add(toURL(this.classesDirectory)); + urls.addAll(getDependencyURLs(new TestArtifactFilter())); + return urls.toArray(URL[]::new); } private void compileSourceFiles() throws IOException, MojoExecutionException { @@ -148,7 +194,8 @@ public class AotGenerateMojo extends AbstractRunMojo { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); try (StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null)) { List options = new ArrayList<>(); - addClasspath(options); + options.add("-cp"); + options.add(ClasspathBuilder.build(Arrays.asList(getClassPathUrls()))); options.add("-d"); options.add(this.classesDirectory.toPath().toAbsolutePath().toString()); Iterable compilationUnits = fm.getJavaFileObjectsFromPaths(sourceFiles); 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 new file mode 100644 index 0000000000..0b98e90806 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CommandLineBuilder.java @@ -0,0 +1,135 @@ +/* + * Copyright 2012-2022 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.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Helper class to build the command-line arguments of a java process. + * + * @author Stephane Nicoll + */ +final class CommandLineBuilder { + + private final List options = new ArrayList<>(); + + private final List classpathElements = new ArrayList<>(); + + private final String mainClass; + + private final List arguments = new ArrayList<>(); + + private CommandLineBuilder(String mainClass) { + this.mainClass = mainClass; + } + + static CommandLineBuilder forMainClass(String mainClass) { + return new CommandLineBuilder(mainClass); + } + + CommandLineBuilder withJvmArguments(String... jvmArguments) { + if (jvmArguments != null) { + this.options.addAll(Arrays.stream(jvmArguments).filter(Objects::nonNull).toList()); + } + return this; + } + + CommandLineBuilder withSystemProperties(Map systemProperties) { + if (systemProperties != null) { + systemProperties.entrySet().stream().map((e) -> SystemPropertyFormatter.format(e.getKey(), e.getValue())) + .forEach(this.options::add); + } + return this; + } + + CommandLineBuilder withClasspath(URL... elements) { + this.classpathElements.addAll(Arrays.asList(elements)); + return this; + } + + CommandLineBuilder withArguments(String... arguments) { + if (arguments != null) { + this.arguments.addAll(Arrays.stream(arguments).filter(Objects::nonNull).toList()); + } + return this; + } + + List build() { + List commandLine = new ArrayList<>(); + if (!this.options.isEmpty()) { + commandLine.addAll(this.options); + } + if (!this.classpathElements.isEmpty()) { + commandLine.add("-cp"); + commandLine.add(ClasspathBuilder.build(this.classpathElements)); + } + commandLine.add(this.mainClass); + if (!this.arguments.isEmpty()) { + commandLine.addAll(this.arguments); + } + return commandLine; + } + + static class ClasspathBuilder { + + static String build(List classpathElements) { + StringBuilder classpath = new StringBuilder(); + for (URL element : classpathElements) { + if (classpath.length() > 0) { + 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. + */ + private static class SystemPropertyFormatter { + + static String format(String key, String value) { + if (key == null) { + return ""; + } + if (value == null || value.isEmpty()) { + return String.format("-D%s", key); + } + return String.format("-D%s=\"%s\"", key, value); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/JavaProcessExecutor.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/JavaProcessExecutor.java new file mode 100644 index 0000000000..e1cc925259 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/JavaProcessExecutor.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2022 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.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.toolchain.Toolchain; +import org.apache.maven.toolchain.ToolchainManager; + +import org.springframework.boot.loader.tools.JavaExecutable; +import org.springframework.boot.loader.tools.RunProcess; + +/** + * Ease the execution of a Java process using Maven's toolchain support. + * + * @author Stephane Nicoll + */ +class JavaProcessExecutor { + + private static final int EXIT_CODE_SIGINT = 130; + + private final MavenSession mavenSession; + + private final ToolchainManager toolchainManager; + + private final Consumer runProcessCustomizer; + + JavaProcessExecutor(MavenSession mavenSession, ToolchainManager toolchainManager) { + this(mavenSession, toolchainManager, null); + } + + private JavaProcessExecutor(MavenSession mavenSession, ToolchainManager toolchainManager, + Consumer runProcessCustomizer) { + this.mavenSession = mavenSession; + this.toolchainManager = toolchainManager; + this.runProcessCustomizer = runProcessCustomizer; + } + + JavaProcessExecutor withRunProcessCustomizer(Consumer customizer) { + Consumer combinedCustomizer = (this.runProcessCustomizer != null) + ? this.runProcessCustomizer.andThen(customizer) : customizer; + return new JavaProcessExecutor(this.mavenSession, this.toolchainManager, combinedCustomizer); + } + + int run(File workingDirectory, List args, Map environmentVariables) + throws MojoExecutionException { + RunProcess runProcess = new RunProcess(workingDirectory, getJavaExecutable()); + try { + int exitCode = runProcess.run(true, args, environmentVariables); + if (!hasTerminatedSuccessfully(exitCode)) { + throw new MojoExecutionException("Process terminated with exit code: " + exitCode); + } + return exitCode; + } + catch (IOException ex) { + throw new MojoExecutionException("Process execution failed", ex); + } + } + + RunProcess runAsync(File workingDirectory, List args, Map environmentVariables) + throws MojoExecutionException { + try { + RunProcess runProcess = new RunProcess(workingDirectory, getJavaExecutable()); + runProcess.run(false, args, environmentVariables); + return runProcess; + } + catch (IOException ex) { + throw new MojoExecutionException("Process execution failed", ex); + } + } + + private boolean hasTerminatedSuccessfully(int exitCode) { + return (exitCode == 0 || exitCode == EXIT_CODE_SIGINT); + } + + private String getJavaExecutable() { + Toolchain toolchain = this.toolchainManager.getToolchainFromBuildContext("jdk", this.mavenSession); + String javaExecutable = (toolchain != null) ? toolchain.findTool("java") : null; + return (javaExecutable != null) ? javaExecutable : new JavaExecutable().toString(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java index eb2ff574d2..7645fa338c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Execute; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; @@ -41,7 +42,7 @@ import org.springframework.boot.loader.tools.RunProcess; @Mojo(name = "run", requiresProject = true, defaultPhase = LifecyclePhase.VALIDATE, requiresDependencyResolution = ResolutionScope.TEST) @Execute(phase = LifecyclePhase.TEST_COMPILE) -public class RunMojo extends AbstractApplicationRunMojo { +public class RunMojo extends AbstractRunMojo { /** * Whether the JVM's launch should be optimized. @@ -60,25 +61,11 @@ public class RunMojo extends AbstractApplicationRunMojo { } @Override - protected void run(File workingDirectory, List args, Map environmentVariables) - throws MojoExecutionException { - int exitCode = forkJvm(workingDirectory, args, environmentVariables); - if (hasTerminatedSuccessfully(exitCode)) { - return; - } - throw new MojoExecutionException("Application finished with exit code: " + exitCode); - } - - private int forkJvm(File workingDirectory, List args, Map environmentVariables) - throws MojoExecutionException { - try { - RunProcess runProcess = new RunProcess(workingDirectory, getJavaExecutable()); - Runtime.getRuntime().addShutdownHook(new Thread(new RunProcessKiller(runProcess))); - return runProcess.run(true, args, environmentVariables); - } - catch (Exception ex) { - throw new MojoExecutionException("Could not exec java", ex); - } + protected void run(JavaProcessExecutor processExecutor, File workingDirectory, List args, + Map environmentVariables) throws MojoExecutionException, MojoFailureException { + processExecutor.withRunProcessCustomizer( + (runProcess) -> Runtime.getRuntime().addShutdownHook(new Thread(new RunProcessKiller(runProcess)))) + .run(workingDirectory, args, environmentVariables); } private static final class RunProcessKiller implements Runnable { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/SpringBootApplicationClassFinder.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/SpringBootApplicationClassFinder.java new file mode 100644 index 0000000000..efa4757c28 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/SpringBootApplicationClassFinder.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2022 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 org.apache.maven.plugin.MojoExecutionException; + +import org.springframework.boot.loader.tools.MainClassFinder; + +/** + * Find a single Spring Boot Application class match based on directory. + * + * @author Stephane Nicoll + * @see MainClassFinder + */ +abstract class SpringBootApplicationClassFinder { + + private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication"; + + static String findSingleClass(File classesDirectory) throws MojoExecutionException { + try { + String mainClass = MainClassFinder.findSingleMainClass(classesDirectory, + SPRING_BOOT_APPLICATION_CLASS_NAME); + if (mainClass != null) { + return mainClass; + } + throw new MojoExecutionException("Unable to find a suitable main class, please add a 'mainClass' property"); + } + catch (IOException ex) { + throw new MojoExecutionException(ex.getMessage(), ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java index 4be3b565ef..a28de2d032 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java @@ -49,7 +49,7 @@ import org.springframework.boot.loader.tools.RunProcess; */ @Mojo(name = "start", requiresProject = true, defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST, requiresDependencyResolution = ResolutionScope.TEST) -public class StartMojo extends AbstractApplicationRunMojo { +public class StartMojo extends AbstractRunMojo { private static final String ENABLE_MBEAN_PROPERTY = "--spring.application.admin.enabled=true"; @@ -86,9 +86,9 @@ public class StartMojo extends AbstractApplicationRunMojo { private final Object lock = new Object(); @Override - protected void run(File workingDirectory, List args, Map environmentVariables) - throws MojoExecutionException, MojoFailureException { - RunProcess runProcess = runProcess(workingDirectory, args, environmentVariables); + protected void run(JavaProcessExecutor processExecutor, File workingDirectory, List args, + Map environmentVariables) throws MojoExecutionException, MojoFailureException { + RunProcess runProcess = processExecutor.runAsync(workingDirectory, args, environmentVariables); try { waitForSpringApplication(); } @@ -98,18 +98,6 @@ public class StartMojo extends AbstractApplicationRunMojo { } } - private RunProcess runProcess(File workingDirectory, List args, Map environmentVariables) - throws MojoExecutionException { - try { - RunProcess runProcess = new RunProcess(workingDirectory, getJavaExecutable()); - runProcess.run(false, args, environmentVariables); - return runProcess; - } - catch (Exception ex) { - throw new MojoExecutionException("Could not exec java", ex); - } - } - @Override protected RunArguments resolveApplicationArguments() { RunArguments applicationArguments = super.resolveApplicationArguments(); 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 new file mode 100644 index 0000000000..aa2430f47a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CommandLineBuilderTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2022 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.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.maven.sample.ClassWithMainMethod; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CommandLineBuilder}. + * + * @author Stephane Nicoll + */ +class CommandLineBuilderTests { + + public static final String CLASS_NAME = ClassWithMainMethod.class.getName(); + + @Test + void buildWithNullJvmArgumentsIsIgnored() { + assertThat(CommandLineBuilder.forMainClass(CLASS_NAME).withJvmArguments((String[]) null).build()) + .containsExactly(CLASS_NAME); + } + + @Test + void buildWithNullIntermediateJvmArgumentIsIgnored() { + assertThat(CommandLineBuilder.forMainClass(CLASS_NAME).withJvmArguments("-verbose:class", null, "-verbose:gc") + .build()).containsExactly("-verbose:class", "-verbose:gc", CLASS_NAME); + } + + @Test + void buildWithJvmArgument() { + assertThat(CommandLineBuilder.forMainClass(CLASS_NAME).withJvmArguments("-verbose:class").build()) + .containsExactly("-verbose:class", CLASS_NAME); + } + + @Test + void buildWithNullSystemPropertyIsIgnored() { + assertThat(CommandLineBuilder.forMainClass(CLASS_NAME).withSystemProperties(null).build()) + .containsExactly(CLASS_NAME); + } + + @Test + void buildWithSystemProperty() { + assertThat(CommandLineBuilder.forMainClass(CLASS_NAME).withSystemProperties(Map.of("flag", "enabled")).build()) + .containsExactly("-Dflag=\"enabled\"", CLASS_NAME); + } + + @Test + void buildWithNullArgumentsIsIgnored() { + assertThat(CommandLineBuilder.forMainClass(CLASS_NAME).withArguments((String[]) null).build()) + .containsExactly(CLASS_NAME); + } + + @Test + void buildWithNullIntermediateArgumentIsIgnored() { + assertThat(CommandLineBuilder.forMainClass(CLASS_NAME).withArguments("--test", null, "--another").build()) + .containsExactly(CLASS_NAME, "--test", "--another"); + } + +}