19
19
import java .io .File ;
20
20
import java .io .FileNotFoundException ;
21
21
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 ;
22
26
import java .lang .reflect .InvocationHandler ;
23
27
import java .lang .reflect .Method ;
24
28
import java .net .JarURLConnection ;
32
36
import java .util .Comparator ;
33
37
import java .util .Enumeration ;
34
38
import java .util .LinkedHashSet ;
39
+ import java .util .Objects ;
35
40
import java .util .Set ;
41
+ import java .util .function .Predicate ;
36
42
import java .util .jar .JarEntry ;
37
43
import java .util .jar .JarFile ;
44
+ import java .util .stream .Collectors ;
45
+ import java .util .stream .Stream ;
38
46
import java .util .zip .ZipException ;
39
47
40
48
import org .apache .commons .logging .Log ;
140
148
*
141
149
* <p><b>Other notes:</b>
142
150
*
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
+ *
143
159
* <p><b>WARNING:</b> Note that "{@code classpath*:}" when combined with
144
160
* Ant-style patterns will only work reliably with at least one root directory
145
161
* before the pattern starts, unless the actual target files reside in the file
174
190
* @author Marius Bogoevici
175
191
* @author Costin Leau
176
192
* @author Phillip Webb
193
+ * @author Sam Brannen
177
194
* @since 1.0.2
178
195
* @see #CLASSPATH_ALL_URL_PREFIX
179
196
* @see org.springframework.util.AntPathMatcher
@@ -184,6 +201,23 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
184
201
185
202
private static final Log logger = LogFactory .getLog (PathMatchingResourcePatternResolver .class );
186
203
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
+
187
221
@ Nullable
188
222
private static Method equinoxResolveMethod ;
189
223
@@ -280,14 +314,17 @@ public Resource[] getResources(String locationPattern) throws IOException {
280
314
if (locationPattern .startsWith (CLASSPATH_ALL_URL_PREFIX )) {
281
315
// a class path resource (multiple resources for same name possible)
282
316
String locationPatternWithoutPrefix = locationPattern .substring (CLASSPATH_ALL_URL_PREFIX .length ());
317
+ // Search the module path first.
318
+ Set <Resource > resources = findAllModulePathResources (locationPatternWithoutPrefix );
283
319
if (getPathMatcher ().isPattern (locationPatternWithoutPrefix )) {
284
320
// a class path resource pattern
285
- return findPathMatchingResources (locationPattern );
321
+ Collections . addAll ( resources , findPathMatchingResources (locationPattern ) );
286
322
}
287
323
else {
288
324
// all class path resources with the given name
289
- return findAllClassPathResources (locationPatternWithoutPrefix );
325
+ Collections . addAll ( resources , findAllClassPathResources (locationPatternWithoutPrefix ) );
290
326
}
327
+ return resources .toArray (new Resource [0 ]);
291
328
}
292
329
else {
293
330
// Generally only look for a pattern after a prefix here,
@@ -830,6 +867,81 @@ protected File[] listDirectory(File dir) {
830
867
return files ;
831
868
}
832
869
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
+ }
833
945
834
946
private static String stripLeadingSlash (String path ) {
835
947
return (path .startsWith ("/" ) ? path .substring (1 ) : path );
0 commit comments