diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java index 8aff467ca2f3..273e02033978 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java @@ -19,6 +19,10 @@ 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.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 @@ * *

Other notes: * + *

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. + * *

WARNING: 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 @@ * @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 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 isNotSystemModule = + resolvedModule -> !systemModuleNames.contains(resolvedModule.name()); + @Nullable private static Method equinoxResolveMethod; @@ -280,14 +314,17 @@ public Resource[] getResources(String locationPattern) throws IOException { 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 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 @@ protected File[] listDirectory(File dir) { return files; } + /** + * Resolve the given location pattern into {@code Resource} objects for all + * matching resources found in the module path. + *

The location pattern may be an explicit resource path such as + * {@code "com/example/config.xml"} or a pattern such as + * "com/example/**/config-*.xml" to be matched using the + * configured {@link #getPathMatcher() PathMatcher}. + *

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 findAllModulePathResources(String locationPattern) throws IOException { + Set result = new LinkedHashSet<>(16); + String resourcePattern = stripLeadingSlash(locationPattern); + Predicate 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 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); diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternResolver.java index 822694cf7483..00df4bc0b539 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternResolver.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternResolver.java @@ -41,13 +41,14 @@ * strategy implementation. This interface just specifies the conversion method * rather than a specific pattern format. * - *

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. + *

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 @@ public interface ResourcePatternResolver extends ResourceLoader { /** - * Pseudo URL prefix for all matching resources from the class path: "classpath*:" - *

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*:"}. + *

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"}. + *

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*:";