Spring Annotations: @RequestMapping and its Variants

Introduction

If you've read anything about Spring, developed a project, or was even remotely interested in how it works, you've been introduced to the @RequestMapping annotation.

It's one of the basic annotations in Spring which maps HTTP requests (URLs) with methods:

@RequestMapping("/")
public void helloWorld() {
    return "Hello World!";
}

It allows us to run methods and pieces of code each time an endpoint is hit by the end-user with an HTTP request. In this case, it's a simple root mapping that returns the String "Hello World!" when the root endpoint is hit.

The @RequestMapping Annotation

The @RequestMapping annotation itself offers more than shown in the previous example. Let's briefly cover some of the basic use cases of the annotation.

@RequestMapping Scopes

The mapping can be assigned at a class level or a method level:

@RequestMapping("/admin")
public class AdminController() {
    @RequestMapping("/adminPanel")
    public String adminPanel() {
        return "adminPanel.html";
    }

    @RequestMapping("/statistics")
    public String statistics() {
        return "statistics.html";
    }
}

Here, the /admin mapping is assigned on the class level, whereas the /adminPanel and /statistics mappings are assigned to methods. The class-level mapping will be used as a prefix for all of these method-level mappings: /admin/adminPanel and /admin/statistics.

This way, we can segregate our application into logical parts. For an example, all administrator related logic will be behind the /admin wall, which only administrator users can enter. All non-administrator users will be redirected to the client side of the application.

Multiple Mappings per Method

If need be, we can assign multiple mappings to a single method, such as:

@RequestMapping("/", "/index", "/home")
public void helloWorld() {
    return "Hello World!";
}

Hitting any of these endpoints will result in the helloWorld() method handling our request.

Request Methods

The most common attribute used is the method attribute which allows us to specify and narrow down the request method that our method handles (GET, POST, DELETE, etc.) :

@RequestMapping(value = "/addProduct", method = RequestMethod.POST)
public String addProductPost(@ModelAttribute("product") Product product) {
    // some code
}

If we don't specify a request method, the default request method is GET.

Request Headers

We can further specify the mapping by defining one or more headers. In this case, the helloWorld() method will handle the request if its content-type is text/plain or text/html:

@RequestMapping(value = "/header", headers = {"content-type=text/plain", "content-type=text/html"})
String helloWorld() {
    return "Hello World!";
}

Path Variables

A lot of websites rely on path variables to showcase a specific product, page, profile, etc. to the end-user, such as example.com/viewProduct/324, where 324 is the product ID:

@RequestMapping("/viewProduct/{productId}")
public String viewProduct(@PathVariable int productId, Model model) {
    Product product = productService.getProductById(productId);
    model.addAttribute("product", product);

    return "viewProduct";
}

Here, after the mapping, we encase the path variable with curly brackets ({}) and assign that value to our productId. The @PathVariable annotation takes care of this for us. Then, we can find the product via our ProductService and display it to the end-user.

Of course, the same approach can be used for other variables such as:

@RequestMapping("/users/{username}")
public String viewUser(@PathVariable("username") String username) {
    Profile profile = profileService.getProfileByUsername(username);
    // rest of the code
}

Request Parameters

Very similar to path variables, many applications rely on request parameters to alter the state of the page. We can use the @RequestParam annotation to bind a request parameter to a variable:

@RequestMapping("/login")
public String login(@RequestParam(value = "error", required = false) String error, @RequestParam(value = "logout", required = false) String logout, Model model) {

    if (error != null) {
        model.addAttribute("error", "Wrong username or password!");
    }
    if (logout != null) {
        model.addAttribute("msg", "You have successfully logged out!");
    }
    return "login";
}

Producible and Consumable

Using the @RequestMapping annotation, we can also produce or consume media types. To produce a media type such as "JSON", you can use the produces attribute alongside the @ResponseBody annotation, whereas to consume a media type you can use the consumes attribute:

@RequestMapping("/annotations")
public class SomeController {
    @RequestMapping(value = "/produce", produces = {"application/json"})
    @ResponseBody
    String produces() {
        return "Here's how you use the 'produces' attribute";
    }

    @RequestMapping(value = "/consume", consumes = {"application/JSON", "application/XML"})
    String consume() {
        return "Here's how you use a 'consume' attribute";
    }
}

Composed @RequestMapping Variants

A single mapping can correspond to different methods if we ensure there's no ambiguity as to which method should handle which version of the mapping.

We can have multiple methods with the same mapping if their request method is different:

@RequestMapping("/hello")
public String hello() {
    return "hello";
}

@RequestMapping(value = "/hello", method = RequestMethod.POST)
public String helloPost() {
    return "hello post";
}

@RequestMapping(value = "/hello", method = RequestMethod.PUT)
public String helloGet() {
    return "hello put";
}

@RequestMapping(value = "/hello", method = RequestMethod.DELETE)
public String helloDelete() {
    return "hello delete";
}

This same method has 4 different variations:

  • Default (GET)
  • POST
  • PUT
  • DELETE

With more variations, the readability of code gets reduced, and this quite honestly is already verbose for a simple mapping. Request mappings in bigger applications can get very complex and convoluted, and we'd want to keep it simpler and more readable.

For this purpose, we're introduced to several new annotations, the @RequestMapping variants:

  • @GetMapping
  • @PostMapping
  • @DeleteMapping
  • @PutMapping
  • @PatchMapping

There are quite literally just shortcut versions for the mappings above where @PostMapping("/hello") is equal to @RequestMapping(value="/hello", RequestMethod.POST).

Taking a closer look at how these annotations are defined, we can get a clear idea of what they do:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {

    @AliasFor(annotation = RequestMapping.class)
    String name() default "";

    @AliasFor(annotation = RequestMapping.class)
    String[] value() default {};

    @AliasFor(annotation = RequestMapping.class)
    String[] path() default {};

    @AliasFor(annotation = RequestMapping.class)
    String[] params() default {};

    @AliasFor(annotation = RequestMapping.class)
    String[] headers() default {};

    @AliasFor(annotation = RequestMapping.class)
    String[] produces() default {};
}

That being said, we can rewrite the examples above:

@GetMapping
public String hello() {
    return "hello";
}

@PostMapping
public String helloPost() {
    return "hello post";
}

@PutMapping
public String helloGet() {
    return "hello put";
}

@DeleteMapping
public String helloDelete() {
    return "hello delete";
}

As this is both a simpler, more readable and newer way of writing annotations, it's generally preferred to write them like this. If need be, you can still define any other attributes such as the value, params, path, etc.

Conclusion

@RequestMapping is a quintessential annotation in the Spring framework which allows us to map HTTP requests with methods we'd wish to run.

As of Spring 4.3, it's preferred and advised to use the shortcuts made for this annotation for a more readable and less verbose codebase.