Appearance
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
}
}| Request | Response |
|---|---|
GET /view/fetchUser | 404 Not Found: no view with that name exists |
GET /view/withBody | fetching 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:
| Attribute | Meaning |
|---|---|
path (alias value) | the URL. path and value are aliases: use either. |
method | the HTTP method: RequestMethod.GET, POST, PUT, PATCH, DELETE |
consumes / produces | the 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:
| Request | Response |
|---|---|
?firstName=John&lastName=Smith&age=32 | fetching user details of John Smith, age 32 |
?firstName=John | fetching user details of John null, age null |
?lastName=Smith | 400 Bad Request |
requiredistrueby 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 showsMissingServletRequestParameterException: Required request parameter 'firstName' ... is not present).- With
required = false, a missing parameter isnull. That is whyageis anIntegerhere: a primitiveintcannot benull. (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/JohnMark 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 use | optional filters, search options | identifying 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:
emailmatches the fieldemaildirectly.user_namedoes not matchusername, so@JsonProperty("user_name")tells Jackson which key belongs to the field. Without it,usernamewould staynull.
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:
| Part | Example |
|---|---|
| status code | 200 OK, 400 Bad Request, 404 Not Found |
| headers | Content-Type: text/plain, your own headers |
| body | the 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 johnSo 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 request | Annotation | Example |
|---|---|---|
| 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 + PropertyEditor | trim and lower-case |
| the response | return value, or ResponseEntity | status, 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
- Which classes does Spring consider when it builds the handler mapping?
- What is the only difference between
@Controllerand@RestController? - A
@Controllermethod without@ResponseBodyreturns"hello". What does Spring do, and what does the client get here? - What do
path,valueandmethodmean in@RequestMapping? Which two are aliases? - What is
@GetMapping("/x")equivalent to? What does@RequestMapping("/api")on the class do? firstNameis a required@RequestParamand the request leaves it out. Is the method matched? What does the client get?- Why is
agedeclared asIntegerrather thanintwhen it is optional? - How do you make every
firstNameparameter trimmed and lower-case before the method sees it? - When would you use
@PathVariablerather than@RequestParam? - The JSON key is
user_nameand the Java field isusername. What happens without@JsonProperty, and how do you fix it? - What can a
ResponseEntityset that a plain return value cannot?
Answers
- Only classes annotated
@Controlleror@RestController. @RestControlleradds@ResponseBodyto every method.- It treats
"hello"as a view name and tries to render that view. With no such view, the client gets 404 Not Found. path: the URL;value: the same thing (alias ofpath);method: the HTTP method.@RequestMapping(path = "/x", method = RequestMethod.GET). It sets a prefix that is joined to every method's path.- The URL matches the method, but Spring does not call it: 400 Bad Request (
MissingServletRequestParameterException). - A missing optional parameter is
null, and a primitiveintcannot holdnull. (Or usedefaultValue.) - Write a
PropertyEditorSupportsubclass whosesetAsTexttrims and lower-cases, and register it forString/"firstName"in an@InitBindermethod of the controller. - When the value identifies the resource and is part of the path, like
/users/42. usernamestaysnull, because Jackson matches by name. Put@JsonProperty("user_name")on the field.- The status code and headers, besides the body.