skip to content

Explain the wildcard tokens in Spring path patterns: ?, *, and **. How do they differ and where can each appear?

level: seniorimportance: should knowfreq 50%

answer

  1. ? = one char; * = one segment; ** = many segments
  2. never crosses a slash
  3. PathPattern: ** only at the end, once
  4. {*name} captures the whole tail (leading slash included)
  5. Most-specific pattern wins: literal > {var} > * > **

basics

~20 s

? matches exactly one character within a segment. * matches zero or more characters within a single path segment (no slash). ** matches zero or more whole segments and, under PathPattern, must be the last element. You can capture ** with {*name}.

solid answer

~40 s

Spring path patterns use three wildcards. ? matches a single character inside one segment. * (single asterisk) matches any number of characters but stays within one path segment — it never crosses a /. ** (double asterisk) matches across multiple segments, i.e. the rest of the path. With the modern PathPatternParser, ** may appear only once and only at the very end of the pattern; AntPathMatcher was looser. To capture what ** consumes as a variable, PathPattern offers the {*name} syntax — @GetMapping("/files/{*path}") binds the entire remaining path (leading slash included) to a String. Wildcards drive Spring's most-specific-wins ordering: an exact match beats {var}, which beats *, which beats **. Typical uses: /assets/** for static resources, /api/*/health for a fixed-shape multi-service route, and {*path} for catch-all forwarding or SPA fallback controllers.

code

java · 15 lines
java
@RestController
public class RoutingExamples {

    @GetMapping("/api/*/health")            // exactly one variable segment
    public String health() { return "UP"; }

    @GetMapping("/pages/page?.html")        // single-character wildcard
    public String page() { ... }

    // Capturing catch-all: relativePath = "/img/logo.png" for /files/img/logo.png
    @GetMapping("/files/{*relativePath}")
    public Resource serve(@PathVariable String relativePath) {
        return storage.load(relativePath);
    }
}

go deeper

for a junior

Aware * and ** exist; may confuse their scope.

for a middle

Knows *=one segment, **=many, and basic precedence.

for a senior

Explains {name} capture, PathPattern's trailing-* rule, and specificity ordering with real routing examples.

for a principal

Considers static-resource shadowing, path-traversal security on catch-alls, and why PathPattern's constraints improve performance/determinism.

## The three wildcard tokens Spring MVC path patterns (whether parsed by `PathPatternParser` or the legacy `AntPathMatcher`) support three wildcard characters, each with a distinct scope. ### `?` — single character `/pages/page?.html` matches `/pages/page1.html`, `/pages/pageA.html`, but not `/pages/page.html` (needs exactly one char) nor `/pages/page12.html` (only one char). Scope: within a segment. ### `*` — any characters, one segment A single asterisk matches zero or more characters **but never crosses the `/` delimiter**. `/files/*.log` matches `/files/app.log` but not `/files/2024/app.log`. `/api/*/health` matches `/api/orders/health` and `/api/users/health` — exactly one variable segment. ### `**` — any number of segments Double asterisk matches zero or more whole path segments, effectively "the rest of the path." `/resources/**` matches `/resources/`, `/resources/css/app.css`, `/resources/a/b/c.png`. **PathPattern restriction**: with `PathPatternParser`, `**` may appear **only once, and only as the final segment** of the pattern. Patterns like `/a/**/b` are rejected. The legacy `AntPathMatcher` allowed `**` in the middle, which is one reason PathPattern is faster and less ambiguous. ## Capturing the tail: `{*name}` `PathPattern` adds a capturing multi-segment variable: `{*name}` captures everything `**` would match, and binds it to a `@PathVariable`. ```java @GetMapping("/files/{*relativePath}") public Resource serve(@PathVariable String relativePath) { ... } ``` For `/files/img/logo.png`, `relativePath` = `/img/logo.png` (note the leading slash is included). This is the idiomatic catch-all — for static-resource serving, SPA fallback routes, or proxy forwarding. The legacy matcher had no direct capturing equivalent. ## Precedence — most specific wins When several patterns match a URL, Spring orders them from most to least specific and picks the winner. Roughly: an **exact literal** beats a **single `{var}`**, which beats **`*`**, which beats **`**`**. `PathPattern` computes a specificity/score for each parsed pattern; `AntPathMatcher` uses `AntPatternComparator`. So `/api/users/health` (literal) wins over `/api/*/health` wins over `/api/**`. ## Where each may appear - `?` and `*`: anywhere inside a segment, any number of times. - `**` / `{*name}`: PathPattern → only as the trailing element; AntPathMatcher → anywhere. ## Gotchas - **`*` doesn't cross slashes** — a very common misconception. Use `**` for multi-segment matching. - **Static resources**: Spring Boot maps `/**` for static content by default; a greedy custom `/**` controller can accidentally shadow it — order and specificity matter. - **Security**: catch-all `{*path}` routes that serve files must guard against path traversal (`../`); Spring's `ResourceHttpRequestHandler` normalizes and blocks this, but hand-rolled file serving must validate. - **Suffix pattern matching** (`.*` extension matching) is disabled by default in modern Spring for security reasons — don't rely on `/foo.*` semantics.

  • Does /files/* match /files/2024/report.pdf?
    No. A single * stays within one segment and won't cross the slash before 2024. You'd need /files/** or /files/{*path} to match the multi-segment tail.
  • Why does PathPatternParser forbid ** anywhere but the end?
    Pre-parsing patterns into a linked structure with a trailing multi-segment matcher makes matching linear and unambiguous; mid-pattern ** would require backtracking, hurting performance and determinism. AntPathMatcher allowed it but re-parsed on every request.

saying these in an interview costs you the question

  • Saying * matches across multiple path segments (it doesn't — that's **)
  • Believing ** can appear in the middle of a pattern under PathPattern
  • Not knowing {*name} exists for capturing the tail
  • Thinking wildcard order is declaration-order rather than specificity-based

context