Spring RestController Usage
Rest Controller Annotations Annotations used for Spring MVC are used in Spring Boot too. Here are the common annotations:
@RestController - @Controller + @ResponseBody
@ResponseBody - indicates that the result type should be written straight in the response body in whatever format you specify like JSON or XML.
@RequestMapping - This annotation is used at both the class and method level. The @RequestMapping annotation is used to map web requests onto specific handler classes and handler methods.
@PostMapping - shortcut for @RequestMapping(method = RequestMethod.POST). There is also @GetMapping, @PutMapping, @DeleteMapping
@RequestParam - get the parameters in the request URL
@RequestBody - method parameter should be bound to the value of the HTTP request body.
@PathVariable - used to handle dynamic changes in the URI where a certain URI value acts as a parameter.
Sample Application The sample application demos most of the commonly used annotations in a Spring Boot web application
Entity class Task
1 2 3 4 5 6 7 8 9 10 11 12 13 @Entity @Data public class Task { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) private Long id; String description; boolean done; }
MyController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 @RestController @RequestMapping("api/tasks") class TaskController { @Autowired TaskRepository repository; @GetMapping public ResponseEntity<List<Task>> getAll () { try { List<Task> tasks = new ArrayList <Task>(); repository.findAll().forEach(tasks::add); if (tasks.isEmpty()) return new ResponseEntity <>(HttpStatus.NO_CONTENT); return new ResponseEntity <>(tasks, HttpStatus.OK); } catch (Exception e) { return new ResponseEntity <>(null , HttpStatus.INTERNAL_SERVER_ERROR); } } @GetMapping("/{id}") public ResponseEntity<Task> getById (@PathVariable("id") Long id) { Optional<Task> existingtaskOptional = repository.findById(id); if (existingtaskOptional.isPresent()) { return new ResponseEntity <>(existingtaskOptional.get(), HttpStatus.OK); } else { return new ResponseEntity <>(HttpStatus.NOT_FOUND); } } @GetMapping(value="/search", params = {"description", "done"}) public ResponseEntity<List<Task>> getByDescriptionAndDone ( @RequestParam(value="description", required = true) String description, @RequestParam(value="done", required = false, defaultValue = "false") boolean done) { List<Task> tasksFound = repository.findByDescriptionAndDone(description, done); return ResponseEntity.ok(tasksFound); } @PostMapping public ResponseEntity<Task> create (@RequestBody Task task) { try { Task savedtask = repository.save(task); return ResponseEntity.status(HttpStatus.CREATED).body(savedtask); } catch (Exception e) { return ResponseEntity.internalServerError().build(); } } @PutMapping( value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<Task> update (@PathVariable("id") Long id, @RequestBody Task task) { Optional<Task> existingtaskOptional = repository.findById(id); if (existingtaskOptional.isPresent()) { Task existingtask = existingtaskOptional.get(); existingtask.setDescription(task.getDescription()); existingtask.setDone(task.isDone()); return new ResponseEntity <>(repository.save(existingtask), HttpStatus.OK); } else { return new ResponseEntity <>(HttpStatus.NOT_FOUND); } } @DeleteMapping("/{id}") public ResponseEntity<HttpStatus> delete (@PathVariable("id") Long id) { try { repository.deleteById(id); return new ResponseEntity <>(HttpStatus.NO_CONTENT); } catch (Exception e) { return new ResponseEntity <>(HttpStatus.EXPECTATION_FAILED); } } }
@RestController A convenience annotation that is itself annotated with @Controller and @ResponseBody. This is used to indicate the class is used as Rest Controller.
@RequestMapping annotation can be used at the class level to map requests.
1 2 3 4 @RestController @RequestMapping("api/tasks") class TaskController {}
@GetMapping You can use @RequestMapping to map a GET request. However, it is often more convenient to use @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, or @PatchMapping.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @GetMapping public ResponseEntity<List<Task>> getAll () { try { List<Task> tasks = new ArrayList <Task>(); repository.findAll().forEach(tasks::add); if (tasks.isEmpty()) return new ResponseEntity <>(HttpStatus.NO_CONTENT); return new ResponseEntity <>(tasks, HttpStatus.OK); } catch (Exception e) { return new ResponseEntity <>(null , HttpStatus.INTERNAL_SERVER_ERROR); } }
Note that @GetMapping is different from @GetMapping("/"). The latter maps to the root URL(“/“) while the former maps to the current URL. You need to add “/“ to the URL to map to the root URL.
@PathVairiable Use @PathVariable to bind parameter to path variable.
1 2 3 4 5 6 7 8 9 10 @GetMapping("/byId/{id}") public ResponseEntity<Task> getById (@PathVariable("id") Long id) { Optional<Task> existingtaskOptional = repository.findById(id); if (existingtaskOptional.isPresent()) { return new ResponseEntity <>(existingtaskOptional.get(), HttpStatus.OK); } else { return new ResponseEntity <>(HttpStatus.NOT_FOUND); } }
@RequestParam Use @RequestParam to bind parameters to query parameters.
You can set if parameter is required and its default value for @RequestParam.
1 2 3 4 5 6 7 @GetMapping(value="/search", params = {"description", "done"}) public ResponseEntity<List<Task>> getByDescriptionAndDone ( @RequestParam(value="description", required = true) String description, @RequestParam(value="done", required = false, defaultValue = "false") boolean done) { List<Task> tasksFound = repository.findByDescriptionAndDone(description, done); return ResponseEntity.ok(tasksFound); }
@RequestBody POST or PUT request may have request body, use @RequestBody to bind the parameter to request body
1 2 3 4 5 6 7 8 9 @PostMapping public ResponseEntity<Task> create (@RequestBody Task task) { try { Task savedtask = repository.save(task); return new ResponseEntity <>(savedtask, HttpStatus.CREATED); } catch (Exception e) { return new ResponseEntity <>(null , HttpStatus.INTERNAL_SERVER_ERROR); } }
consumes and produces consumes element narrows the primary mapping by media types that can be consumed by the mapped handler. produces element narrows the primary mapping by media types that can be produced by the mapped handler.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @PutMapping( value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<Task> update (@PathVariable("id") Long id, @RequestBody Task task) { Optional<Task> existingtaskOptional = repository.findById(id); if (existingtaskOptional.isPresent()) { Task existingtask = existingtaskOptional.get(); existingtask.setDescription(task.getDescription()); existingtask.setDone(task.isDone()); return new ResponseEntity <>(repository.save(existingtask), HttpStatus.OK); } else { return new ResponseEntity <>(HttpStatus.NOT_FOUND); } }
Return a ResponseEntity You can create a ResponseEntity using new and return it.
1 2 3 4 5 6 7 8 9 @PostMapping public ResponseEntity<Task> create (@RequestBody Task task) { try { Task savedtask = repository.save(task); return new ResponseEntity <>(savedtask, HttpStatus.CREATED); } catch (Exception e) { return new ResponseEntity <>(null , HttpStatus.INTERNAL_SERVER_ERROR); } }
Or you can use a Builder
1 2 3 4 5 6 7 8 9 @PostMapping public ResponseEntity<Task> create (@RequestBody Task task) { try { Task savedtask = repository.save(task); return ResponseEntity.status(HttpStatus.CREATED).body(savedtask); } catch (Exception e) { return ResponseEntity.internalServerError().build(); } }
Download files Return value is org.springframework.core.io.Resource. You can use InputStreamResource or ByteArrayResource to return file.
Using InputStreamResource
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @GetMapping("/download") public ResponseEntity<Resource> downloadFile () throws FileNotFoundException { String filename = "abc.txt" ; File file = ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX + filename); InputStreamResource resource = new InputStreamResource (new FileInputStream (file)); HttpHeaders headers = new HttpHeaders (); headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=abc.txt" ); headers.add("Cache-Control" , "no-cache, no-store, must-revalidate" ); headers.add("Pragma" , "no-cache" ); headers.add("Expires" , "0" ); return ResponseEntity.ok() .headers(headers) .contentLength(file.length()) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(resource); }
Use ByteArrayResource if byte array is already available.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @GetMapping("/downloadimage") public ResponseEntity<Resource> downloadImage () throws IOException { String filename = "star.jpeg" ; File file = ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX + filename); ByteArrayResource resource = new ByteArrayResource (FileUtils.readFileToByteArray(file)); HttpHeaders headers = new HttpHeaders (); headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=star.jpeg" ); headers.add("Cache-Control" , "no-cache, no-store, must-revalidate" ); headers.add("Pragma" , "no-cache" ); headers.add("Expires" , "0" ); return ResponseEntity.ok() .headers(headers) .contentLength(file.length()) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(resource); }
Another option is to write to copy to the response output stream.
1 2 3 4 5 6 7 8 9 10 @GetMapping(path = "/fileoutput", produces = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity<?> fileoutput(HttpServletResponse response) throws Exception { String filename = "abc.txt" ; File file = ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX + filename); InputStream yourInputStream = new FileInputStream (file); IOUtils.copy(yourInputStream, response.getOutputStream()); response.flushBuffer(); return ResponseEntity.ok().build(); }
Unit Test Use MockMvc to test the controller without starting the server. You can use Mockito to mock the repository and inject it to the controller. This way you can test the controller in isolation without starting the server and loading the full application context.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class TaskControllerTest { private TaskRepository repository; private MockMvc mockMvc; @BeforeAll public void setup () { repository = Mockito.mock(TaskRepository.class); this .mockMvc = MockMvcBuilders.standaloneSetup(new TaskController (repository)).build(); } @Test public void findTask () throws Exception { Task laundryTask = new Task (); laundryTask.setDescription("Laundry" ); when(repository.findAll()).thenReturn(List.of(laundryTask)); this .mockMvc.perform(get("/api/tasks" )) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].description" ).value("Laundry" )); } @Test public void addTask () throws Exception { Task laundryTask = new Task (); laundryTask.setDescription("Laundry" ); when(repository.save(any(Task.class))).thenReturn(laundryTask); this .mockMvc.perform(post("/api/tasks" ) .contentType(MediaType.APPLICATION_JSON) .content("{\"description\": \"Laundry\"}" )) .andExpect(status().isCreated()) .andExpect(jsonPath("$.description" ).value("Laundry" )); } }
Integration Test You can use @SpringBootTest to start a complete Spring Boot application with FULL app context.
Use @SpringBootTest for integration test. The initial data can be loaded using src/test/resources/data.sql file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles(profiles = "integration-test") public class TaskControllerFullConfigTest { @Autowired private TaskRepository repository; @Autowired private MockMvc mockMvc; @Test public void findTask () throws Exception { this .mockMvc.perform(get("/api/tasks" )) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].description" ).value("Laundry" )); } }
Reference