Add support for invoking AOT to the Maven Plugin

This commit adds an `aot-generate` goal to the Maven Plugin that
triggers AOT generation on the application. The new goal shares a
number of properties with the existing `run` goal and uses the same
algorithm to detect the main class to use.

Closes gh-30525
This commit is contained in:
Stephane Nicoll 2022-03-30 19:40:01 +02:00 committed by Andy Wilkinson
parent 096420cc4c
commit e81c6337c6
12 changed files with 581 additions and 105 deletions

View File

@ -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")

View File

@ -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 <<run,run goal>> for that reason.
include::goals/aot-generate.adoc[leveloffset=+1]

View File

@ -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]

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<artifactId>aot</artifactId>
<build>
<plugins>
<!-- tag::aot[] -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>aot-generate</id>
<goals>
<goal>aot-generate</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- end::aot[] -->
</plugins>
</build>
</project>

View File

@ -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<String> 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);
}
}
}

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>aot</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>aot-generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>@project.version@</version>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>@jakarta-servlet.version@</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -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);
}
}

View File

@ -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<String, String> environmentVariables)
throws MojoExecutionException, MojoFailureException {
List<String> 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<String> args, Map<String, String> environmentVariables)
throws MojoExecutionException, MojoFailureException;
@Override
protected URL[] getClassPathUrls() throws MojoExecutionException {
try {
List<URL> 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<String> 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<URL> 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);
}
}
}
}

View File

@ -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<String> 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<String> args, Map<String, String> environmentVariables)
protected abstract void run(File workingDirectory, String startClassName, Map<String, String> 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<String> args) {
protected void addArgs(List<String> 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<String> args) {
protected void addJvmArgs(List<String> args) {
RunArguments jvmArguments = resolveJvmArguments();
Collections.addAll(args, jvmArguments.asArray());
logArguments("JVM argument(s): ", jvmArguments.asArray());
}
private void addAgents(List<String> 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<String> args) throws MojoExecutionException {
protected void addClasspath(List<String> 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<URL> 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<URL> urls) throws MalformedURLException {
protected void addUserDefinedDirectories(List<URL> 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<URL> 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<URL> urls) throws MalformedURLException {
protected void addProjectClasses(List<URL> urls) throws MalformedURLException {
urls.add(this.classesDirectory.toURI().toURL());
}
private void addDependencies(List<URL> urls) throws MalformedURLException, MojoExecutionException {
FilterArtifacts filters = (this.useTestClasspath ? getFilters() : getFilters(new TestArtifactFilter()));
protected void addDependencies(List<URL> urls, FilterArtifacts filters)
throws MalformedURLException, MojoExecutionException {
Set<Artifact> 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);

View File

@ -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<String, String> 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<String, String> environmentVariables) throws MojoExecutionException {
List<String> 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<String> args, Map<String, String> 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<URL> 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<Path> 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<String> options = List.of("-cp",
Arrays.stream(classpathUrls).map(URL::toString).collect(Collectors.joining(":")), "-d",
this.classesDirectory.toPath().toAbsolutePath().toString());
Iterable<? extends JavaFileObject> 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<Path> 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<JavaFileObject> {
private final StringBuilder message = new StringBuilder();
@Override
public void report(Diagnostic<? extends JavaFileObject> 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();
}
}
}

View File

@ -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<String> args, Map<String, String> 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);

View File

@ -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";