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
This commit is contained in:
parent
6bd25f60dd
commit
19b436c6aa
|
@ -19,6 +19,10 @@ package org.springframework.core.io.support;
|
|||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.module.ModuleFinder;
|
||||
import java.lang.module.ModuleReader;
|
||||
import java.lang.module.ResolvedModule;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.JarURLConnection;
|
||||
|
@ -32,9 +36,13 @@ import java.util.Collections;
|
|||
import java.util.Comparator;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipException;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
|
@ -140,6 +148,14 @@ import org.springframework.util.StringUtils;
|
|||
*
|
||||
* <p><b>Other notes:</b>
|
||||
*
|
||||
* <p>As of Spring Framework 6.0, if {@link #getResources(String)} is invoked
|
||||
* with a location pattern using the "classpath*:" prefix it will first search
|
||||
* all modules in the {@linkplain ModuleLayer#boot() boot layer}, excluding
|
||||
* {@linkplain ModuleFinder#ofSystem() system modules}. It will then search the
|
||||
* classpath using {@link Classloader} APIs as described previously and return the
|
||||
* combined results. Consequently, some of the limitations of classpath searches
|
||||
* may not apply when applications are deployed as modules.
|
||||
*
|
||||
* <p><b>WARNING:</b> Note that "{@code classpath*:}" when combined with
|
||||
* Ant-style patterns will only work reliably with at least one root directory
|
||||
* before the pattern starts, unless the actual target files reside in the file
|
||||
|
@ -174,6 +190,7 @@ import org.springframework.util.StringUtils;
|
|||
* @author Marius Bogoevici
|
||||
* @author Costin Leau
|
||||
* @author Phillip Webb
|
||||
* @author Sam Brannen
|
||||
* @since 1.0.2
|
||||
* @see #CLASSPATH_ALL_URL_PREFIX
|
||||
* @see org.springframework.util.AntPathMatcher
|
||||
|
@ -184,6 +201,23 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
|||
|
||||
private static final Log logger = LogFactory.getLog(PathMatchingResourcePatternResolver.class);
|
||||
|
||||
/**
|
||||
* {@link Set} of {@linkplain ModuleFinder#ofSystem() system module} names.
|
||||
* @since 6.0
|
||||
* @see #isNotSystemModule
|
||||
*/
|
||||
private static final Set<String> systemModuleNames = ModuleFinder.ofSystem().findAll().stream()
|
||||
.map(moduleReference -> moduleReference.descriptor().name()).collect(Collectors.toSet());
|
||||
|
||||
/**
|
||||
* {@link Predicate} that tests whether the supplied {@link ResolvedModule}
|
||||
* is not a {@linkplain ModuleFinder#ofSystem() system module}.
|
||||
* @since 6.0
|
||||
* @see #systemModuleNames
|
||||
*/
|
||||
private static final Predicate<ResolvedModule> isNotSystemModule =
|
||||
resolvedModule -> !systemModuleNames.contains(resolvedModule.name());
|
||||
|
||||
@Nullable
|
||||
private static Method equinoxResolveMethod;
|
||||
|
||||
|
@ -280,14 +314,17 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
|||
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
|
||||
// a class path resource (multiple resources for same name possible)
|
||||
String locationPatternWithoutPrefix = locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length());
|
||||
// Search the module path first.
|
||||
Set<Resource> resources = findAllModulePathResources(locationPatternWithoutPrefix);
|
||||
if (getPathMatcher().isPattern(locationPatternWithoutPrefix)) {
|
||||
// a class path resource pattern
|
||||
return findPathMatchingResources(locationPattern);
|
||||
Collections.addAll(resources, findPathMatchingResources(locationPattern));
|
||||
}
|
||||
else {
|
||||
// all class path resources with the given name
|
||||
return findAllClassPathResources(locationPatternWithoutPrefix);
|
||||
Collections.addAll(resources, findAllClassPathResources(locationPatternWithoutPrefix));
|
||||
}
|
||||
return resources.toArray(new Resource[0]);
|
||||
}
|
||||
else {
|
||||
// Generally only look for a pattern after a prefix here,
|
||||
|
@ -830,6 +867,81 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
|||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the given location pattern into {@code Resource} objects for all
|
||||
* matching resources found in the module path.
|
||||
* <p>The location pattern may be an explicit resource path such as
|
||||
* {@code "com/example/config.xml"} or a pattern such as
|
||||
* <code>"com/example/**/config-*.xml"</code> to be matched using the
|
||||
* configured {@link #getPathMatcher() PathMatcher}.
|
||||
* <p>The default implementation scans all modules in the {@linkplain ModuleLayer#boot()
|
||||
* boot layer}, excluding {@linkplain ModuleFinder#ofSystem() system modules}.
|
||||
* @param locationPattern the location pattern to resolve
|
||||
* @return a modifiable {@code Set} containing the corresponding {@code Resource}
|
||||
* objects
|
||||
* @throws IOException in case of I/O errors
|
||||
* @since 6.0
|
||||
* @see ModuleLayer#boot()
|
||||
* @see ModuleFinder#ofSystem()
|
||||
* @see ModuleReader
|
||||
* @see PathMatcher#match(String, String)
|
||||
*/
|
||||
protected Set<Resource> findAllModulePathResources(String locationPattern) throws IOException {
|
||||
Set<Resource> result = new LinkedHashSet<>(16);
|
||||
String resourcePattern = stripLeadingSlash(locationPattern);
|
||||
Predicate<String> resourcePatternMatches = (getPathMatcher().isPattern(resourcePattern) ?
|
||||
path -> getPathMatcher().match(resourcePattern, path) :
|
||||
resourcePattern::equals);
|
||||
|
||||
try {
|
||||
ModuleLayer.boot().configuration().modules().stream()
|
||||
.filter(isNotSystemModule)
|
||||
.forEach(resolvedModule -> {
|
||||
// NOTE: a ModuleReader and a Stream returned from ModuleReader.list() must be closed.
|
||||
try (ModuleReader moduleReader = resolvedModule.reference().open();
|
||||
Stream<String> names = moduleReader.list()) {
|
||||
names.filter(resourcePatternMatches)
|
||||
.map(name -> findResource(moduleReader, name))
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(result::add);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Failed to read contents of module [%s]".formatted(resolvedModule), ex);
|
||||
}
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (UncheckedIOException ex) {
|
||||
// Unwrap IOException to conform to this method's contract.
|
||||
throw ex.getCause();
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Resolved module-path location pattern [%s] to resources %s".formatted(resourcePattern, result));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Resource findResource(ModuleReader moduleReader, String name) {
|
||||
try {
|
||||
return moduleReader.find(name)
|
||||
// If it's a "file:" URI, use FileSystemResource to avoid duplicates
|
||||
// for the same path discovered via class-path scanning.
|
||||
.map(uri -> ResourceUtils.URL_PROTOCOL_FILE.equals(uri.getScheme()) ?
|
||||
new FileSystemResource(uri.getPath()) :
|
||||
UrlResource.from(uri))
|
||||
.orElse(null);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Failed to find resource [%s] in module path".formatted(name), ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String stripLeadingSlash(String path) {
|
||||
return (path.startsWith("/") ? path.substring(1) : path);
|
||||
|
|
|
@ -41,13 +41,14 @@ import org.springframework.core.io.ResourceLoader;
|
|||
* strategy implementation. This interface just specifies the conversion method
|
||||
* rather than a specific pattern format.
|
||||
*
|
||||
* <p>This interface also defines a {@code "classpath*:"} resource prefix for all
|
||||
* matching resources from the class path. Note that the resource location may
|
||||
* also contain placeholders — for example {@code "/beans-*.xml"}. JAR files
|
||||
* or different directories in the class path can contain multiple files of the
|
||||
* same name.
|
||||
* <p>This interface also defines a {@value #CLASSPATH_ALL_URL_PREFIX} resource
|
||||
* prefix for all matching resources from the module path and the class path. Note
|
||||
* that the resource location may also contain placeholders — for example
|
||||
* {@code "/beans-*.xml"}. JAR files or different directories in the module path
|
||||
* or class path can contain multiple files of the same name.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 1.0.2
|
||||
* @see org.springframework.core.io.Resource
|
||||
* @see org.springframework.core.io.ResourceLoader
|
||||
|
@ -57,10 +58,13 @@ import org.springframework.core.io.ResourceLoader;
|
|||
public interface ResourcePatternResolver extends ResourceLoader {
|
||||
|
||||
/**
|
||||
* Pseudo URL prefix for all matching resources from the class path: "classpath*:"
|
||||
* <p>This differs from ResourceLoader's classpath URL prefix in that it
|
||||
* retrieves all matching resources for a given name (e.g. "/beans.xml"),
|
||||
* for example in the root of all deployed JAR files.
|
||||
* Pseudo URL prefix for all matching resources from the class path: {@code "classpath*:"}.
|
||||
* <p>This differs from ResourceLoader's {@code "classpath:"} URL prefix in
|
||||
* that it retrieves all matching resources for a given path — for
|
||||
* example, to locate all "beans.xml" files in the root of all deployed JAR
|
||||
* files you can use the location pattern {@code "classpath*:/beans.xml"}.
|
||||
* <p>As of Spring Framework 6.0, the semantics for the {@code "classpath*:"}
|
||||
* prefix have been expanded to include the module path as well as the class path.
|
||||
* @see org.springframework.core.io.ResourceLoader#CLASSPATH_URL_PREFIX
|
||||
*/
|
||||
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
|
||||
|
|
Loading…
Reference in New Issue