Skip to content

Commit 19b436c

Browse files
committed
Support module path scanning for "classpath*:" resource prefix
Prior to this commit, searching for classpath resources using the "classpath*:" resource prefix did not find all applicable resources for applications deployed as modules -- for example, when test classes and resources are patched into the application module automatically by Maven Surefire. This affected component scanning -- for example, via [@]ComponentScan -- and PathMatchingResourcePatternResolver.getResources(String) in general. This commit addresses this by introducing first-class support for scanning the module path when PathMatchingResourcePatternResolver's getResources(String) method is invoked with a location pattern using the "classpath*:" resource prefix. Specifically, getResources(String) first searches all modules in the boot layer, excluding system modules. It then searches the classpath using the existing Classloader-based algorithm and returns the combined results. Closes gh-28506
1 parent 6bd25f6 commit 19b436c

File tree

2 files changed

+127
-11
lines changed

2 files changed

+127
-11
lines changed

spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java

+114-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
import java.io.File;
2020
import java.io.FileNotFoundException;
2121
import java.io.IOException;
22+
import java.io.UncheckedIOException;
23+
import java.lang.module.ModuleFinder;
24+
import java.lang.module.ModuleReader;
25+
import java.lang.module.ResolvedModule;
2226
import java.lang.reflect.InvocationHandler;
2327
import java.lang.reflect.Method;
2428
import java.net.JarURLConnection;
@@ -32,9 +36,13 @@
3236
import java.util.Comparator;
3337
import java.util.Enumeration;
3438
import java.util.LinkedHashSet;
39+
import java.util.Objects;
3540
import java.util.Set;
41+
import java.util.function.Predicate;
3642
import java.util.jar.JarEntry;
3743
import java.util.jar.JarFile;
44+
import java.util.stream.Collectors;
45+
import java.util.stream.Stream;
3846
import java.util.zip.ZipException;
3947

4048
import org.apache.commons.logging.Log;
@@ -140,6 +148,14 @@
140148
*
141149
* <p><b>Other notes:</b>
142150
*
151+
* <p>As of Spring Framework 6.0, if {@link #getResources(String)} is invoked
152+
* with a location pattern using the "classpath*:" prefix it will first search
153+
* all modules in the {@linkplain ModuleLayer#boot() boot layer}, excluding
154+
* {@linkplain ModuleFinder#ofSystem() system modules}. It will then search the
155+
* classpath using {@link Classloader} APIs as described previously and return the
156+
* combined results. Consequently, some of the limitations of classpath searches
157+
* may not apply when applications are deployed as modules.
158+
*
143159
* <p><b>WARNING:</b> Note that "{@code classpath*:}" when combined with
144160
* Ant-style patterns will only work reliably with at least one root directory
145161
* before the pattern starts, unless the actual target files reside in the file
@@ -174,6 +190,7 @@
174190
* @author Marius Bogoevici
175191
* @author Costin Leau
176192
* @author Phillip Webb
193+
* @author Sam Brannen
177194
* @since 1.0.2
178195
* @see #CLASSPATH_ALL_URL_PREFIX
179196
* @see org.springframework.util.AntPathMatcher
@@ -184,6 +201,23 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
184201

185202
private static final Log logger = LogFactory.getLog(PathMatchingResourcePatternResolver.class);
186203

204+
/**
205+
* {@link Set} of {@linkplain ModuleFinder#ofSystem() system module} names.
206+
* @since 6.0
207+
* @see #isNotSystemModule
208+
*/
209+
private static final Set<String> systemModuleNames = ModuleFinder.ofSystem().findAll().stream()
210+
.map(moduleReference -> moduleReference.descriptor().name()).collect(Collectors.toSet());
211+
212+
/**
213+
* {@link Predicate} that tests whether the supplied {@link ResolvedModule}
214+
* is not a {@linkplain ModuleFinder#ofSystem() system module}.
215+
* @since 6.0
216+
* @see #systemModuleNames
217+
*/
218+
private static final Predicate<ResolvedModule> isNotSystemModule =
219+
resolvedModule -> !systemModuleNames.contains(resolvedModule.name());
220+
187221
@Nullable
188222
private static Method equinoxResolveMethod;
189223

@@ -280,14 +314,17 @@ public Resource[] getResources(String locationPattern) throws IOException {
280314
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
281315
// a class path resource (multiple resources for same name possible)
282316
String locationPatternWithoutPrefix = locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length());
317+
// Search the module path first.
318+
Set<Resource> resources = findAllModulePathResources(locationPatternWithoutPrefix);
283319
if (getPathMatcher().isPattern(locationPatternWithoutPrefix)) {
284320
// a class path resource pattern
285-
return findPathMatchingResources(locationPattern);
321+
Collections.addAll(resources, findPathMatchingResources(locationPattern));
286322
}
287323
else {
288324
// all class path resources with the given name
289-
return findAllClassPathResources(locationPatternWithoutPrefix);
325+
Collections.addAll(resources, findAllClassPathResources(locationPatternWithoutPrefix));
290326
}
327+
return resources.toArray(new Resource[0]);
291328
}
292329
else {
293330
// Generally only look for a pattern after a prefix here,
@@ -830,6 +867,81 @@ protected File[] listDirectory(File dir) {
830867
return files;
831868
}
832869

870+
/**
871+
* Resolve the given location pattern into {@code Resource} objects for all
872+
* matching resources found in the module path.
873+
* <p>The location pattern may be an explicit resource path such as
874+
* {@code "com/example/config.xml"} or a pattern such as
875+
* <code>"com/example/**&#47;config-*.xml"</code> to be matched using the
876+
* configured {@link #getPathMatcher() PathMatcher}.
877+
* <p>The default implementation scans all modules in the {@linkplain ModuleLayer#boot()
878+
* boot layer}, excluding {@linkplain ModuleFinder#ofSystem() system modules}.
879+
* @param locationPattern the location pattern to resolve
880+
* @return a modifiable {@code Set} containing the corresponding {@code Resource}
881+
* objects
882+
* @throws IOException in case of I/O errors
883+
* @since 6.0
884+
* @see ModuleLayer#boot()
885+
* @see ModuleFinder#ofSystem()
886+
* @see ModuleReader
887+
* @see PathMatcher#match(String, String)
888+
*/
889+
protected Set<Resource> findAllModulePathResources(String locationPattern) throws IOException {
890+
Set<Resource> result = new LinkedHashSet<>(16);
891+
String resourcePattern = stripLeadingSlash(locationPattern);
892+
Predicate<String> resourcePatternMatches = (getPathMatcher().isPattern(resourcePattern) ?
893+
path -> getPathMatcher().match(resourcePattern, path) :
894+
resourcePattern::equals);
895+
896+
try {
897+
ModuleLayer.boot().configuration().modules().stream()
898+
.filter(isNotSystemModule)
899+
.forEach(resolvedModule -> {
900+
// NOTE: a ModuleReader and a Stream returned from ModuleReader.list() must be closed.
901+
try (ModuleReader moduleReader = resolvedModule.reference().open();
902+
Stream<String> names = moduleReader.list()) {
903+
names.filter(resourcePatternMatches)
904+
.map(name -> findResource(moduleReader, name))
905+
.filter(Objects::nonNull)
906+
.forEach(result::add);
907+
}
908+
catch (IOException ex) {
909+
if (logger.isDebugEnabled()) {
910+
logger.debug("Failed to read contents of module [%s]".formatted(resolvedModule), ex);
911+
}
912+
throw new UncheckedIOException(ex);
913+
}
914+
});
915+
}
916+
catch (UncheckedIOException ex) {
917+
// Unwrap IOException to conform to this method's contract.
918+
throw ex.getCause();
919+
}
920+
921+
if (logger.isTraceEnabled()) {
922+
logger.trace("Resolved module-path location pattern [%s] to resources %s".formatted(resourcePattern, result));
923+
}
924+
return result;
925+
}
926+
927+
@Nullable
928+
private static Resource findResource(ModuleReader moduleReader, String name) {
929+
try {
930+
return moduleReader.find(name)
931+
// If it's a "file:" URI, use FileSystemResource to avoid duplicates
932+
// for the same path discovered via class-path scanning.
933+
.map(uri -> ResourceUtils.URL_PROTOCOL_FILE.equals(uri.getScheme()) ?
934+
new FileSystemResource(uri.getPath()) :
935+
UrlResource.from(uri))
936+
.orElse(null);
937+
}
938+
catch (Exception ex) {
939+
if (logger.isDebugEnabled()) {
940+
logger.debug("Failed to find resource [%s] in module path".formatted(name), ex);
941+
}
942+
return null;
943+
}
944+
}
833945

834946
private static String stripLeadingSlash(String path) {
835947
return (path.startsWith("/") ? path.substring(1) : path);

spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternResolver.java

+13-9
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,14 @@
4141
* strategy implementation. This interface just specifies the conversion method
4242
* rather than a specific pattern format.
4343
*
44-
* <p>This interface also defines a {@code "classpath*:"} resource prefix for all
45-
* matching resources from the class path. Note that the resource location may
46-
* also contain placeholders &mdash; for example {@code "/beans-*.xml"}. JAR files
47-
* or different directories in the class path can contain multiple files of the
48-
* same name.
44+
* <p>This interface also defines a {@value #CLASSPATH_ALL_URL_PREFIX} resource
45+
* prefix for all matching resources from the module path and the class path. Note
46+
* that the resource location may also contain placeholders &mdash; for example
47+
* {@code "/beans-*.xml"}. JAR files or different directories in the module path
48+
* or class path can contain multiple files of the same name.
4949
*
5050
* @author Juergen Hoeller
51+
* @author Sam Brannen
5152
* @since 1.0.2
5253
* @see org.springframework.core.io.Resource
5354
* @see org.springframework.core.io.ResourceLoader
@@ -57,10 +58,13 @@
5758
public interface ResourcePatternResolver extends ResourceLoader {
5859

5960
/**
60-
* Pseudo URL prefix for all matching resources from the class path: "classpath*:"
61-
* <p>This differs from ResourceLoader's classpath URL prefix in that it
62-
* retrieves all matching resources for a given name (e.g. "/beans.xml"),
63-
* for example in the root of all deployed JAR files.
61+
* Pseudo URL prefix for all matching resources from the class path: {@code "classpath*:"}.
62+
* <p>This differs from ResourceLoader's {@code "classpath:"} URL prefix in
63+
* that it retrieves all matching resources for a given path &mdash; for
64+
* example, to locate all "beans.xml" files in the root of all deployed JAR
65+
* files you can use the location pattern {@code "classpath*:/beans.xml"}.
66+
* <p>As of Spring Framework 6.0, the semantics for the {@code "classpath*:"}
67+
* prefix have been expanded to include the module path as well as the class path.
6468
* @see org.springframework.core.io.ResourceLoader#CLASSPATH_URL_PREFIX
6569
*/
6670
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

0 commit comments

Comments
 (0)