What reliability and portability pitfalls arise with classpath*: wildcard scanning across exploded classes, standard JARs, and Spring Boot fat JARs?
answer
- classpath*: root split -> ClassLoader.getResources(rootDir)
- root wildcard (classpath*:*.xml) fragile: getResources("") gap
- Boot fat JAR = nested jars, LaunchedURLClassLoader, getFile() throws
- IDE exploded hides failures -> test the packaged artifact
- no order guarantee; cache; anchor under a package
basics
~20 sWildcard classpath*: scanning depends on the classloader enumerating JAR roots. Root-level wildcards can silently miss files in some servers and in Spring Boot's nested-JAR layout. Anchor patterns under a concrete package, avoid getFile(), and use getInputStream() so code works both exploded and packaged.
solid answer
~40 s`PathMatchingResourcePatternResolver` expands `classpath*:` by asking the `ClassLoader` for all roots, then walking each root/JAR with an `AntPathMatcher`. This is robust when the pattern is anchored under a real package (`classpath*:META-INF/spring/*.xml`) but fragile when the wildcard sits at the root (`classpath*:*.xml`), because it relies on `ClassLoader.getResources("")` returning JAR roots — many app-server and custom classloaders don't. Spring Boot fat JARs add a wrinkle: dependencies are **nested** JARs served by `LaunchedURLClassLoader`, so filesystem-style `getFile()` fails and only URL/stream access works; anchored `classpath*:` patterns are supported but you must read via `getInputStream()`. Portability rules I enforce: always anchor under a package segment, never rely on `Resource[]` ordering for precedence, prefer `getInputStream()` over `getFile()`, cache scan results (scanning every JAR is slow), and test the packaged artifact, not just the IDE run, because exploded classpaths hide these failures.
code
java · 22 linesimport org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import java.io.InputStream;
import java.util.Comparator;
public class RobustScan {
private final PathMatchingResourcePatternResolver resolver =
new PathMatchingResourcePatternResolver();
// Anchored under META-INF (not a bare root wildcard); stream-based (fat-JAR safe).
public void loadPlugins() throws Exception {
Resource[] found = resolver.getResources("classpath*:META-INF/katajob/plugin-*.json");
java.util.Arrays.sort(found, Comparator.comparing(Resource::getFilename)); // deterministic
for (Resource r : found) {
if (!r.isReadable()) continue;
try (InputStream in = r.getInputStream()) { // NOT getFile() — nested JARs have no File
parse(in);
}
}
}
private void parse(InputStream in) { /* ... */ }
}go deeper
Beyond scope; a junior isn't expected to reason about classloader/packaging effects.
Should at least know getFile() can fail in JARs and to prefer getInputStream().
Should explain the root-split expansion, anchoring patterns, and the exploded-vs-packaged divergence.
Sets org-wide conventions: anchored patterns, stream access, caching, deterministic sorting, packaged-artifact tests, and explicit descriptors over scanning when possible.
## How classpath*: expansion actually works When you call `getResources("classpath*:some/pkg/*.xml")`, `PathMatchingResourcePatternResolver`: 1. Splits the location into a **root** part (the longest leading portion with no wildcard, e.g. `some/pkg/`) and a **pattern** part (`*.xml`). 2. Calls `ClassLoader.getResources("some/pkg/")` to enumerate **every** occurrence of that root directory across all classpath entries (dirs and JARs). 3. For each root URL, walks it — filesystem directory traversal for `file:` URLs, JAR entry enumeration for `jar:` URLs — matching the pattern with `AntPathMatcher`. The reliability of step 2 depends entirely on the classloader. ## Pitfall 1 — root-level wildcards `classpath*:*.xml` or `classpath*:/*.xml` forces Spring to enumerate the classpath **root** via `ClassLoader.getResources("")`. The JDK's `URLClassLoader` returns JAR roots for the empty string, but **many classloaders do not** — several servlet containers, OSGi, and custom loaders return nothing or only directory roots, so files inside JARs are silently missed. **Fix:** always put at least one concrete, non-wildcard segment at the front: `classpath*:META-INF/*.xml`, `classpath*:config/**/*.yml`. ## Pitfall 2 — Spring Boot fat / executable JARs Spring Boot repackages your app as a single JAR with dependency JARs **nested** inside `BOOT-INF/lib/` and your classes under `BOOT-INF/classes/`. These are served by Boot's `LaunchedURLClassLoader` using a custom `jar:` URL scheme (nested `jar:file:app.jar!/BOOT-INF/lib/dep.jar!/...`). Consequences: - **`Resource.getFile()` throws** — there is no real `java.io.File` for a nested-JAR entry. Code that scans then calls `getFile()` works in the IDE (exploded classes on disk) and breaks in production. - **Anchored `classpath*:` patterns work**, but only through URL/stream access; Boot's loader supports enumerating nested JARs for `getResources(rootDir)`. - Deeply recursive `**` across many nested JARs can be slow and, in older Boot/loader versions, had edge-case misses. ## Pitfall 3 — exploded vs packaged divergence Running from an IDE puts classes and resources as **plain files** on the filesystem, so almost everything — including `getFile()` and sloppy root wildcards — appears to work. The same code fails once packaged. **Rule: integration-test the built artifact** (the actual JAR/WAR), not just the exploded classpath. ## Pitfall 4 — ordering and determinism The returned `Resource[]` follows classloader/classpath order, which varies by build tool, OS, and container. Never use array position to decide precedence (e.g. "last one wins"); if you need deterministic override semantics, sort by a stable key (filename, an embedded order property) yourself. ## Pitfall 5 — performance `classpath*:` opens and walks **every** JAR on the classpath — potentially hundreds in a large app. It's fine at startup but must never sit on a request hot path. Scan once, cache the resolved `Resource[]` (or the parsed content). ## Pitfall 6 — encoding of special characters Resource URLs can contain URL-encoded characters (spaces -> `%20`) when the deployment path has spaces. Prefer `getURI()` and stream access over hand-parsing `getURL().getPath()`. ## Practices I standardize - Anchor every `classpath*:` under a concrete package segment. - Read via `getInputStream()`; treat `getFile()` as unavailable. - Cache scan results; never scan per-request. - Sort results explicitly when precedence matters. - Add a test that runs against the packaged JAR to catch nested-JAR regressions. - Prefer explicit registration (`spring.factories`, `@Import`, an index) over broad filesystem-style scanning when the module set is known — it's faster and deployment-agnostic. ## When to avoid scanning entirely If you control all the modules, an explicit descriptor (Spring's `AutoConfiguration.imports` / `spring.factories`, or a generated index) beats wildcard scanning: it's O(1), order-controlled, and immune to classloader quirks. Reserve `classpath*:` for genuine open-ended plugin discovery.
- Why does resource scanning that works in the IDE break in a Spring Boot fat JAR?The IDE runs from exploded classes/resources on the filesystem, so getFile() and even loose root wildcards work. The fat JAR nests dependency JARs and serves them via LaunchedURLClassLoader; there's no java.io.File for nested entries, so getFile() throws and code must use getInputStream()/getURI().
- When would you avoid classpath*: scanning altogether?When the module set is known and closed. An explicit descriptor (spring.factories / AutoConfiguration.imports, @Import, or a build-time index) is O(1), order-controlled, and immune to classloader quirks. Reserve wildcard scanning for genuinely open-ended plugin discovery.
- Why is relying on Resource[] order dangerous for override semantics?The order reflects classloader/classpath ordering, which varies by build tool, OS, and packaging. For deterministic 'later overrides earlier' behavior you must sort by a stable key yourself rather than trusting array position.
saying these in an interview costs you the question
- Calling getFile() on scanned resources — breaks inside JARs / Boot fat JARs.
- Using a bare-root wildcard like classpath*:*.xml and assuming it finds files in every environment.
- Trusting Resource[] ordering for precedence.
- Validating only in the IDE and never against the packaged artifact.
- Running classpath*: scans on a request hot path instead of caching.