diff --git a/spring-context/src/main/java/org/springframework/context/aot/AotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/AotProcessor.java
new file mode 100644
index 0000000000..a28ee2dd42
--- /dev/null
+++ b/spring-context/src/main/java/org/springframework/context/aot/AotProcessor.java
@@ -0,0 +1,215 @@
+/*
+ * 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.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+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.
+ *
+ * @author Stephane Nicoll
+ * @author Andy Wilkinson
+ * @author Phillip Webb
+ * @since 6.0
+ */
+public abstract class AotProcessor {
+
+ 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 instance.
+ *
+ * @param application the application entry point
+ * @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 AotProcessor(Class> application, Path sourceOutput, Path resourceOutput,
+ Path classOutput, String groupId, String 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 target outputs first, followed by
+ * {@link #performAotProcessing(GenericApplicationContext)}.
+ * @return the {@code ClassName} of the {@code ApplicationContextInitializer}
+ * entry point
+ */
+ public ClassName process() {
+ deleteExistingOutput();
+ GenericApplicationContext applicationContext = prepareApplicationContext(this.application);
+ return performAotProcessing(applicationContext);
+ }
+
+ /**
+ * Delete the source, resource and class outputs.
+ */
+ protected void deleteExistingOutput() {
+ deleteExistingOutput(this.sourceOutput, this.resourceOutput, this.classOutput);
+ }
+
+ /**
+ * Perform ahead-of-time processing on the specified context. Code,
+ * resources and generated classes are stored in the configured outputs. In
+ * particular, additional hints are registered for the application and its
+ * entry point.
+ * @param applicationContext the context to process.
+ */
+ protected ClassName performAotProcessing(GenericApplicationContext applicationContext) {
+ FileSystemGeneratedFiles generatedFiles = new FileSystemGeneratedFiles(this::getRoot);
+ DefaultGenerationContext generationContext = new DefaultGenerationContext(
+ createClassNameGenerator(), generatedFiles);
+ ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
+ ClassName generatedInitializerClassName = generator.processAheadOfTime(applicationContext, generationContext);
+ registerEntryPointHint(generationContext, generatedInitializerClassName);
+ generationContext.writeGeneratedContent();
+ writeHints(generationContext.getRuntimeHints());
+ writeNativeImageProperties(getDefaultNativeImageArguments(this.application.getName()));
+ return generatedInitializerClassName;
+ }
+
+ /**
+ * Callback to customize the {@link ClassNameGenerator}. By default, a
+ * standard {@link ClassNameGenerator} using the configured application
+ * as the default target is used.
+ * @return the class name generator
+ */
+ protected ClassNameGenerator createClassNameGenerator() {
+ return new ClassNameGenerator(ClassName.get(this.application));
+ }
+
+ /**
+ * Return the native image arguments to use. By default, the main
+ * class to use, as well as standard application flags are added.
+ *
If the returned list is empty, no {@code native-image.properties} is
+ * contributed.
+ * @param application the application entry point
+ * @return the native image options to contribute
+ */
+ protected List getDefaultNativeImageArguments(String application) {
+ List args = new ArrayList<>();
+ args.add("-H:Class=" + application);
+ args.add("--report-unsupported-elements-at-runtime");
+ args.add("--no-fallback");
+ args.add("--install-exit-handlers");
+ return args;
+ }
+
+ private void registerEntryPointHint(DefaultGenerationContext generationContext,
+ ClassName generatedInitializerClassName) {
+ TypeReference generatedType = TypeReference.of(generatedInitializerClassName.canonicalName());
+ TypeReference applicationType = TypeReference.of(this.application);
+ ReflectionHints reflection = generationContext.getRuntimeHints().reflection();
+ reflection.registerType(applicationType);
+ reflection.registerType(generatedType, typeHint -> typeHint.onReachableType(applicationType)
+ .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;
+ }
+ 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");
+ try {
+ if (!Files.exists(file)) {
+ Files.createDirectories(file.getParent());
+ Files.createFile(file);
+ }
+ Files.writeString(file, sb.toString());
+ }
+ catch (IOException 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/AotProcessorTests.java
new file mode 100644
index 0000000000..e5a6a957b6
--- /dev/null
+++ b/spring-context/src/test/java/org/springframework/context/aot/AotProcessorTests.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.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.javapoet.ClassName;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link AotProcessor}.
+ *
+ * @author Stephane Nicoll
+ */
+class AotProcessorTests {
+
+ @Test
+ void processGeneratesAssets(@TempDir Path directory) {
+ GenericApplicationContext context = new AnnotationConfigApplicationContext();
+ context.registerBean(SampleApplication.class);
+ AotProcessor processor = new TestAotProcessor(SampleApplication.class, directory);
+ ClassName className = processor.process();
+ assertThat(className).isEqualTo(ClassName.get(SampleApplication.class.getPackageName(),
+ "AotProcessorTests_SampleApplication__ApplicationContextInitializer"));
+ assertThat(directory).satisfies(hasGeneratedAssetsForSampleApplication());
+ }
+
+ @Test
+ void processingDeletesExistingOutput(@TempDir Path directory) throws IOException {
+ Path sourceOutput = directory.resolve("source");
+ Path resourceOutput = directory.resolve("resource");
+ Path classOutput = directory.resolve("class");
+ Path existingSourceOutput = createExisting(sourceOutput);
+ Path existingResourceOutput = createExisting(resourceOutput);
+ Path existingClassOutput = createExisting(classOutput);
+ AotProcessor processor = new TestAotProcessor(SampleApplication.class,
+ sourceOutput, resourceOutput, classOutput);
+ processor.process();
+ assertThat(existingSourceOutput).doesNotExist();
+ assertThat(existingResourceOutput).doesNotExist();
+ assertThat(existingClassOutput).doesNotExist();
+ }
+
+ @Test
+ void processWithEmptyNativeImageArgumentsDoesNotCreateNativeImageProperties(@TempDir Path directory) {
+ GenericApplicationContext context = new AnnotationConfigApplicationContext();
+ context.registerBean(SampleApplication.class);
+ AotProcessor processor = new TestAotProcessor(SampleApplication.class, directory) {
+ @Override
+ protected List getDefaultNativeImageArguments(String application) {
+ return Collections.emptyList();
+ }
+ };
+ processor.process();
+ assertThat(directory.resolve("resource/META-INF/native-image/com.example/example/native-image.properties"))
+ .doesNotExist();
+ }
+
+ private Path createExisting(Path directory) throws IOException {
+ Path existing = directory.resolve("existing");
+ Files.createDirectories(directory);
+ Files.createFile(existing);
+ return existing;
+ }
+
+ private Consumer hasGeneratedAssetsForSampleApplication() {
+ return directory -> {
+ assertThat(directory.resolve(
+ "source/org/springframework/context/aot/AotProcessorTests_SampleApplication__ApplicationContextInitializer.java"))
+ .exists().isRegularFile();
+ assertThat(directory.resolve("source/org/springframework/context/aot/AotProcessorTests__BeanDefinitions.java"))
+ .exists().isRegularFile();
+ assertThat(directory.resolve(
+ "source/org/springframework/context/aot/AotProcessorTests_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 \\
+ --report-unsupported-elements-at-runtime \\
+ --no-fallback \\
+ --install-exit-handlers
+ """);
+ };
+ }
+
+
+ private static class TestAotProcessor extends AotProcessor {
+
+ public TestAotProcessor(Class> application,
+ Path sourceOutput, Path resourceOutput, Path classOutput) {
+ super(application, sourceOutput, resourceOutput, classOutput, "com.example", "example");
+ }
+
+ public TestAotProcessor(Class> application, Path rootPath) {
+ super(application, rootPath.resolve("source"), rootPath.resolve("resource"),
+ rootPath.resolve("class"), "com.example", "example");
+ }
+
+ @Override
+ protected GenericApplicationContext prepareApplicationContext(Class> application) {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+ context.register(application);
+ return context;
+ }
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ static class SampleApplication {
+
+ @Bean
+ public String testBean() {
+ return "Hello";
+ }
+
+ }
+
+}