Skip to content

Commit

Permalink
Add support for invoking AOT in the Maven Plugin
Browse files Browse the repository at this point in the history
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 spring-projectsgh-30525
  • Loading branch information
snicoll authored and wilkinsona committed Apr 6, 2022
1 parent 007dc11 commit c72e2eb
Show file tree
Hide file tree
Showing 12 changed files with 577 additions and 107 deletions.
Expand Up @@ -34,11 +34,10 @@ 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"))

intTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-buildpack-platform"))
intTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-tools"))
intTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
intTestImplementation("org.apache.maven.shared:maven-invoker") {
exclude(group: "javax.inject", module: "javax.inject")
Expand All @@ -53,6 +52,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")
Expand Down
@@ -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]
Expand Up @@ -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]
Expand Down
@@ -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>


@@ -0,0 +1,76 @@
/*
* 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/reflect-config.json");
});
}

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

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

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

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

}

0 comments on commit c72e2eb

Please sign in to comment.