diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrar.java b/spring-core/src/main/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrar.java
new file mode 100644
index 00000000000..95bb0472321
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrar.java
@@ -0,0 +1,111 @@
+/*
+ * 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.aot.hint.support;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.aot.hint.ResourceHints;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.ResourceUtils;
+
+/**
+ * Register the necessary resource hints for loading files from the classpath.
+ *
+ *
Candidates are identified by a file name, a location, and an extension.
+ * The location can be the empty string to refer to the root of the classpath.
+ *
+ * @author Stephane Nicoll
+ * @since 6.0
+ */
+public class FilePatternResourceHintsRegistrar {
+
+ private final List names;
+
+ private final List locations;
+
+ private final List extensions;
+
+ /**
+ * Create a new instance for the specified file names, locations, and file
+ * extensions.
+ * @param names the file names
+ * @param locations the classpath locations
+ * @param extensions the file extensions (starts with a dot)
+ */
+ public FilePatternResourceHintsRegistrar(List names, List locations,
+ List extensions) {
+ this.names = validateNames(names);
+ this.locations = validateLocations(locations);
+ this.extensions = validateExtensions(extensions);
+ }
+
+ private static List validateNames(List names) {
+ for (String name : names) {
+ if (name.contains("*")) {
+ throw new IllegalArgumentException("File name '" + name + "' cannot contain '*'");
+ }
+ }
+ return names;
+ }
+
+ private static List validateLocations(List locations) {
+ Assert.notEmpty(locations, () -> "At least one location should be specified");
+ List parsedLocations = new ArrayList<>();
+ for (String location : locations) {
+ if (location.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) {
+ location = location.substring(ResourceUtils.CLASSPATH_URL_PREFIX.length());
+ }
+ if (location.startsWith("/")) {
+ location = location.substring(1);
+ }
+ if (!location.isEmpty() && !location.endsWith("/")) {
+ location = location + "/";
+ }
+ parsedLocations.add(location);
+ }
+ return parsedLocations;
+
+ }
+
+ private static List validateExtensions(List extensions) {
+ for (String extension : extensions) {
+ if (!extension.startsWith(".")) {
+ throw new IllegalArgumentException("Extension '" + extension + "' should start with '.'");
+ }
+ }
+ return extensions;
+ }
+
+ public void registerHints(ResourceHints hints, @Nullable ClassLoader classLoader) {
+ ClassLoader classLoaderToUse = (classLoader != null) ? classLoader : getClass().getClassLoader();
+ List includes = new ArrayList<>();
+ for (String location : this.locations) {
+ if (classLoaderToUse.getResource(location) != null) {
+ for (String extension : this.extensions) {
+ for (String name : this.names) {
+ includes.add(location + name + "*" + extension);
+ }
+ }
+ }
+ }
+ if (!includes.isEmpty()) {
+ hints.registerPattern(hint -> hint.includes(includes.toArray(String[]::new)));
+ }
+ }
+}
diff --git a/spring-core/src/test/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrarTests.java b/spring-core/src/test/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrarTests.java
new file mode 100644
index 00000000000..cf419bf8dab
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrarTests.java
@@ -0,0 +1,135 @@
+/*
+ * 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.aot.hint.support;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.aot.hint.ResourceHints;
+import org.springframework.aot.hint.ResourcePatternHint;
+import org.springframework.aot.hint.ResourcePatternHints;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link FilePatternResourceHintsRegistrar}.
+ *
+ * @author Stephane Nicoll
+ */
+class FilePatternResourceHintsRegistrarTests {
+
+ private final ResourceHints hints = new ResourceHints();
+
+ @Test
+ void createWithInvalidName() {
+ assertThatIllegalArgumentException().isThrownBy(() -> new FilePatternResourceHintsRegistrar(
+ List.of("test*"), List.of(""), List.of(".txt")))
+ .withMessageContaining("cannot contain '*'");
+ }
+
+ @Test
+ void createWithInvalidExtension() {
+ assertThatIllegalArgumentException().isThrownBy(() -> new FilePatternResourceHintsRegistrar(
+ List.of("test"), List.of(""), List.of("txt")))
+ .withMessageContaining("should start with '.'");
+ }
+
+ @Test
+ void registerWithSinglePattern() {
+ new FilePatternResourceHintsRegistrar(List.of("test"), List.of(""), List.of(".txt"))
+ .registerHints(this.hints, null);
+ assertThat(this.hints.resourcePatterns()).singleElement()
+ .satisfies(includes("test*.txt"));
+ }
+
+ @Test
+ void registerWithMultipleNames() {
+ new FilePatternResourceHintsRegistrar(List.of("test", "another"), List.of(""), List.of(".txt"))
+ .registerHints(this.hints, null);
+ assertThat(this.hints.resourcePatterns()).singleElement()
+ .satisfies(includes("test*.txt", "another*.txt"));
+ }
+
+ @Test
+ void registerWithMultipleLocations() {
+ new FilePatternResourceHintsRegistrar(List.of("test"), List.of("", "META-INF"), List.of(".txt"))
+ .registerHints(this.hints, null);
+ assertThat(this.hints.resourcePatterns()).singleElement()
+ .satisfies(includes("test*.txt", "META-INF/test*.txt"));
+ }
+
+ @Test
+ void registerWithMultipleExtensions() {
+ new FilePatternResourceHintsRegistrar(List.of("test"), List.of(""), List.of(".txt", ".conf"))
+ .registerHints(this.hints, null);
+ assertThat(this.hints.resourcePatterns()).singleElement()
+ .satisfies(includes("test*.txt", "test*.conf"));
+ }
+
+ @Test
+ void registerWithLocationWithoutTrailingSlash() {
+ new FilePatternResourceHintsRegistrar(List.of("test"), List.of("META-INF"), List.of(".txt"))
+ .registerHints(this.hints, null);
+ assertThat(this.hints.resourcePatterns()).singleElement()
+ .satisfies(includes("META-INF/test*.txt"));
+ }
+
+ @Test
+ void registerWithLocationWithLeadingSlash() {
+ new FilePatternResourceHintsRegistrar(List.of("test"), List.of("/"), List.of(".txt"))
+ .registerHints(this.hints, null);
+ assertThat(this.hints.resourcePatterns()).singleElement()
+ .satisfies(includes("test*.txt"));
+ }
+
+ @Test
+ void registerWithLocationUsingResourceClasspathPrefix() {
+ new FilePatternResourceHintsRegistrar(List.of("test"), List.of("classpath:META-INF"), List.of(".txt"))
+ .registerHints(this.hints, null);
+ assertThat(this.hints.resourcePatterns()).singleElement()
+ .satisfies(includes("META-INF/test*.txt"));
+ }
+
+ @Test
+ void registerWithLocationUsingResourceClasspathPrefixAndTrailingSlash() {
+ new FilePatternResourceHintsRegistrar(List.of("test"), List.of("classpath:/META-INF"), List.of(".txt"))
+ .registerHints(this.hints, null);
+ assertThat(this.hints.resourcePatterns()).singleElement()
+ .satisfies(includes("META-INF/test*.txt"));
+ }
+
+ @Test
+ void registerWithNonExistingLocationDoesNotRegisterHint() {
+ new FilePatternResourceHintsRegistrar(List.of("test"),
+ List.of("does-not-exist/", "another-does-not-exist/"),
+ List.of(".txt")).registerHints(this.hints, null);
+ assertThat(this.hints.resourcePatterns()).isEmpty();
+ }
+
+
+ private Consumer includes(String... patterns) {
+ return hint -> {
+ assertThat(hint.getIncludes().stream().map(ResourcePatternHint::getPattern))
+ .containsExactlyInAnyOrder(patterns);
+ assertThat(hint.getExcludes()).isEmpty();
+ };
+ }
+
+}