Close

Spring MVC - URI Patterns

[Last Updated: Mar 9, 2026]

In this tutorial we will learn how many different kind of URI patterns can be used with @RequestMapping#value.

AntPathMatcher

In versions before Spring 6, the default component for pattern matching was AntPathMatcher which follows these rule:

  • ? matches one character
  • * matches zero or more characters
  • ** matches zero or more directories in a path

Other than above, regex pattern can also be used with template variables.

PathPatternParser

The PathPatternParser has been available for use in Spring MVC from version 5.3, and is enabled by default from version 6.0.

  • Old Way (AntPathMatcher): Tried to match the entire URL string at once. It struggled with encoded characters and was prone to errors.
  • New Way: (PathPatternParser) It breaks the path into segments first. It looks at the path piece-by-piece. This allows Spring to decode and clean each segment individually without breaking the overall structure of the URL via PathPatternParser.

PathPatternParser follows these rules:

  • ? matches one character.
  • * matches zero or more characters within a path segment.
  • ** matches zero or more path segments until the end of the path.
  • {spring} matches a path segment and captures it as a variable named "spring".
  • {spring:[a-z]+} matches a regexp against a path segment and captures it as a variable.
  • {*spring} matches zero or more path segments until the end and captures it as a variable.
  • Note: and {*spring} are only supported at the end of a pattern to eliminate ambiguity.

In contrast to AntPathMatcher, ** is supported only at the end of a pattern. For example /pages/** is valid but /pages/**/details is not. The same applies also to the capturing variant {*spring}. The aim is to eliminate ambiguity when comparing patterns for specificity.

Let's go through examples to understand how these rules work.

Examples

In following example we are using @ResponseBody which causes the object returned by a handler method to serialize directly into the HTTP response body rather than being interpreted as a view name to be rendered.

To test examples, please run embedded Jetty server setup in pom.xml

mvn jetty:run

Pattern with ?

Each ? matches a single character:

@Controller
@ResponseBody
public class MyController {

    @RequestMapping("/car?/s?o?/info")
    public String test1(HttpServletRequest request) {
        return "from test1(), request uri: " + request.getRequestURI();
    }
    .............
}

Above handler will map to cars/shop/info, cart/show/info etc:

$ curl http://localhost:8080/cars/shop/info
from test1(), request uri: /cars/shop/info
$ curl http://localhost:8080/cart/show/info
from test1(), request uri: /cart/show/info

Pattern with *

Each * matches zero or more characters but within a single path segment (path segments are separated by /):

@Controller
@ResponseBody
public class MyController {
    .............
    @RequestMapping("/c*/s*d/info")
    public String test2(HttpServletRequest request) {
        return "from test2(), request uri: " + request.getRequestURI();
    }
    .............
}

Above handler will map to cars/speed/info, cabbie/signalized/info etc:

$ curl http://localhost:8080/cars/speed/info
from test2(), request uri: /cars/speed/info
$ curl http://localhost:8080/cabbie/signalized/info
from test2(), request uri: /cabbie/signalized/info

Pattern with **

Double wildcards can match anything including forward slashes. They are used to match directories in path.

@Controller
@ResponseBody
public class MyController {
    .............
    @RequestMapping("/card/**")
    public String test3(HttpServletRequest request) {
        return "from test3(), request uri: " + request.getRequestURI();
    }
    .............
}

Above handler will map to /card, /card/about, /card/visa/registration etc:

$ curl http://localhost:8080/card
from test3(), request uri: /card
$ curl http://localhost:8080/card/about
from test3(), request uri: /card/about
$ curl http://localhost:8080/card/visa/registration
from test3(), request uri: /card/visa/registration

Path variables

Spring supports URI template capturing via @PathVariable annotation. A template can have a regex pattern as well.

@Controller
@ResponseBody
public class MyController {
    .............
    @RequestMapping("/card/{type}/{id:i.\\d+.*}")
    public String test4(@PathVariable String type,
                        @PathVariable String id,
                        HttpServletRequest request) {
        return "from test4(), request uri: " + request.getRequestURI() + "\n" +
                "type: " + type + ", id: " + id;
    }
}
$ curl http://localhost:8080/card/visa/i2345d
from test4(), request uri: /card/visa/i2345d
type: visa, id: i2345d
$ curl http://localhost:8080/card/master/i3234
from test4(), request uri: /card/master/i3234
type: master, id: i3234

Selection between multiple matches

When multiple patterns match a request URI, they must be compared to find the best match. This done by finding the most specific match. A score is calculated to find the most specific match. The lowest score wins the comparison. Following rules are applied:

  • A single wildcard (*) is counted as one.
  • A double wildcard (**) is counted as two.
  • URI path without any pattern (e.g. /cars/dealer) has zero count. Also a single '?' is counted as zero (e.g. /car?/d??r has total zero count).
  • If one of the handlers has just "/**" (i.e. @RequestMapping("/**")) then it is always matched at end. Also patterns like /employee/** (prefix pattern, the one ending with /**) are matched after the patterns which don't have double wildcards.
  • One template variable within a pair of curly braces (e.g: /card/{type}) is counted as one.
  • Regex with * inside template variable (e.g. /card/{id:n.*} is also counted as one. Whereas, other regex quantifiers (+ or ?) are not counted at all. (This is not true for new PathPatternParser, Spring 6+ default behavior. where /card/{id:n.*} and /card/{id:n.+} will cause ambagious handler error).
  • If multiple patterns have equal score, the longer pattern is chosen. Here length is calculated via String#length(). In case of Template variables, content between { and } are reduced to a single # (e.g. /card/{var1}/{var2} to /card/#/#) before calculating length.
  • At this point, if two patterns have same length, then the pattern with less number of wildcards (*) is selected.
  • At this point, if two patterns have same number of wildcards(*), then the pattern with less number of template variables are selected.

Above logic is implemented in AntPathStringMatcher#compare method (this class is nested class of AntPathMatcher).

Let's see more examples:

package com.logicbig.example;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import jakarta.servlet.http.HttpServletRequest;

@Controller
@ResponseBody
public class MyController {

    @RequestMapping("/car?/s?o?/info")//score 0
    public String test1(HttpServletRequest request) {
        return "from test1(), request uri: " + request.getRequestURI();
    }

    @RequestMapping("/c*/s*d/info")//score 2, length = 12
    public String test2(HttpServletRequest request) {
        return "from test2(), request uri: " + request.getRequestURI();
    }

    @RequestMapping("/card/**")
    //score 2 but will be used after others because of prefix pattern
    public String test3(HttpServletRequest request) {
        return "from test3(), request uri: " + request.getRequestURI();
    }

    //2 template variables = score 2
    @RequestMapping("/card/{type}/{id:i.\\d+.*}")
    public String test4(@PathVariable String type,
                        @PathVariable String id,
                        HttpServletRequest request) {
        return "from test4(), request uri: " + request.getRequestURI() + "\n" +
                "type: " + type + ", id: " + id;
    }
}

Request URI: 'card/shod/info':

This URI matches all handlers methods, but test1() will be selected because it has the lowest score:

$ curl http://localhost:8080/care/shod/info
from test1(), request uri: /care/shod/info

Request URI 'card/send/info':

It matches all handlers except for test1().

test2() and test5() have the lowest count (2), but test2() will be selected because it is longer in length.

$ curl http://localhost:8080/card/send/info
from test2(), request uri: /card/send/info

Let's see other controller:

package com.logicbig.example;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import jakarta.servlet.http.HttpServletRequest;

@Controller
@RequestMapping("/other/")
@ResponseBody
public class MyController2 {

    @RequestMapping("/c*/s*d/info")//score 2, length = 12, wildcards=2
    public String otherTest1(HttpServletRequest request) {
        return "from otherTest1(), request uri: " + request.getRequestURI();
    }

    @RequestMapping("/card/{type}/info")//score 1, length 12 (/card/#/info), wildcards=0
    public String otherTest2(@PathVariable String type, HttpServletRequest request) {
        return "from otherTest2(), request uri: " + request.getRequestURI() + "\n" +
                "type: " + type;
    }
}

In this case the request uri /other/card/send/info matches both methods but the second method will be selected because it has the lowest score:

$ curl http://localhost:8080/other/card/send/info
from otherTest2(), request uri: /other/card/send/info
type: send

Here is another controller:

package com.logicbig.example;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import jakarta.servlet.http.HttpServletRequest;

@Controller
@RequestMapping("/another/")
@ResponseBody
public class MyController3 {

    @RequestMapping("/c*/s*d/info")//score 2, length = 12, wildcards=2
    public String anotherTest1(HttpServletRequest request) {
        return "from anotherTest1(), request uri: " + request.getRequestURI();
    }

    @RequestMapping("/card/{type}/inf{id:.+}")//score 2, length 12 (/card/#/info), wildcard = 0
    public String anotherTest2(@PathVariable String type, @PathVariable String id,
                        HttpServletRequest request) {
        return "from anotherTest2(), request uri: " + request.getRequestURI()+"\n"+
                "type: "+type+", id: "+id;
    }
}

In this case the request uri /another/card/send/info matches both methods. Both methods have same scores and same length but last method will be selected because it has the less number of wildcards (*):

$ curl http://localhost:8080/another/card/send/info
from anotherTest2(), request uri: /another/card/send/info
type: send, id: o

Also check out following tutorials, where URI path pattern is involved:

Example Project

Dependencies and Technologies Used:

  • spring-webmvc 7.0.5 (Spring Web MVC)
     Version Compatibility: 3.2.9.RELEASE - 7.0.5Version List
    ×

    Version compatibilities of spring-webmvc with this example:

      javax.servlet-api:3.x
    • 3.2.9.RELEASE
    • 3.2.10.RELEASE
    • 3.2.11.RELEASE
    • 3.2.12.RELEASE
    • 3.2.13.RELEASE
    • 3.2.14.RELEASE
    • 3.2.15.RELEASE
    • 3.2.16.RELEASE
    • 3.2.17.RELEASE
    • 3.2.18.RELEASE
    • 4.0.0.RELEASE
    • 4.0.1.RELEASE
    • 4.0.2.RELEASE
    • 4.0.3.RELEASE
    • 4.0.4.RELEASE
    • 4.0.5.RELEASE
    • 4.0.6.RELEASE
    • 4.0.7.RELEASE
    • 4.0.8.RELEASE
    • 4.0.9.RELEASE
    • 4.1.0.RELEASE
    • 4.1.1.RELEASE
    • 4.1.2.RELEASE
    • 4.1.3.RELEASE
    • 4.1.4.RELEASE
    • 4.1.5.RELEASE
    • 4.1.6.RELEASE
    • 4.1.7.RELEASE
    • 4.1.8.RELEASE
    • 4.1.9.RELEASE
    • 4.2.0.RELEASE
    • 4.2.1.RELEASE
    • 4.2.2.RELEASE
    • 4.2.3.RELEASE
    • 4.2.4.RELEASE
    • 4.2.5.RELEASE
    • 4.2.6.RELEASE
    • 4.2.7.RELEASE
    • 4.2.8.RELEASE
    • 4.2.9.RELEASE
    • 4.3.0.RELEASE
    • 4.3.1.RELEASE
    • 4.3.2.RELEASE
    • 4.3.3.RELEASE
    • 4.3.4.RELEASE
    • 4.3.5.RELEASE
    • 4.3.6.RELEASE
    • 4.3.7.RELEASE
    • 4.3.8.RELEASE
    • 4.3.9.RELEASE
    • 4.3.10.RELEASE
    • 4.3.11.RELEASE
    • 4.3.12.RELEASE
    • 4.3.13.RELEASE
    • 4.3.14.RELEASE
    • 4.3.15.RELEASE
    • 4.3.16.RELEASE
    • 4.3.17.RELEASE
    • 4.3.18.RELEASE
    • 4.3.19.RELEASE
    • 4.3.20.RELEASE
    • 4.3.21.RELEASE
    • 4.3.22.RELEASE
    • 4.3.23.RELEASE
    • 4.3.24.RELEASE
    • 4.3.25.RELEASE
    • 4.3.26.RELEASE
    • 4.3.27.RELEASE
    • 4.3.28.RELEASE
    • 4.3.29.RELEASE
    • 4.3.30.RELEASE
    • 5.0.0.RELEASE
    • 5.0.1.RELEASE
    • 5.0.2.RELEASE
    • 5.0.3.RELEASE
    • 5.0.4.RELEASE
    • 5.0.5.RELEASE
    • 5.0.6.RELEASE
    • 5.0.7.RELEASE
    • 5.0.8.RELEASE
    • 5.0.9.RELEASE
    • 5.0.10.RELEASE
    • 5.0.11.RELEASE
    • 5.0.12.RELEASE
    • 5.0.13.RELEASE
    • 5.0.14.RELEASE
    • 5.0.15.RELEASE
    • 5.0.16.RELEASE
    • 5.0.17.RELEASE
    • 5.0.18.RELEASE
    • 5.0.19.RELEASE
    • 5.0.20.RELEASE
    • 5.1.0.RELEASE
    • 5.1.1.RELEASE
    • 5.1.2.RELEASE
    • 5.1.3.RELEASE
    • 5.1.4.RELEASE
    • 5.1.5.RELEASE
    • 5.1.6.RELEASE
    • 5.1.7.RELEASE
    • 5.1.8.RELEASE
    • 5.1.9.RELEASE
    • 5.1.10.RELEASE
    • 5.1.11.RELEASE
    • 5.1.12.RELEASE
    • 5.1.13.RELEASE
    • 5.1.14.RELEASE
    • 5.1.15.RELEASE
    • 5.1.16.RELEASE
    • 5.1.17.RELEASE
    • 5.1.18.RELEASE
    • 5.1.19.RELEASE
    • 5.1.20.RELEASE
    • 5.2.0.RELEASE
    • 5.2.1.RELEASE
    • 5.2.2.RELEASE
    • 5.2.3.RELEASE
    • 5.2.4.RELEASE
    • 5.2.5.RELEASE
    • 5.2.6.RELEASE
    • 5.2.7.RELEASE
    • 5.2.8.RELEASE
    • 5.2.9.RELEASE
    • 5.2.10.RELEASE
    • 5.2.11.RELEASE
    • 5.2.12.RELEASE
    • 5.2.13.RELEASE
    • 5.2.14.RELEASE
    • 5.2.15.RELEASE
    • 5.2.16.RELEASE
    • 5.2.17.RELEASE
    • 5.2.18.RELEASE
    • 5.2.19.RELEASE
    • 5.2.20.RELEASE
    • 5.2.21.RELEASE
    • 5.2.22.RELEASE
    • 5.2.23.RELEASE
    • 5.2.24.RELEASE
    • 5.2.25.RELEASE
    • 5.3.0
    • 5.3.1
    • 5.3.2
    • 5.3.3
    • 5.3.4
    • javax.servlet-api:4.x
    • 5.3.5
    • 5.3.6
    • 5.3.7
    • 5.3.8
    • 5.3.9
    • 5.3.10
    • 5.3.11
    • 5.3.12
    • 5.3.13
    • 5.3.14
    • 5.3.15
    • 5.3.16
    • 5.3.17
    • 5.3.18
    • 5.3.19
    • 5.3.20
    • 5.3.21
    • 5.3.22
    • 5.3.23
    • 5.3.24
    • 5.3.25
    • 5.3.26
    • 5.3.27
    • 5.3.28
    • 5.3.29
    • 5.3.30
    • 5.3.31
    • 5.3.32
    • 5.3.33
    • 5.3.34
    • 5.3.35
    • 5.3.36
    • 5.3.37
    • 5.3.38
    • 5.3.39
    • javax.* -> jakarta.*
      jakarta.servlet-api:6.x
      Java 17 min
    • 6.0.0
    • 6.0.1
    • 6.0.2
    • 6.0.3
    • 6.0.4
    • 6.0.5
    • 6.0.6
    • 6.0.7
    • 6.0.8
    • 6.0.9
    • 6.0.10
    • 6.0.11
    • 6.0.12
    • 6.0.13
    • 6.0.14
    • 6.0.15
    • 6.0.16
    • 6.0.17
    • 6.0.18
    • 6.0.19
    • 6.0.20
    • 6.0.21
    • 6.0.22
    • 6.0.23
    • 6.1.0
    • 6.1.1
    • 6.1.2
    • 6.1.3
    • 6.1.4
    • 6.1.5
    • 6.1.6
    • 6.1.7
    • 6.1.8
    • 6.1.9
    • 6.1.10
    • 6.1.11
    • 6.1.12
    • 6.1.13
    • 6.1.14
    • 6.1.15
    • 6.1.16
    • 6.1.17
    • 6.1.18
    • 6.1.19
    • 6.1.20
    • 6.1.21
    • 6.2.0
    • 6.2.1
    • 6.2.2
    • 6.2.3
    • 6.2.4
    • 6.2.5
    • 6.2.6
    • 6.2.7
    • 6.2.8
    • 6.2.9
    • 6.2.10
    • 6.2.11
    • 6.2.12
    • 6.2.13
    • 6.2.14
    • 6.2.15
    • 6.2.16
    • 7.0.0
    • 7.0.1
    • 7.0.2
    • 7.0.3
    • 7.0.4
    • 7.0.5

    Versions in green have been tested.

  • jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
  • JDK 25
  • Maven 3.9.11

Spring MVC - URI Patterns Select All Download
  • spring-uri-pattern-example
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • MyController.java

    See Also

    Join