diff --git a/spring-context/src/main/java/org/springframework/context/aot/AbstractAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/AbstractAotProcessor.java new file mode 100644 index 0000000000..de9bffc103 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/aot/AbstractAotProcessor.java @@ -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. + * + *

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); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/aot/AotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java similarity index 75% rename from spring-context/src/main/java/org/springframework/context/aot/AotProcessor.java rename to spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java index 26d7e68d0f..c645a47f54 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/AotProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java @@ -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. + * + *

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 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; - }; - } - } diff --git a/spring-context/src/test/java/org/springframework/context/aot/AotProcessorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ContextAotProcessorTests.java similarity index 81% rename from spring-context/src/test/java/org/springframework/context/aot/AotProcessorTests.java rename to spring-context/src/test/java/org/springframework/context/aot/ContextAotProcessorTests.java index 3152cdc905..1b306574fe 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/AotProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ContextAotProcessorTests.java @@ -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 getDefaultNativeImageArguments(String application) { return Collections.emptyList(); @@ -95,19 +95,19 @@ class AotProcessorTests { private Consumer 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"); } diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/TestAotProcessor.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestAotProcessor.java index 188806448a..19cd25e29b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/TestAotProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestAotProcessor.java @@ -16,52 +16,29 @@ package org.springframework.test.context.aot; -import java.io.File; -import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; import java.util.Set; import java.util.stream.Stream; -import org.springframework.aot.generate.FileSystemGeneratedFiles; import org.springframework.aot.generate.GeneratedFiles; -import org.springframework.aot.generate.GeneratedFiles.Kind; -import org.springframework.aot.hint.RuntimeHints; -import org.springframework.aot.nativex.FileNativeConfigurationWriter; -import org.springframework.util.Assert; -import org.springframework.util.FileSystemUtils; +import org.springframework.context.aot.AbstractAotProcessor; /** * Filesystem-based ahead-of-time (AOT) processing base implementation that scans * the provided classpath roots for Spring integration test classes and then * generates AOT artifacts for those test classes in the configured output directories. * - *

Typically used in a build tool. + *

Concrete implementations are typically used to kick off optimization of a + * test suite in a build tool. * * @author Sam Brannen - * @author Stephane Nicoll - * @author Andy Wilkinson - * @author Phillip Webb * @since 6.0 * @see TestContextAotGenerator - * @see FileNativeConfigurationWriter - * @see org.springframework.context.aot.AotProcessor + * @see org.springframework.context.aot.ContextAotProcessor */ -public class TestAotProcessor { +public abstract class TestAotProcessor extends AbstractAotProcessor { - private final Path[] classpathRoots; - - private final Path sourceOutput; - - private final Path resourceOutput; - - private final Path classOutput; - - private final String groupId; - - private final String artifactId; + private final Set classpathRoots; /** @@ -76,15 +53,11 @@ public class TestAotProcessor { * @param artifactId the artifact ID of the application, used to locate * {@code native-image.properties} */ - public TestAotProcessor(Path[] classpathRoots, Path sourceOutput, Path resourceOutput, Path classOutput, + public TestAotProcessor(Set classpathRoots, Path sourceOutput, Path resourceOutput, Path classOutput, String groupId, String artifactId) { + super(sourceOutput, resourceOutput, classOutput, groupId, artifactId); this.classpathRoots = classpathRoots; - this.sourceOutput = sourceOutput; - this.resourceOutput = resourceOutput; - this.classOutput = classOutput; - this.groupId = groupId; - this.artifactId = artifactId; } @@ -98,24 +71,6 @@ public class TestAotProcessor { performAotProcessing(); } - /** - * Delete the source, resource, and class output directories. - */ - protected void deleteExistingOutput() { - deleteExistingOutput(this.sourceOutput, this.resourceOutput, this.classOutput); - } - - private void deleteExistingOutput(Path... paths) { - for (Path path : paths) { - try { - FileSystemUtils.deleteRecursively(path); - } - catch (IOException ex) { - throw new UncheckedIOException("Failed to delete existing output in '%s'".formatted(path), ex); - } - } - } - /** * Perform ahead-of-time processing of Spring integration test classes. *

Code, resources, and generated classes are stored in the configured @@ -124,43 +79,14 @@ public class TestAotProcessor { * components used by the tests. */ protected void performAotProcessing() { - TestClassScanner scanner = new TestClassScanner(Set.of(this.classpathRoots)); + TestClassScanner scanner = new TestClassScanner(this.classpathRoots); Stream> testClasses = scanner.scan(); - GeneratedFiles generatedFiles = new FileSystemGeneratedFiles(this::getRoot); + GeneratedFiles generatedFiles = createFileSystemGeneratedFiles(); TestContextAotGenerator generator = new TestContextAotGenerator(generatedFiles); generator.processAheadOfTime(testClasses); writeHints(generator.getRuntimeHints()); } - private Path getRoot(Kind kind) { - return switch (kind) { - case SOURCE -> this.sourceOutput; - case RESOURCE -> this.resourceOutput; - case CLASS -> this.classOutput; - }; - } - - private void writeHints(RuntimeHints hints) { - FileNativeConfigurationWriter writer = - new FileNativeConfigurationWriter(this.resourceOutput, this.groupId, this.artifactId); - writer.write(hints); - } - - - public static void main(String[] args) { - int requiredArgs = 6; - Assert.isTrue(args.length >= requiredArgs, () -> - "Usage: %s " - .formatted(TestAotProcessor.class.getName())); - Path[] classpathRoots = Arrays.stream(args[0].split(File.pathSeparator)).map(Paths::get).toArray(Path[]::new); - Path sourceOutput = Paths.get(args[1]); - Path resourceOutput = Paths.get(args[2]); - Path classOutput = Paths.get(args[3]); - String groupId = args[4]; - String artifactId = args[5]; - new TestAotProcessor(classpathRoots, sourceOutput, resourceOutput, classOutput, groupId, artifactId).process(); - } - } diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java index fdce41c3f3..c98c3ca2f2 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java @@ -22,6 +22,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.stream.Stream; import org.junit.jupiter.api.Test; @@ -58,14 +59,15 @@ class TestAotProcessorTests extends AbstractAotTests { BasicSpringVintageTests.class ).forEach(testClass -> copy(testClass, classpathRoot)); - Path[] classpathRoots = { classpathRoot }; + Set classpathRoots = Set.of(classpathRoot); Path sourceOutput = tempDir.resolve("generated/sources"); Path resourceOutput = tempDir.resolve("generated/resources"); Path classOutput = tempDir.resolve("generated/classes"); String groupId = "org.example"; String artifactId = "app-tests"; - TestAotProcessor processor = new TestAotProcessor(classpathRoots, sourceOutput, resourceOutput, classOutput, groupId, artifactId); + TestAotProcessor processor = + new DemoTestAotProcessor(classpathRoots, sourceOutput, resourceOutput, classOutput, groupId, artifactId); processor.process(); assertThat(findFiles(sourceOutput)).containsExactlyInAnyOrderElementsOf(expectedSourceFiles()); @@ -97,4 +99,13 @@ class TestAotProcessorTests extends AbstractAotTests { return Arrays.stream(expectedSourceFilesForBasicSpringTests).map(Path::of).toList(); } + + private static class DemoTestAotProcessor extends TestAotProcessor { + + DemoTestAotProcessor(Set classpathRoots, Path sourceOutput, Path resourceOutput, Path classOutput, + String groupId, String artifactId) { + super(classpathRoots, sourceOutput, resourceOutput, classOutput, groupId, artifactId); + } + + } }