1919import java .io .File ;
2020import java .io .FileNotFoundException ;
2121import 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 ;
2226import java .lang .reflect .InvocationHandler ;
2327import java .lang .reflect .Method ;
2428import java .net .JarURLConnection ;
3236import java .util .Comparator ;
3337import java .util .Enumeration ;
3438import java .util .LinkedHashSet ;
39+ import java .util .Objects ;
3540import java .util .Set ;
41+ import java .util .function .Predicate ;
3642import java .util .jar .JarEntry ;
3743import java .util .jar .JarFile ;
44+ import java .util .stream .Collectors ;
45+ import java .util .stream .Stream ;
3846import java .util .zip .ZipException ;
3947
4048import org .apache .commons .logging .Log ;
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
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/**/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 );
0 commit comments