Redesign AOT processors for consistency and simplification

There's currently a considerable amount of overlap between the
implementations of AotProcessor and TestAotProcessor. In addition
AotProcessor is abstract and does not include a main() method; whereas,
TestAotProcessor is concrete and does include a main() method.

To address these issues, this commit:

- Introduces an AbstractAotProcessor base class that AotProcessor and
  TestAotProcessor now both extend

- Moves common properties/functionality to AbstractAotProcessor

- Renames AotProcessor to ContextAotProcessor

- Makes TestAotProcessor abstract like ContextAotProcessor

- Removes the main() method from TestAotProcessor

Closes gh-29266
This commit is contained in:
Sam Brannen
2022-10-10 15:25:33 +02:00
parent 061fa475ee
commit b2b3163fae
5 changed files with 200 additions and 163 deletions

View File

@@ -0,0 +1,148 @@
/*
* Copyright 2002-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.context.aot;
import java.io.IOException;
import java.nio.file.Path;
import org.springframework.aot.generate.FileSystemGeneratedFiles;
import org.springframework.aot.generate.GeneratedFiles.Kind;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.nativex.FileNativeConfigurationWriter;
import org.springframework.util.FileSystemUtils;
/**
* Abstract base class for filesystem-based ahead-of-time (AOT) processing.
*
* <p>Concrete implementations are typically used to kick off optimization of an
* application or test suite in a build tool.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @author Phillip Webb
* @author Sam Brannen
* @since 6.0
* @see FileSystemGeneratedFiles
* @see FileNativeConfigurationWriter
* @see org.springframework.context.aot.ContextAotProcessor
* @see org.springframework.test.context.aot.TestAotProcessor
*/
public abstract class AbstractAotProcessor {
private final Path sourceOutput;
private final Path resourceOutput;
private final Path classOutput;
private final String groupId;
private final String artifactId;
/**
* Create a new processor instance.
* @param sourceOutput the location of generated sources
* @param resourceOutput the location of generated resources
* @param classOutput the location of generated classes
* @param groupId the group ID of the application, used to locate
* {@code native-image.properties}
* @param artifactId the artifact ID of the application, used to locate
* {@code native-image.properties}
*/
protected AbstractAotProcessor(Path sourceOutput, Path resourceOutput,
Path classOutput, String groupId, String artifactId) {
this.sourceOutput = sourceOutput;
this.resourceOutput = resourceOutput;
this.classOutput = classOutput;
this.groupId = groupId;
this.artifactId = artifactId;
}
/**
* Get the output directory for generated sources.
*/
protected Path getSourceOutput() {
return this.sourceOutput;
}
/**
* Get the output directory for generated resources.
*/
protected Path getResourceOutput() {
return this.resourceOutput;
}
/**
* Get the output directory for generated classes.
*/
protected Path getClassOutput() {
return this.classOutput;
}
/**
* Get the group ID of the application.
*/
protected String getGroupId() {
return this.groupId;
}
/**
* Get the artifact ID of the application.
*/
protected String getArtifactId() {
return this.artifactId;
}
/**
* Delete the source, resource, and class output directories.
*/
protected void deleteExistingOutput() {
deleteExistingOutput(getSourceOutput(), getResourceOutput(), getClassOutput());
}
private void deleteExistingOutput(Path... paths) {
for (Path path : paths) {
try {
FileSystemUtils.deleteRecursively(path);
}
catch (IOException ex) {
throw new RuntimeException("Failed to delete existing output in '" + path + "'");
}
}
}
protected FileSystemGeneratedFiles createFileSystemGeneratedFiles() {
return new FileSystemGeneratedFiles(this::getRoot);
}
private Path getRoot(Kind kind) {
return switch (kind) {
case SOURCE -> getSourceOutput();
case RESOURCE -> getResourceOutput();
case CLASS -> getClassOutput();
};
}
protected void writeHints(RuntimeHints hints) {
FileNativeConfigurationWriter writer =
new FileNativeConfigurationWriter(getResourceOutput(), getGroupId(), getArtifactId());
writer.write(hints);
}
}

View File

@@ -26,40 +26,29 @@ import java.util.List;
import org.springframework.aot.generate.ClassNameGenerator;
import org.springframework.aot.generate.DefaultGenerationContext;
import org.springframework.aot.generate.FileSystemGeneratedFiles;
import org.springframework.aot.generate.GeneratedFiles.Kind;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.aot.nativex.FileNativeConfigurationWriter;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.javapoet.ClassName;
import org.springframework.util.CollectionUtils;
import org.springframework.util.FileSystemUtils;
/**
* Filesystem-based ahead-of-time processing base implementation. Typically
* used to kick off the optimizations of an application in a build tool.
* Filesystem-based ahead-of-time (AOT) processing base implementation.
*
* <p>Concrete implementations are typically used to kick off optimization of an
* application in a build tool.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @author Phillip Webb
* @since 6.0
* @see org.springframework.test.context.aot.TestAotProcessor
*/
public abstract class AotProcessor {
public abstract class ContextAotProcessor extends AbstractAotProcessor {
private final Class<?> application;
private final Path sourceOutput;
private final Path resourceOutput;
private final Path classOutput;
private final String groupId;
private final String artifactId;
/**
* Create a new processor instance.
* @param application the application entry point
@@ -71,25 +60,13 @@ public abstract class AotProcessor {
* @param artifactId the artifact ID of the application, used to locate
* {@code native-image.properties}
*/
protected AotProcessor(Class<?> application, Path sourceOutput, Path resourceOutput,
protected ContextAotProcessor(Class<?> application, Path sourceOutput, Path resourceOutput,
Path classOutput, String groupId, String artifactId) {
super(sourceOutput, resourceOutput, classOutput, groupId, artifactId);
this.application = application;
this.sourceOutput = sourceOutput;
this.resourceOutput = resourceOutput;
this.classOutput = classOutput;
this.groupId = groupId;
this.artifactId = artifactId;
}
/**
* Prepare the {@link GenericApplicationContext} for the specified
* application to be used against an {@link ApplicationContextAotGenerator}.
* @return a non-refreshed {@link GenericApplicationContext}
*/
protected abstract GenericApplicationContext prepareApplicationContext(Class<?> application);
/**
* Invoke the processing by clearing output directories first, followed by
* {@link #performAotProcessing(GenericApplicationContext)}.
@@ -103,11 +80,11 @@ public abstract class AotProcessor {
}
/**
* Delete the source, resource, and class output directories.
* Prepare the {@link GenericApplicationContext} for the specified
* application to be used against an {@link ApplicationContextAotGenerator}.
* @return a non-refreshed {@link GenericApplicationContext}
*/
protected void deleteExistingOutput() {
deleteExistingOutput(this.sourceOutput, this.resourceOutput, this.classOutput);
}
protected abstract GenericApplicationContext prepareApplicationContext(Class<?> application);
/**
* Perform ahead-of-time processing of the specified context.
@@ -117,7 +94,7 @@ public abstract class AotProcessor {
* @param applicationContext the context to process
*/
protected ClassName performAotProcessing(GenericApplicationContext applicationContext) {
FileSystemGeneratedFiles generatedFiles = new FileSystemGeneratedFiles(this::getRoot);
FileSystemGeneratedFiles generatedFiles = createFileSystemGeneratedFiles();
DefaultGenerationContext generationContext = new DefaultGenerationContext(
createClassNameGenerator(), generatedFiles);
ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
@@ -167,12 +144,6 @@ public abstract class AotProcessor {
.withConstructor(Collections.emptyList(), ExecutableMode.INVOKE));
}
private void writeHints(RuntimeHints hints) {
FileNativeConfigurationWriter writer =
new FileNativeConfigurationWriter(this.resourceOutput, this.groupId, this.artifactId);
writer.write(hints);
}
private void writeNativeImageProperties(List<String> args) {
if (CollectionUtils.isEmpty(args)) {
return;
@@ -180,8 +151,8 @@ public abstract class AotProcessor {
StringBuilder sb = new StringBuilder();
sb.append("Args = ");
sb.append(String.join(String.format(" \\%n"), args));
Path file = this.resourceOutput
.resolve("META-INF/native-image/" + this.groupId + "/" + this.artifactId + "/native-image.properties");
Path file = getResourceOutput()
.resolve("META-INF/native-image/" + getGroupId() + "/" + getArtifactId() + "/native-image.properties");
try {
if (!Files.exists(file)) {
Files.createDirectories(file.getParent());
@@ -190,27 +161,8 @@ public abstract class AotProcessor {
Files.writeString(file, sb.toString());
}
catch (IOException ex) {
throw new IllegalStateException("Failed to write native-image properties", ex);
throw new IllegalStateException("Failed to write native-image.properties", ex);
}
}
private void deleteExistingOutput(Path... paths) {
for (Path path : paths) {
try {
FileSystemUtils.deleteRecursively(path);
}
catch (IOException ex) {
throw new RuntimeException("Failed to delete existing output in '" + path + "'");
}
}
}
private Path getRoot(Kind kind) {
return switch (kind) {
case SOURCE -> this.sourceOutput;
case RESOURCE -> this.resourceOutput;
case CLASS -> this.classOutput;
};
}
}

View File

@@ -35,20 +35,20 @@ import org.springframework.javapoet.ClassName;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link AotProcessor}.
* Tests for {@link ContextAotProcessor}.
*
* @author Stephane Nicoll
*/
class AotProcessorTests {
class ContextAotProcessorTests {
@Test
void processGeneratesAssets(@TempDir Path directory) {
GenericApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBean(SampleApplication.class);
AotProcessor processor = new TestAotProcessor(SampleApplication.class, directory);
ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory);
ClassName className = processor.process();
assertThat(className).isEqualTo(ClassName.get(SampleApplication.class.getPackageName(),
"AotProcessorTests_SampleApplication__ApplicationContextInitializer"));
"ContextAotProcessorTests_SampleApplication__ApplicationContextInitializer"));
assertThat(directory).satisfies(hasGeneratedAssetsForSampleApplication());
context.close();
}
@@ -61,7 +61,7 @@ class AotProcessorTests {
Path existingSourceOutput = createExisting(sourceOutput);
Path existingResourceOutput = createExisting(resourceOutput);
Path existingClassOutput = createExisting(classOutput);
AotProcessor processor = new TestAotProcessor(SampleApplication.class,
ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class,
sourceOutput, resourceOutput, classOutput);
processor.process();
assertThat(existingSourceOutput).doesNotExist();
@@ -73,7 +73,7 @@ class AotProcessorTests {
void processWithEmptyNativeImageArgumentsDoesNotCreateNativeImageProperties(@TempDir Path directory) {
GenericApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBean(SampleApplication.class);
AotProcessor processor = new TestAotProcessor(SampleApplication.class, directory) {
ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory) {
@Override
protected List<String> getDefaultNativeImageArguments(String application) {
return Collections.emptyList();
@@ -95,19 +95,19 @@ class AotProcessorTests {
private Consumer<Path> hasGeneratedAssetsForSampleApplication() {
return directory -> {
assertThat(directory.resolve(
"source/org/springframework/context/aot/AotProcessorTests_SampleApplication__ApplicationContextInitializer.java"))
"source/org/springframework/context/aot/ContextAotProcessorTests_SampleApplication__ApplicationContextInitializer.java"))
.exists().isRegularFile();
assertThat(directory.resolve("source/org/springframework/context/aot/AotProcessorTests__BeanDefinitions.java"))
assertThat(directory.resolve("source/org/springframework/context/aot/ContextAotProcessorTests__BeanDefinitions.java"))
.exists().isRegularFile();
assertThat(directory.resolve(
"source/org/springframework/context/aot/AotProcessorTests_SampleApplication__BeanFactoryRegistrations.java"))
"source/org/springframework/context/aot/ContextAotProcessorTests_SampleApplication__BeanFactoryRegistrations.java"))
.exists().isRegularFile();
assertThat(directory.resolve("resource/META-INF/native-image/com.example/example/reflect-config.json"))
.exists().isRegularFile();
Path nativeImagePropertiesFile = directory
.resolve("resource/META-INF/native-image/com.example/example/native-image.properties");
assertThat(nativeImagePropertiesFile).exists().isRegularFile().hasContent("""
Args = -H:Class=org.springframework.context.aot.AotProcessorTests$SampleApplication \\
Args = -H:Class=org.springframework.context.aot.ContextAotProcessorTests$SampleApplication \\
--report-unsupported-elements-at-runtime \\
--no-fallback \\
--install-exit-handlers
@@ -116,14 +116,14 @@ class AotProcessorTests {
}
private static class TestAotProcessor extends AotProcessor {
private static class DemoContextAotProcessor extends ContextAotProcessor {
public TestAotProcessor(Class<?> application,
DemoContextAotProcessor(Class<?> application,
Path sourceOutput, Path resourceOutput, Path classOutput) {
super(application, sourceOutput, resourceOutput, classOutput, "com.example", "example");
}
public TestAotProcessor(Class<?> application, Path rootPath) {
DemoContextAotProcessor(Class<?> application, Path rootPath) {
super(application, rootPath.resolve("source"), rootPath.resolve("resource"),
rootPath.resolve("class"), "com.example", "example");
}