diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle index b9c3536d4d..c1395c2fd1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle @@ -34,6 +34,7 @@ dependencies { exclude(group: "javax.inject", module: "javax.inject") } + implementation("org.springframework:spring-context") implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-buildpack-platform")) implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-tools")) @@ -53,6 +54,8 @@ dependencies { exclude(group: "javax.inject", module: "javax.inject") } + mavenRepository(project(path: ":spring-boot-project:spring-boot", configuration: "mavenRepository")) + runtimeOnly("org.sonatype.plexus:plexus-build-api") testImplementation("org.assertj:assertj-core") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/aot.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/aot.adoc new file mode 100644 index 0000000000..0e42694479 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/aot.adoc @@ -0,0 +1,22 @@ +[[aot]] += Optimizing Your Application at Build-Time + +Spring AOT inspects an application at build-time and generates an optimized version of it. +Based on your `@SpringBootApplication`-annotated main class, the AOT engine generates a persistent view of the beans that are going to be contributed at runtime in a way that bean instantiation is as straightforward as possible. +Additional post-processing of the factory is possible using callbacks. +For instance, these are used to generate the necessary reflection configuration that GraalVM needs to initialize the context in a native image. + +To configure your application to use this feature, add an execution for the `aot-generate` goal, as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/aot/pom.xml[tags=aot] +---- + +As the `BeanFactory` is fully prepared at build-time, conditions are also evaluated. +This has an important difference compared to what a regular Spring Boot application does at runtime. +For instance, if you want to opt-in or opt-out for certain features, you need to configure the environment used at build time to do so. +The `aot-generate` goal shares a number of properties with the <> for that reason. + + +include::goals/aot-generate.adoc[leveloffset=+1] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/index.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/index.adoc index 3e291430a2..3cff6030a6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/index.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/index.adoc @@ -36,6 +36,8 @@ include::packaging-oci-image.adoc[leveloffset=+1] include::running.adoc[leveloffset=+1] +include::aot.adoc[leveloffset=+1] + include::integration-tests.adoc[leveloffset=+1] include::build-info.adoc[leveloffset=+1] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/aot/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/aot/pom.xml new file mode 100644 index 0000000000..5799945c6e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/aot/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + aot + + + + + org.springframework.boot + spring-boot-maven-plugin + + + aot-generate + + aot-generate + + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AotGenerateTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AotGenerateTests.java new file mode 100644 index 0000000000..9be3b2a20a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AotGenerateTests.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.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for the Maven plugin's AOT support. + * + * @author Stephane Nicoll + */ +@ExtendWith(MavenBuildExtension.class) +public class AotGenerateTests { + + @TestTemplate + void whenAotRunsSourcesAreGenerated(MavenBuild mavenBuild) { + mavenBuild.project("aot").goals("package").execute((project) -> { + Path aotDirectory = project.toPath().resolve("target/spring-aot/main"); + assertThat(collectRelativeFileNames(aotDirectory.resolve("sources"))) + .containsOnly("org/test/SampleApplication__ApplicationContextInitializer.java"); + assertThat(collectRelativeFileNames(aotDirectory.resolve("resources"))).containsOnly( + "META-INF/native-image/org.springframework.boot.maven.it/aot/reflect-config.json", + "META-INF/native-image/org.springframework.boot.maven.it/aot/native-image.properties"); + }); + } + + @TestTemplate + void whenAotRunsSourcesAreCompiled(MavenBuild mavenBuild) { + mavenBuild.project("aot").goals("package").execute((project) -> { + Path classesDirectory = project.toPath().resolve("target/classes"); + assertThat(collectRelativeFileNames(classesDirectory)) + .contains("org/test/SampleApplication__ApplicationContextInitializer.class"); + }); + } + + @TestTemplate + void whenAotRunsResourcesAreCopiedToTargetClasses(MavenBuild mavenBuild) { + mavenBuild.project("aot").goals("package").execute((project) -> { + Path classesDirectory = project.toPath().resolve("target/classes/META-INF/native-image"); + assertThat(collectRelativeFileNames(classesDirectory)).contains( + "org.springframework.boot.maven.it/aot/reflect-config.json", + "org.springframework.boot.maven.it/aot/native-image.properties"); + }); + } + + Stream collectRelativeFileNames(Path sourceDirectory) { + try { + return Files.walk(sourceDirectory).filter(Files::isRegularFile) + .map((path) -> path.subpath(sourceDirectory.getNameCount(), path.getNameCount()).toString()); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/aot/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/aot/pom.xml new file mode 100644 index 0000000000..f02fad2280 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/aot/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aot + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + aot-generate + + + + + + + + + org.springframework.boot + spring-boot + @project.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/aot/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/aot/src/main/java/org/test/SampleApplication.java new file mode 100644 index 0000000000..cbf535181a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/aot/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +import org.springframework.boot.SpringApplication; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class SampleApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleApplication.class, args); + } + +} 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 new file mode 100644 index 0000000000..578162b4c5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractApplicationRunMojo.java @@ -0,0 +1,144 @@ +/* + * 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/AbstractRunMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java index 440285282a..20b294ec51 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,7 +20,6 @@ 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; @@ -30,7 +29,6 @@ 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; @@ -41,12 +39,11 @@ 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.FileUtils; import org.springframework.boot.loader.tools.JavaExecutable; import org.springframework.boot.loader.tools.MainClassFinder; /** - * Base class to run a spring application. + * Base class to support running a process that deals with a Spring application. * * @author Phillip Webb * @author Stephane Nicoll @@ -54,19 +51,19 @@ 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) - private MavenProject project; + protected MavenProject project; /** * The current Maven session. This is used for toolchain manager API calls. @@ -82,31 +79,6 @@ 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. @@ -185,14 +157,7 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { * @since 1.0.0 */ @Parameter(defaultValue = "${project.build.outputDirectory}", required = true) - 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; + protected File classesDirectory; /** * Skip the execution. @@ -207,31 +172,30 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { getLog().debug("skipping run as per configuration."); return; } - run(getStartClass()); - } - - private void run(String startClassName) throws MojoExecutionException, MojoFailureException { - List args = new ArrayList<>(); - addAgents(args); - addJvmArgs(args); - addClasspath(args); - args.add(startClassName); - addArgs(args); - run((this.workingDirectory != null) ? this.workingDirectory : this.project.getBasedir(), args, + run((this.workingDirectory != null) ? this.workingDirectory : this.project.getBasedir(), getStartClass(), determineEnvironmentVariables()); } /** - * Run with a forked VM, using the specified command line arguments. + * Run with a forked VM, using the specified class name. * @param workingDirectory the working directory of the forked JVM - * @param args the arguments (JVM arguments and application arguments) + * @param startClassName the name of the class to execute * @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) + 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); + } + /** * Resolve the application arguments to use. * @return a {@link RunArguments} defining the application arguments @@ -261,7 +225,7 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { return new EnvVariables(this.environmentVariables); } - private void addArgs(List args) { + protected void addArgs(List args) { RunArguments applicationArguments = resolveApplicationArguments(); Collections.addAll(args, applicationArguments.asArray()); logArguments("Application argument(s): ", applicationArguments.asArray()); @@ -290,26 +254,12 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { return new RunArguments(stringBuilder.toString()); } - private void addJvmArgs(List args) { + protected 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="); @@ -324,7 +274,7 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { } } - private void addClasspath(List args) throws MojoExecutionException { + protected void addClasspath(List args) throws MojoExecutionException { try { StringBuilder classpath = new StringBuilder(); for (URL ele : getClassPathUrls()) { @@ -344,7 +294,7 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { } } - private String getStartClass() throws MojoExecutionException { + protected String getStartClass() throws MojoExecutionException { String mainClass = this.mainClass; if (mainClass == null) { try { @@ -361,21 +311,9 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { return mainClass; } - protected URL[] getClassPathUrls() throws MojoExecutionException { - try { - List urls = new ArrayList<>(); - addUserDefinedDirectories(urls); - addResources(urls); - addProjectClasses(urls); - addDependencies(urls); - return urls.toArray(new URL[0]); - } - catch (IOException ex) { - throw new MojoExecutionException("Unable to build classpath", ex); - } - } + protected abstract URL[] getClassPathUrls() throws MojoExecutionException; - private void addUserDefinedDirectories(List urls) throws MalformedURLException { + protected void addUserDefinedDirectories(List urls) throws MalformedURLException { if (this.directories != null) { for (String directory : this.directories) { urls.add(new File(directory).toURI().toURL()); @@ -383,22 +321,12 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { } } - 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 { + protected void addProjectClasses(List urls) throws MalformedURLException { urls.add(this.classesDirectory.toURI().toURL()); } - private void addDependencies(List urls) throws MalformedURLException, MojoExecutionException { - FilterArtifacts filters = (this.useTestClasspath ? getFilters() : getFilters(new TestArtifactFilter())); + protected void addDependencies(List urls, FilterArtifacts filters) + throws MalformedURLException, MojoExecutionException { Set artifacts = filterDependencies(this.project.getArtifacts(), filters); for (Artifact artifact : artifacts) { if (artifact.getFile() != null) { @@ -413,7 +341,7 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { } } - private static class TestArtifactFilter extends AbstractArtifactFeatureFilter { + static class TestArtifactFilter extends AbstractArtifactFeatureFilter { TestArtifactFilter() { super("", Artifact.SCOPE_TEST); 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 new file mode 100644 index 0000000000..b33c73601f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AotGenerateMojo.java @@ -0,0 +1,205 @@ +/* + * 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.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.tools.Diagnostic; +import javax.tools.DiagnosticListener; +import javax.tools.JavaCompiler; +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +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.springframework.boot.loader.tools.RunProcess; +import org.springframework.util.FileSystemUtils; + +/** + * Invoke the AOT engine on the application. + * + * @author Stephane Nicoll + * @author Andy Wilkinson + * @since 3.0.0 + */ +@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 { + + private static final String AOT_PROCESSOR_CLASS_NAME = "org.springframework.boot.AotProcessor"; + + /** + * Directory containing the generated sources. + */ + @Parameter(defaultValue = "${project.build.directory}/spring-aot/main/sources", required = true) + private File generatedSources; + + /** + * Directory containing the generated resources. + */ + @Parameter(defaultValue = "${project.build.directory}/spring-aot/main/resources", required = true) + private File generatedResources; + + @Override + protected void run(File workingDirectory, String startClassName, Map environmentVariables) + throws MojoExecutionException, MojoFailureException { + try { + deletePreviousAotAssets(); + generateAotAssets(workingDirectory, startClassName, environmentVariables); + compileSourceFiles(getClassPathUrls()); + copyNativeConfiguration(this.generatedResources.toPath()); + } + catch (Exception ex) { + throw new MojoExecutionException(ex.getMessage(), ex); + } + } + + private void deletePreviousAotAssets() { + FileSystemUtils.deleteRecursively(this.generatedSources); + FileSystemUtils.deleteRecursively(this.generatedResources); + } + + 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.project.getGroupId()); + args.add(this.project.getArtifactId()); + addArgs(args); + 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); + } + } + + 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 void compileSourceFiles(URL[] classpathUrls) throws IOException { + List sourceFiles = Files.walk(this.generatedSources.toPath()).filter(Files::isRegularFile).toList(); + if (sourceFiles.isEmpty()) { + return; + } + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + try (StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null)) { + List options = List.of("-cp", + Arrays.stream(classpathUrls).map(URL::toString).collect(Collectors.joining(":")), "-d", + this.classesDirectory.toPath().toAbsolutePath().toString()); + Iterable compilationUnits = fm.getJavaFileObjectsFromPaths(sourceFiles); + Errors errors = new Errors(); + CompilationTask task = compiler.getTask(null, fm, errors, options, null, compilationUnits); + boolean result = task.call(); + if (!result || errors.hasReportedErrors()) { + throw new IllegalStateException("Unable to compile generated source" + errors); + } + } + } + + private void copyNativeConfiguration(Path generatedResources) throws IOException { + Path targetDirectory = this.classesDirectory.toPath().resolve("META-INF/native-image"); + Path sourceDirectory = generatedResources.resolve("META-INF/native-image"); + List files = Files.walk(sourceDirectory).filter(Files::isRegularFile).toList(); + for (Path file : files) { + String relativeFileName = file.subpath(sourceDirectory.getNameCount(), file.getNameCount()).toString(); + getLog().debug("Copying '" + relativeFileName + "' to " + targetDirectory); + Path target = targetDirectory.resolve(relativeFileName); + Files.createDirectories(target.getParent()); + Files.copy(file, target, StandardCopyOption.REPLACE_EXISTING); + } + } + + /** + * {@link DiagnosticListener} used to collect errors. + */ + static class Errors implements DiagnosticListener { + + private final StringBuilder message = new StringBuilder(); + + @Override + public void report(Diagnostic diagnostic) { + if (diagnostic.getKind() == Diagnostic.Kind.ERROR) { + this.message.append("\n"); + this.message.append(diagnostic.getMessage(Locale.getDefault())); + this.message.append(" "); + this.message.append(diagnostic.getSource().getName()); + this.message.append(" "); + this.message.append(diagnostic.getLineNumber()).append(":").append(diagnostic.getColumnNumber()); + } + } + + boolean hasReportedErrors() { + return this.message.length() > 0; + } + + @Override + public String toString() { + return this.message.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 6b911fcc40..47844341ce 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 @@ -42,9 +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 AbstractRunMojo { - - private static final int EXIT_CODE_SIGINT = 130; +public class RunMojo extends AbstractApplicationRunMojo { /** * Whether the JVM's launch should be optimized. @@ -78,7 +76,7 @@ public class RunMojo extends AbstractRunMojo { protected void run(File workingDirectory, List args, Map environmentVariables) throws MojoExecutionException { int exitCode = forkJvm(workingDirectory, args, environmentVariables); - if (exitCode == 0 || exitCode == EXIT_CODE_SIGINT) { + if (hasTerminatedSuccessfully(exitCode)) { return; } throw new MojoExecutionException("Application finished with exit code: " + exitCode); 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 66edfd0cd2..275f4864f8 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 AbstractRunMojo { +public class StartMojo extends AbstractApplicationRunMojo { private static final String ENABLE_MBEAN_PROPERTY = "--spring.application.admin.enabled=true";