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); } }
@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 using @WebMvcTest Use @WebMvcTest annotation to test a controller without the full application configuration.
By default @WebMvcTest auto configures the Controller and MockMvc. You can use @MockBean to create beans required by the Controller.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @WebMvcTest(TaskController.class) public class TaskControllerTest { @MockBean private TaskRepository repository; @Autowired private MockMvc mockMvc; @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" )); } }
If you want to only test a controller use @WebMvcTest annotation.
If you are looking to load your full application context and use MockMVC, you should consider @SpringBootTest combined with @AutoConfigureMockMvc rather than this annotation.
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