Skip to content
Chapter 05 · Spring Boot

Controller Annotations: @RestController, @RequestMapping and Request Data ​

Chapter 3 built a controller without explaining its annotations. This chapter explains them, one step at a time, by building a UserController that serves /api/fetchUser and /api/saveUser.

Recall the request flow from chapter 2. Every request reaches the DispatcherServlet, which uses handler mapping to choose a controller method:

The annotations in this chapter are the input to that mapping: which classes can handle requests (step 1), which URL goes to which method (step 2), and how parts of the request become method parameters (steps 3 to 6). Step 7 is the way back. How Java reads annotations at run time (reflection) is the Core Java chapter on annotations; here we use them.

Step 1: mark the class as a controller ​

java
@RestController
public class UserController {
}

@Controller and @RestController both tell Spring: this class handles HTTP requests. When Spring builds the handler mapping, it only looks at classes with one of these annotations. A method in any other class is never called for a request, whatever annotations it has.

The difference between the two is one annotation:

text
@RestController = @Controller + @ResponseBody

@ResponseBody says: the method's return value is the HTTP response body. Without it, Spring treats a returned String as the name of a view (a page template such as hello.jsp or hello.html) and tries to render that page.

java
@Controller
@RequestMapping("/view")
public class ViewController {

    @GetMapping("/fetchUser")
    public String noBody() {
        return "fetching user details";      // looked up as a VIEW named "fetching user details"
    }

    @GetMapping("/withBody")
    @ResponseBody
    public String withBody() {
        return "fetching user details";      // sent as the response body
    }
}
RequestResponse
GET /view/fetchUser404 Not Found: no view with that name exists
GET /view/withBodyfetching user details

A REST API never renders pages, so @ResponseBody would be needed on every method. @RestController puts it on all of them at once, which is why REST controllers almost always use @RestController.

Step 2: map URLs to methods ​

@RequestMapping ​

java
@RestController
public class UserController {

    @RequestMapping(path = "/api/fetchUser", method = RequestMethod.GET)
    public String getUserDetails() {
        return "fetched user details";
    }
}

@RequestMapping maps a request to this method. Its main attributes:

AttributeMeaning
path (alias value)the URL. path and value are aliases: use either.
methodthe HTTP method: RequestMethod.GET, POST, PUT, PATCH, DELETE
consumes / producesthe content types accepted and returned, e.g. "application/json"

At startup Spring scans the controllers, reads these annotations, and builds the mapping table: GET /api/fetchUser → UserController.getUserDetails. (@RequestMapping is itself annotated with @Mapping and @Reflective; that is how Spring's processing finds it.)

Shortcuts: @GetMapping and friends ​

java
@GetMapping("/api/fetchUser")      // = @RequestMapping(path = "/api/fetchUser", method = RequestMethod.GET)

@GetMapping, @PostMapping, @PutMapping, @PatchMapping and @DeleteMapping are @RequestMapping with the method already filled in. They read better, so they are what you normally write.

A common prefix on the class ​

When all the endpoints of a controller start the same way, put the shared part on the class:

java
@RestController
@RequestMapping("/api")                // prefix for every method below
public class UserController {

    @GetMapping("/fetchUser")          // GET  /api/fetchUser
    public String getUserDetails() { ... }

    @PostMapping("/saveUser")          // POST /api/saveUser
    public String saveUser() { ... }
}

Spring joins the two: class path + method path.

Step 3: read query parameters with @RequestParam ​

Query parameters come after ?, separated by &:

text
http://localhost:8080/api/fetchUser?firstName=John&lastName=Smith&age=32
                                    └──────────── query parameters ────────────┘

@RequestParam binds each one to a method parameter:

java
@GetMapping("/fetchUser")
public String getUserDetails(@RequestParam(name = "firstName") String firstName,
                             @RequestParam(name = "lastName", required = false) String lastName,
                             @RequestParam(name = "age", required = false) Integer age) {
    return "fetching user details of " + firstName + " " + lastName + ", age " + age;
}

name (alias value) is the parameter's name in the URL. required decides what happens when it is missing:

RequestResponse
?firstName=John&lastName=Smith&age=32fetching user details of John Smith, age 32
?firstName=Johnfetching user details of John null, age null
?lastName=Smith400 Bad Request
  • required is true by default. When a required parameter is missing, the URL still matches this method, but Spring refuses to call it and answers 400 Bad Request (the log shows MissingServletRequestParameterException: Required request parameter 'firstName' ... is not present).
  • With required = false, a missing parameter is null. That is why age is an Integer here: a primitive int cannot be null. (Or give it a default: @RequestParam(defaultValue = "0") int age.)

Type conversion. Everything in a URL is text. Spring converts it to the parameter's type: primitives, their wrappers, String, enums. If the text cannot be converted (age=xx into an Integer), the answer is again 400 Bad Request (MethodArgumentTypeMismatchException).

Step 4: custom conversion with @InitBinder ​

Users type names any way they like: john, JOHN, John. Say every firstName should arrive trimmed and in lower case, before your method sees it. Write a property editor, which turns the request's text into the value:

java
class FirstNamePropertyEditor extends PropertyEditorSupport {

    @Override
    public void setAsText(String text) {           // text = the raw value from the request
        setValue(text.trim().toLowerCase());       // the value the method will receive
    }
}

Register it in an @InitBinder method of the controller:

java
@RestController
@RequestMapping("/api")
public class UserController {

    @InitBinder
    protected void initBinder(DataBinder binder) {
        //                           type of the value, name of the parameter, editor
        binder.registerCustomEditor(String.class, "firstName", new FirstNamePropertyEditor());
    }

    @GetMapping("/fetchUser")
    public String getUserDetails(@RequestParam(name = "firstName") String firstName) {
        return "fetching user details of " + firstName;
    }
}

Before each handler method of this controller runs, Spring calls the @InitBinder method and uses the registered editor for every String parameter named firstName. GET /api/fetchUser?firstName=%20%20JOHN%20 (that is " JOHN ") now returns fetching user details of john.

The same mechanism converts text into your own types, such as a date string into a date object.

Step 5: read path variables with @PathVariable ​

Sometimes the value is part of the path, not a query parameter:

text
http://localhost:8080/api/fetchUser/John

Mark the variable part with {...} in the mapping, and extract it with @PathVariable:

java
@GetMapping("/fetchUser/{firstName}")
public String getUserByPath(@PathVariable("firstName") String firstName) {
    return "fetching user details of " + firstName;
}

GET /api/fetchUser/John puts John into firstName. (In this controller, the @InitBinder editor from step 4 applies here too, because the parameter is also a String named firstName: the result is john.)

@RequestParam@PathVariable
URL/fetchUser?firstName=John/fetchUser/John
Typical useoptional filters, search optionsidentifying a resource (/users/42)

Step 6: read a JSON body with @RequestBody ​

A POST usually sends data in the body, as JSON:

http
POST /api/saveUser
Content-Type: application/json

{ "user_name": "John", "email": "john@example.com" }

Create a class shaped like the JSON:

java
public class User {

    @JsonProperty("user_name")         // the JSON key for this field
    private String username;
    private String email;

    // getters and setters
}

And receive it with @RequestBody:

java
@PostMapping("/saveUser")
public String saveUser(@RequestBody User user) {
    return "username: " + user.getUsername() + ", email: " + user.getEmail();
}

Response: username: John, email: john@example.com.

Spring Boot uses the Jackson library to turn JSON into the Java object. Jackson matches JSON keys to the class's properties by name:

  • email matches the field email directly.
  • user_name does not match username, so @JsonProperty("user_name") tells Jackson which key belongs to the field. Without it, username would stay null.

Step 7: what goes back ​

With @RestController, whatever the method returns is the response body, sent with status 200 OK. A response has more than a body, though:

PartExample
status code200 OK, 400 Bad Request, 404 Not Found
headersContent-Type: text/plain, your own headers
bodythe data

To control all three, return a ResponseEntity, which represents the entire response:

java
@GetMapping("/fetchUser")
public ResponseEntity<String> getUserDetails(@RequestParam String firstName) {
    String output = "fetching user details of " + firstName;
    return ResponseEntity.status(HttpStatus.OK)       // status
            .header("X-Source", "demo")               // a header
            .body(output);                            // body
}
http
HTTP/1.1 200
X-Source: demo
Content-Type: text/plain;charset=UTF-8

fetching user details of john

So far HttpStatus.OK is all we need. Chapter 21 covers ResponseEntity and the status codes properly: which code to return for which situation.

Summary: where each part of the request goes ​

In the requestAnnotationExample
which class handles it@RestController (or @Controller + @ResponseBody)
URL and HTTP method@RequestMapping, @GetMapping, @PostMapping, ...@GetMapping("/fetchUser")
query parameter@RequestParam?firstName=John
part of the path@PathVariable/fetchUser/{firstName}
JSON body@RequestBody (+ @JsonProperty for different names){"user_name": "John"}
custom text conversion@InitBinder + PropertyEditortrim and lower-case
the responsereturn value, or ResponseEntitystatus, headers, body

Next: The controller used @Autowired in chapter 3 and Spring created it without any new. Chapter 6 explains how: what a bean is, how the IoC container finds and creates beans, and the lifecycle each bean goes through.

Check yourself ​

  1. Which classes does Spring consider when it builds the handler mapping?
  2. What is the only difference between @Controller and @RestController?
  3. A @Controller method without @ResponseBody returns "hello". What does Spring do, and what does the client get here?
  4. What do path, value and method mean in @RequestMapping? Which two are aliases?
  5. What is @GetMapping("/x") equivalent to? What does @RequestMapping("/api") on the class do?
  6. firstName is a required @RequestParam and the request leaves it out. Is the method matched? What does the client get?
  7. Why is age declared as Integer rather than int when it is optional?
  8. How do you make every firstName parameter trimmed and lower-case before the method sees it?
  9. When would you use @PathVariable rather than @RequestParam?
  10. The JSON key is user_name and the Java field is username. What happens without @JsonProperty, and how do you fix it?
  11. What can a ResponseEntity set that a plain return value cannot?
Answers
  1. Only classes annotated @Controller or @RestController.
  2. @RestController adds @ResponseBody to every method.
  3. It treats "hello" as a view name and tries to render that view. With no such view, the client gets 404 Not Found.
  4. path: the URL; value: the same thing (alias of path); method: the HTTP method.
  5. @RequestMapping(path = "/x", method = RequestMethod.GET). It sets a prefix that is joined to every method's path.
  6. The URL matches the method, but Spring does not call it: 400 Bad Request (MissingServletRequestParameterException).
  7. A missing optional parameter is null, and a primitive int cannot hold null. (Or use defaultValue.)
  8. Write a PropertyEditorSupport subclass whose setAsText trims and lower-cases, and register it for String/"firstName" in an @InitBinder method of the controller.
  9. When the value identifies the resource and is part of the path, like /users/42.
  10. username stays null, because Jackson matches by name. Put @JsonProperty("user_name") on the field.
  11. The status code and headers, besides the body.