Skip to content

Commit d11b344

Browse files
snicollwilkinsona
authored andcommitted
Add support for invoking AOT in 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
1 parent 1870d22 commit d11b344

File tree

12 files changed

+579
-105
lines changed

12 files changed

+579
-105
lines changed

spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dependencies {
3434
exclude(group: "javax.inject", module: "javax.inject")
3535
}
3636

37+
implementation("org.springframework:spring-context")
3738
implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-buildpack-platform"))
3839
implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-tools"))
3940

@@ -53,6 +54,8 @@ dependencies {
5354
exclude(group: "javax.inject", module: "javax.inject")
5455
}
5556

57+
mavenRepository(project(path: ":spring-boot-project:spring-boot", configuration: "mavenRepository"))
58+
5659
runtimeOnly("org.sonatype.plexus:plexus-build-api")
5760

5861
testImplementation("org.assertj:assertj-core")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[[aot]]
2+
= Optimizing Your Application at Build-Time
3+
4+
Spring AOT inspects an application at build-time and generates an optimized version of it.
5+
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.
6+
Additional post-processing of the factory is possible using callbacks.
7+
For instance, these are used to generate the necessary reflection configuration that GraalVM needs to initialize the context in a native image.
8+
9+
To configure your application to use this feature, add an execution for the `aot-generate` goal, as shown in the following example:
10+
11+
[source,xml,indent=0,subs="verbatim,attributes",tabsize=4]
12+
----
13+
include::../maven/aot/pom.xml[tags=aot]
14+
----
15+
16+
As the `BeanFactory` is fully prepared at build-time, conditions are also evaluated.
17+
This has an important difference compared to what a regular Spring Boot application does at runtime.
18+
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.
19+
The `aot-generate` goal shares a number of properties with the <<run,run goal>> for that reason.
20+
21+
22+
include::goals/aot-generate.adoc[leveloffset=+1]

spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/index.adoc

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ include::packaging-oci-image.adoc[leveloffset=+1]
3636

3737
include::running.adoc[leveloffset=+1]
3838

39+
include::aot.adoc[leveloffset=+1]
40+
3941
include::integration-tests.adoc[leveloffset=+1]
4042

4143
include::build-info.adoc[leveloffset=+1]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project>
3+
<modelVersion>4.0.0</modelVersion>
4+
<artifactId>aot</artifactId>
5+
<build>
6+
<plugins>
7+
<!-- tag::aot[] -->
8+
<plugin>
9+
<groupId>org.springframework.boot</groupId>
10+
<artifactId>spring-boot-maven-plugin</artifactId>
11+
<executions>
12+
<execution>
13+
<id>aot-generate</id>
14+
<goals>
15+
<goal>aot-generate</goal>
16+
</goals>
17+
</execution>
18+
</executions>
19+
</plugin>
20+
<!-- end::aot[] -->
21+
</plugins>
22+
</build>
23+
</project>
24+
25+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.maven;
18+
19+
import java.io.IOException;
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
22+
import java.util.stream.Stream;
23+
24+
import org.junit.jupiter.api.TestTemplate;
25+
import org.junit.jupiter.api.extension.ExtendWith;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
/**
30+
* Integration tests for the Maven plugin's AOT support.
31+
*
32+
* @author Stephane Nicoll
33+
*/
34+
@ExtendWith(MavenBuildExtension.class)
35+
public class AotGenerateTests {
36+
37+
@TestTemplate
38+
void whenAotRunsSourcesAreGenerated(MavenBuild mavenBuild) {
39+
mavenBuild.project("aot").goals("package").execute((project) -> {
40+
Path aotDirectory = project.toPath().resolve("target/spring-aot/main");
41+
assertThat(collectRelativeFileNames(aotDirectory.resolve("sources")))
42+
.containsOnly("org/test/SampleApplication__ApplicationContextInitializer.java");
43+
assertThat(collectRelativeFileNames(aotDirectory.resolve("resources"))).containsOnly(
44+
"META-INF/native-image/reflect-config.json", "META-INF/native-image/native-image.properties");
45+
});
46+
}
47+
48+
@TestTemplate
49+
void whenAotRunsSourcesAreCompiled(MavenBuild mavenBuild) {
50+
mavenBuild.project("aot").goals("package").execute((project) -> {
51+
Path classesDirectory = project.toPath().resolve("target/classes");
52+
assertThat(collectRelativeFileNames(classesDirectory))
53+
.contains("org/test/SampleApplication__ApplicationContextInitializer.class");
54+
});
55+
}
56+
57+
@TestTemplate
58+
void whenAotRunsResourcesAreCopiedUsingProjectCoordinates(MavenBuild mavenBuild) {
59+
mavenBuild.project("aot").goals("package").execute((project) -> {
60+
Path classesDirectory = project.toPath().resolve("target/classes/META-INF/native-image");
61+
assertThat(collectRelativeFileNames(classesDirectory)).contains(
62+
"org.springframework.boot.maven.it/aot/reflect-config.json",
63+
"org.springframework.boot.maven.it/aot/native-image.properties");
64+
});
65+
}
66+
67+
Stream<String> collectRelativeFileNames(Path sourceDirectory) {
68+
try {
69+
return Files.walk(sourceDirectory).filter(Files::isRegularFile)
70+
.map((path) -> path.subpath(sourceDirectory.getNameCount(), path.getNameCount()).toString());
71+
}
72+
catch (IOException ex) {
73+
throw new IllegalStateException(ex);
74+
}
75+
}
76+
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
<groupId>org.springframework.boot.maven.it</groupId>
6+
<artifactId>aot</artifactId>
7+
<version>0.0.1.BUILD-SNAPSHOT</version>
8+
<properties>
9+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
10+
<maven.compiler.source>@java.version@</maven.compiler.source>
11+
<maven.compiler.target>@java.version@</maven.compiler.target>
12+
</properties>
13+
<build>
14+
<plugins>
15+
<plugin>
16+
<groupId>@project.groupId@</groupId>
17+
<artifactId>@project.artifactId@</artifactId>
18+
<version>@project.version@</version>
19+
<executions>
20+
<execution>
21+
<goals>
22+
<goal>aot-generate</goal>
23+
</goals>
24+
</execution>
25+
</executions>
26+
</plugin>
27+
</plugins>
28+
</build>
29+
<dependencies>
30+
<dependency>
31+
<groupId>org.springframework.boot</groupId>
32+
<artifactId>spring-boot</artifactId>
33+
<version>@project.version@</version>
34+
</dependency>
35+
<dependency>
36+
<groupId>jakarta.servlet</groupId>
37+
<artifactId>jakarta.servlet-api</artifactId>
38+
<version>@jakarta-servlet.version@</version>
39+
<scope>provided</scope>
40+
</dependency>
41+
</dependencies>
42+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.test;
18+
19+
import org.springframework.boot.SpringApplication;
20+
import org.springframework.context.annotation.Configuration;
21+
22+
@Configuration(proxyBeanMethods = false)
23+
public class SampleApplication {
24+
25+
public static void main(String[] args) {
26+
SpringApplication.run(SampleApplication.class, args);
27+
}
28+
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.maven;
18+
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.net.URL;
22+
import java.util.ArrayList;
23+
import java.util.Arrays;
24+
import java.util.List;
25+
import java.util.Map;
26+
27+
import org.apache.maven.model.Resource;
28+
import org.apache.maven.plugin.MojoExecutionException;
29+
import org.apache.maven.plugin.MojoFailureException;
30+
import org.apache.maven.plugins.annotations.Parameter;
31+
import org.apache.maven.shared.artifact.filter.collection.FilterArtifacts;
32+
33+
import org.springframework.boot.loader.tools.FileUtils;
34+
35+
/**
36+
* Base class to run a spring application.
37+
*
38+
* @author Phillip Webb
39+
* @author Stephane Nicoll
40+
* @author David Liu
41+
* @author Daniel Young
42+
* @author Dmytro Nosan
43+
* @since 6.0.0
44+
* @see RunMojo
45+
* @see StartMojo
46+
*/
47+
public abstract class AbstractApplicationRunMojo extends AbstractRunMojo {
48+
49+
/**
50+
* Add maven resources to the classpath directly, this allows live in-place editing of
51+
* resources. Duplicate resources are removed from {@code target/classes} to prevent
52+
* them to appear twice if {@code ClassLoader.getResources()} is called. Please
53+
* consider adding {@code spring-boot-devtools} to your project instead as it provides
54+
* this feature and many more.
55+
* @since 1.0.0
56+
*/
57+
@Parameter(property = "spring-boot.run.addResources", defaultValue = "false")
58+
private boolean addResources = false;
59+
60+
/**
61+
* Path to agent jars. NOTE: a forked process is required to use this feature.
62+
* @since 2.2.0
63+
*/
64+
@Parameter(property = "spring-boot.run.agents")
65+
private File[] agents;
66+
67+
/**
68+
* Flag to say that the agent requires -noverify.
69+
* @since 1.0.0
70+
*/
71+
@Parameter(property = "spring-boot.run.noverify")
72+
private boolean noverify = false;
73+
74+
/**
75+
* Flag to include the test classpath when running.
76+
* @since 1.3.0
77+
*/
78+
@Parameter(property = "spring-boot.run.useTestClasspath", defaultValue = "false")
79+
private Boolean useTestClasspath;
80+
81+
@Override
82+
protected void run(File workingDirectory, String startClassName, Map<String, String> environmentVariables)
83+
throws MojoExecutionException, MojoFailureException {
84+
List<String> args = new ArrayList<>();
85+
addAgents(args);
86+
addJvmArgs(args);
87+
addClasspath(args);
88+
args.add(startClassName);
89+
addArgs(args);
90+
run(workingDirectory, args, environmentVariables);
91+
}
92+
93+
/**
94+
* Run with a forked VM, using the specified command line arguments.
95+
* @param workingDirectory the working directory of the forked JVM
96+
* @param args the arguments (JVM arguments and application arguments)
97+
* @param environmentVariables the environment variables
98+
* @throws MojoExecutionException in case of MOJO execution errors
99+
* @throws MojoFailureException in case of MOJO failures
100+
*/
101+
protected abstract void run(File workingDirectory, List<String> args, Map<String, String> environmentVariables)
102+
throws MojoExecutionException, MojoFailureException;
103+
104+
@Override
105+
protected URL[] getClassPathUrls() throws MojoExecutionException {
106+
try {
107+
List<URL> urls = new ArrayList<>();
108+
addUserDefinedDirectories(urls);
109+
addResources(urls);
110+
addProjectClasses(urls);
111+
FilterArtifacts filters = (this.useTestClasspath ? getFilters() : getFilters(new TestArtifactFilter()));
112+
addDependencies(urls, filters);
113+
return urls.toArray(new URL[0]);
114+
}
115+
catch (IOException ex) {
116+
throw new MojoExecutionException("Unable to build classpath", ex);
117+
}
118+
}
119+
120+
private void addAgents(List<String> args) {
121+
if (this.agents != null) {
122+
if (getLog().isInfoEnabled()) {
123+
getLog().info("Attaching agents: " + Arrays.asList(this.agents));
124+
}
125+
for (File agent : this.agents) {
126+
args.add("-javaagent:" + agent);
127+
}
128+
}
129+
if (this.noverify) {
130+
args.add("-noverify");
131+
}
132+
}
133+
134+
private void addResources(List<URL> urls) throws IOException {
135+
if (this.addResources) {
136+
for (Resource resource : this.project.getResources()) {
137+
File directory = new File(resource.getDirectory());
138+
urls.add(directory.toURI().toURL());
139+
FileUtils.removeDuplicatesFromOutputDirectory(this.classesDirectory, directory);
140+
}
141+
}
142+
}
143+
144+
}

0 commit comments

Comments
 (0)