Browse Source
Refactor and update the Spring Boot Maven Plugin so that it can be used to perform AOT processing of test classes. Closes gh-32191pull/32196/head
13 changed files with 640 additions and 159 deletions
@ -0,0 +1,25 @@
@@ -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>process-test-aot</id> |
||||
<goals> |
||||
<goal>process-test-aot</goal> |
||||
</goals> |
||||
</execution> |
||||
</executions> |
||||
</plugin> |
||||
<!-- end::aot[] --> |
||||
</plugins> |
||||
</build> |
||||
</project> |
||||
|
||||
|
||||
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
<?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-test</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>process-test-aot</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> |
||||
<dependency> |
||||
<groupId>org.springframework</groupId> |
||||
<artifactId>spring-test</artifactId> |
||||
<version>@spring-framework.version@</version> |
||||
<scope>test</scope> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-test</artifactId> |
||||
<version>@project.version@</version> |
||||
<scope>test</scope> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.assertj</groupId> |
||||
<artifactId>assertj-core</artifactId> |
||||
<version>@assertj.version@</version> |
||||
<scope>test</scope> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.junit.jupiter</groupId> |
||||
<artifactId>junit-jupiter</artifactId> |
||||
<version>@junit-jupiter.version@</version> |
||||
<scope>test</scope> |
||||
</dependency> |
||||
</dependencies> |
||||
</project> |
||||
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
/* |
||||
* 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.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,53 @@
@@ -0,0 +1,53 @@
|
||||
/* |
||||
* 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.test; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.fail; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; |
||||
|
||||
@SpringJUnitConfig |
||||
class SampleApplicationTests { |
||||
|
||||
@Autowired |
||||
private MyBean myBean; |
||||
|
||||
@Test |
||||
void contextLoads() { |
||||
assertThat(this.myBean).isNotNull(); |
||||
} |
||||
|
||||
@Configuration |
||||
static class MyConfig { |
||||
|
||||
@Bean |
||||
MyBean myBean() { |
||||
return new MyBean(); |
||||
} |
||||
|
||||
} |
||||
|
||||
static class MyBean { |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,190 @@
@@ -0,0 +1,190 @@
|
||||
/* |
||||
* 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.Collections; |
||||
import java.util.List; |
||||
import java.util.Locale; |
||||
import java.util.Map; |
||||
|
||||
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.execution.MavenSession; |
||||
import org.apache.maven.plugin.MojoExecutionException; |
||||
import org.apache.maven.plugin.MojoFailureException; |
||||
import org.apache.maven.plugins.annotations.Component; |
||||
import org.apache.maven.plugins.annotations.Parameter; |
||||
import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter; |
||||
import org.apache.maven.toolchain.ToolchainManager; |
||||
|
||||
import org.springframework.boot.maven.CommandLineBuilder.ClasspathBuilder; |
||||
|
||||
/** |
||||
* Abstract base class for AOT processing MOJOs. |
||||
* |
||||
* @author Phillip Webb |
||||
* @since 3.0.0 |
||||
*/ |
||||
public abstract class AbstractAotMojo extends AbstractDependencyFilterMojo { |
||||
|
||||
/** |
||||
* The current Maven session. This is used for toolchain manager API calls. |
||||
*/ |
||||
@Parameter(defaultValue = "${session}", readonly = true) |
||||
private MavenSession session; |
||||
|
||||
/** |
||||
* The toolchain manager to use to locate a custom JDK. |
||||
*/ |
||||
@Component |
||||
private ToolchainManager toolchainManager; |
||||
|
||||
/** |
||||
* Skip the execution. |
||||
*/ |
||||
@Parameter(property = "spring-boot.aot.skip", defaultValue = "false") |
||||
private boolean skip; |
||||
|
||||
/** |
||||
* List of JVM system properties to pass to the AOT process. |
||||
*/ |
||||
@Parameter |
||||
private Map<String, String> systemPropertyVariables; |
||||
|
||||
/** |
||||
* JVM arguments that should be associated with the AOT process. On command line, make |
||||
* sure to wrap multiple values between quotes. |
||||
*/ |
||||
@Parameter(property = "spring-boot.aot.jvmArguments") |
||||
private String jvmArguments; |
||||
|
||||
@Override |
||||
public void execute() throws MojoExecutionException, MojoFailureException { |
||||
if (this.skip) { |
||||
getLog().debug("Skipping AOT execution as per configuration"); |
||||
return; |
||||
} |
||||
try { |
||||
executeAot(); |
||||
} |
||||
catch (Exception ex) { |
||||
throw new MojoExecutionException(ex.getMessage(), ex); |
||||
} |
||||
} |
||||
|
||||
protected abstract void executeAot() throws Exception; |
||||
|
||||
protected void generateAotAssets(URL[] classPath, String processorClassName, String... arguments) throws Exception { |
||||
List<String> command = CommandLineBuilder.forMainClass(processorClassName) |
||||
.withSystemProperties(this.systemPropertyVariables) |
||||
.withJvmArguments(new RunArguments(this.jvmArguments).asArray()).withClasspath(classPath) |
||||
.withArguments(arguments).build(); |
||||
if (getLog().isDebugEnabled()) { |
||||
getLog().debug("Generating AOT assets using command: " + command); |
||||
} |
||||
JavaProcessExecutor processExecutor = new JavaProcessExecutor(this.session, this.toolchainManager); |
||||
processExecutor.run(this.project.getBasedir(), command, Collections.emptyMap()); |
||||
} |
||||
|
||||
protected final void compileSourceFiles(URL[] classPath, File sourcesDirectory, File outputDirectory) |
||||
throws Exception { |
||||
List<Path> sourceFiles = Files.walk(sourcesDirectory.toPath()).filter(Files::isRegularFile).toList(); |
||||
if (sourceFiles.isEmpty()) { |
||||
return; |
||||
} |
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); |
||||
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) { |
||||
List<String> options = new ArrayList<>(); |
||||
options.add("-cp"); |
||||
options.add(ClasspathBuilder.build(Arrays.asList(classPath))); |
||||
options.add("-d"); |
||||
options.add(outputDirectory.toPath().toAbsolutePath().toString()); |
||||
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromPaths(sourceFiles); |
||||
Errors errors = new Errors(); |
||||
CompilationTask task = compiler.getTask(null, fileManager, errors, options, null, compilationUnits); |
||||
boolean result = task.call(); |
||||
if (!result || errors.hasReportedErrors()) { |
||||
throw new IllegalStateException("Unable to compile generated source" + errors); |
||||
} |
||||
} |
||||
} |
||||
|
||||
protected final URL[] getClassPath(File classesDirectory, ArtifactsFilter... artifactFilters) |
||||
throws MojoExecutionException { |
||||
List<URL> urls = new ArrayList<>(); |
||||
urls.add(toURL(classesDirectory)); |
||||
urls.addAll(getDependencyURLs(artifactFilters)); |
||||
return urls.toArray(URL[]::new); |
||||
} |
||||
|
||||
protected final void copyAll(Path from, Path to) throws IOException { |
||||
List<Path> files = (Files.exists(from)) ? Files.walk(from).filter(Files::isRegularFile).toList() |
||||
: Collections.emptyList(); |
||||
for (Path file : files) { |
||||
String relativeFileName = file.subpath(from.getNameCount(), file.getNameCount()).toString(); |
||||
getLog().debug("Copying '" + relativeFileName + "' to " + to); |
||||
Path target = to.resolve(relativeFileName); |
||||
Files.createDirectories(target.getParent()); |
||||
Files.copy(file, target, StandardCopyOption.REPLACE_EXISTING); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* {@link DiagnosticListener} used to collect errors. |
||||
*/ |
||||
protected 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(); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,180 @@
@@ -0,0 +1,180 @@
|
||||
/* |
||||
* Copyright 2012-2022 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.maven; |
||||
|
||||
import java.io.File; |
||||
import java.net.URL; |
||||
import java.nio.file.Files; |
||||
import java.nio.file.Path; |
||||
import java.nio.file.Paths; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.LinkedHashSet; |
||||
import java.util.List; |
||||
import java.util.Set; |
||||
|
||||
import org.apache.maven.artifact.Artifact; |
||||
import org.apache.maven.artifact.DefaultArtifact; |
||||
import org.apache.maven.artifact.handler.DefaultArtifactHandler; |
||||
import org.apache.maven.artifact.repository.ArtifactRepository; |
||||
import org.apache.maven.artifact.resolver.ArtifactResolutionRequest; |
||||
import org.apache.maven.artifact.resolver.ArtifactResolutionResult; |
||||
import org.apache.maven.artifact.resolver.ResolutionErrorHandler; |
||||
import org.apache.maven.plugin.MojoExecutionException; |
||||
import org.apache.maven.plugins.annotations.Component; |
||||
import org.apache.maven.plugins.annotations.LifecyclePhase; |
||||
import org.apache.maven.plugins.annotations.Mojo; |
||||
import org.apache.maven.plugins.annotations.Parameter; |
||||
import org.apache.maven.plugins.annotations.ResolutionScope; |
||||
import org.apache.maven.repository.RepositorySystem; |
||||
|
||||
/** |
||||
* Invoke the AOT engine on tests. |
||||
* |
||||
* @author Phillip Webb |
||||
* @since 3.0.0 |
||||
*/ |
||||
@Mojo(name = "process-test-aot", defaultPhase = LifecyclePhase.PROCESS_TEST_CLASSES, threadSafe = true, |
||||
requiresDependencyResolution = ResolutionScope.TEST, requiresDependencyCollection = ResolutionScope.TEST) |
||||
public class ProcessTestAotMojo extends AbstractAotMojo { |
||||
|
||||
private static final String JUNIT_PLATFORM_GROUP_ID = "org.junit.platform"; |
||||
|
||||
private static final String JUNIT_PLATFORM_COMMONS_ARTIFACT_ID = "junit-platform-commons"; |
||||
|
||||
private static final String JUNIT_PLATFORM_LAUNCHER_ARTIFACT_ID = "junit-platform-launcher"; |
||||
|
||||
private static final String AOT_PROCESSOR_CLASS_NAME = "org.springframework.test.context.aot.TestAotProcessor"; |
||||
|
||||
/** |
||||
* Directory containing the classes and resource files that should be packaged into |
||||
* the archive. |
||||
*/ |
||||
@Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true) |
||||
private File classesDirectory; |
||||
|
||||
/** |
||||
* Directory containing the generated sources. |
||||
*/ |
||||
@Parameter(defaultValue = "${project.build.directory}/spring-aot/test/sources", required = true) |
||||
private File generatedSources; |
||||
|
||||
/** |
||||
* Directory containing the generated resources. |
||||
*/ |
||||
@Parameter(defaultValue = "${project.build.directory}/spring-aot/test/resources", required = true) |
||||
private File generatedResources; |
||||
|
||||
/** |
||||
* Directory containing the generated classes. |
||||
*/ |
||||
@Parameter(defaultValue = "${project.build.directory}/spring-aot/test/classes", required = true) |
||||
private File generatedClasses; |
||||
|
||||
/** |
||||
* Local artifact repository used to resolve JUnit platform launcher jars. |
||||
*/ |
||||
@Parameter(defaultValue = "${localRepository}", required = true, readonly = true) |
||||
private ArtifactRepository localRepository; |
||||
|
||||
/** |
||||
* Remove artifact repositories used to resolve JUnit platform launcher jars. |
||||
*/ |
||||
@Parameter(defaultValue = "${project.remoteArtifactRepositories}", required = true, readonly = true) |
||||
private List<ArtifactRepository> remoteRepositories; |
||||
|
||||
@Component |
||||
private RepositorySystem repositorySystem; |
||||
|
||||
@Component |
||||
private ResolutionErrorHandler resolutionErrorHandler; |
||||
|
||||
@Override |
||||
protected void executeAot() throws Exception { |
||||
if (Boolean.getBoolean("skipTests") || Boolean.getBoolean("maven.test.skip")) { |
||||
getLog().info("Skipping AOT test processing since tests are skipped"); |
||||
return; |
||||
} |
||||
Path testOutputDirectory = Paths.get(this.project.getBuild().getTestOutputDirectory()); |
||||
if (Files.notExists(testOutputDirectory)) { |
||||
getLog().info("Skipping AOT test processing since no tests have been detected"); |
||||
return; |
||||
} |
||||
generateAotAssets(getClassPath(true), AOT_PROCESSOR_CLASS_NAME, getAotArguments()); |
||||
compileSourceFiles(getClassPath(false), this.generatedSources, this.classesDirectory); |
||||
copyAll(this.generatedResources.toPath().resolve("META-INF/native-image"), |
||||
this.classesDirectory.toPath().resolve("META-INF/native-image")); |
||||
copyAll(this.generatedClasses.toPath(), this.classesDirectory.toPath()); |
||||
} |
||||
|
||||
private String[] getAotArguments() { |
||||
List<String> aotArguments = new ArrayList<>(); |
||||
aotArguments.add(this.classesDirectory.toPath().toAbsolutePath().normalize().toString()); |
||||
aotArguments.add(this.generatedSources.toString()); |
||||
aotArguments.add(this.generatedResources.toString()); |
||||
aotArguments.add(this.generatedClasses.toString()); |
||||
aotArguments.add(this.project.getGroupId()); |
||||
aotArguments.add(this.project.getArtifactId()); |
||||
return aotArguments.toArray(String[]::new); |
||||
} |
||||
|
||||
protected URL[] getClassPath(boolean includeJUnitPlatformLauncher) throws Exception { |
||||
URL[] classPath = getClassPath(this.classesDirectory); |
||||
if (!includeJUnitPlatformLauncher || this.project.getArtifactMap() |
||||
.containsKey(JUNIT_PLATFORM_GROUP_ID + ":" + JUNIT_PLATFORM_LAUNCHER_ARTIFACT_ID)) { |
||||
return classPath; |
||||
} |
||||
return addJUnitPlatformLauncher(classPath); |
||||
} |
||||
|
||||
private URL[] addJUnitPlatformLauncher(URL[] classPath) throws Exception { |
||||
String version = getJUnitPlatformVersion(); |
||||
DefaultArtifactHandler handler = new DefaultArtifactHandler("jar"); |
||||
handler.setIncludesDependencies(true); |
||||
ArtifactResolutionResult resolutionResult = resolveArtifact(new DefaultArtifact(JUNIT_PLATFORM_GROUP_ID, |
||||
JUNIT_PLATFORM_LAUNCHER_ARTIFACT_ID, version, null, "jar", null, handler)); |
||||
Set<URL> fullClassPath = new LinkedHashSet<>(Arrays.asList(classPath)); |
||||
for (Artifact artifact : resolutionResult.getArtifacts()) { |
||||
fullClassPath.add(artifact.getFile().toURI().toURL()); |
||||
} |
||||
return fullClassPath.toArray(URL[]::new); |
||||
} |
||||
|
||||
private String getJUnitPlatformVersion() throws MojoExecutionException { |
||||
String id = JUNIT_PLATFORM_GROUP_ID + ":" + JUNIT_PLATFORM_COMMONS_ARTIFACT_ID; |
||||
Artifact platformCommonsArtifact = this.project.getArtifactMap().get(id); |
||||
String version = (platformCommonsArtifact != null) ? platformCommonsArtifact.getBaseVersion() : null; |
||||
if (version == null) { |
||||
throw new MojoExecutionException( |
||||
"Unable to find '%s' dependnecy. Please ensure JUnit is correctly configured.".formatted(id)); |
||||
} |
||||
return version; |
||||
} |
||||
|
||||
private ArtifactResolutionResult resolveArtifact(Artifact artifact) throws Exception { |
||||
ArtifactResolutionRequest request = new ArtifactResolutionRequest(); |
||||
request.setArtifact(artifact); |
||||
request.setLocalRepository(this.localRepository); |
||||
request.setResolveTransitively(true); |
||||
request.setCollectionFilter(new RuntimeArtifactFilter()); |
||||
request.setRemoteRepositories(this.remoteRepositories); |
||||
ArtifactResolutionResult result = this.repositorySystem.resolve(request); |
||||
this.resolutionErrorHandler.throwErrors(request, result); |
||||
return result; |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue