Controller
Trebuie clarificat în primul rând cum funcționează aplicația de backend în Spring Boot. Când aplicația este pornită, se deschide un port și se așteaptă cererile HTTP. Cererea este parsată și transformată într-un context HTTP. Contextul este trimis către un filter chain care va apela rutinele adecvate pentru acea cerere și va întoarce răspunsul înapoi în acel lanț. Fiecare pas executat în acest lanț se numește filter, iar o parte din acestea pot fi definite de către dezvoltatori. Ultimul filtru executat la tratarea cererii apelează clase definite de dezvoltator de tip controller. În controller se specifică ce endpoint-uri/rute din API-ul serverului corespund la ce metode din acea clasă. O clasă controller este o clasă specială ai cărei metode publice sunt apelate la accesul rutelor corespunzătoare metodei, acestea sunt decorate cu @RestController. Pentru ca framework-ul să identifice controllerele și rutele, se decorează clasa și metodele cu adnotări. De exemplu, @RestController care specifică framework-ului că această clasă trebuie să fie folosită ca controller; @RequestMapping("/api/controller") pus pe clasă și @GetMapping("/my-route") pe metoda din controller specifică că atunci când se accesează ruta "/api/controller/my-route" cu un HTTP GET, se apelează acea metodă în cauză.
Decorarea claselor și metodelor cu adnotări este un caz de AOP (aspect-oriented programming).
În cereri HTTP, datele transmise către server pot fi specificate în mai multe locații din cerere care pot fi extrase și pasate automat ca parametri pentru metoda din controller corespunzătoare rutei. Aceste locații se specifică folosind adnotări în fața parametrilor în următoarele moduri:
- Pentru parametri specificați într-o rută, ca de exemplu "api/{type}/user/{id}", se utilizează @PathVariable:
@GetMapping("/api/{type}/user/{id}")
public ResponseEntity<?> myMethod(@PathVariable String type, @PathVariable UUID id) {
// Implementare metodă
}
- Parametrii de URL/query sunt specificați cu @RequestParam.
- Câmpurile din header-ul cererii sunt extrase prin @RequestHeader.
- Pentru form-uri, câmpurile din formular pot fi extrase prin @ModelAttribute, iar fișierele pot fi manipulate cu MultipartFile.
- Body-ul cererii poate fi extras și deserializat într-un singur obiect prin @RequestBody.
Rutele apelate din backend vor răspunde cu un obiect care va fi automat serializat într-un răspuns HTTP ca JSON. Pentru a seta codul de status și a împacheta răspunsul întors, se folosesc metodele din ResponseEntity cum ar fi ResponseEntity.ok(), ResponseEntity.badRequest(), ResponseEntity.status() etc.
Folosiți codurile de status corespunzătoare pentru diferitele cazuri de succes sau eroare. Este o practică bună pentru a documenta API-ul, atât pentru persoane, cât și pentru sistemele conectate la acesta, reducând ambiguități și prevenind cazuri de eroare pentru clienții API-ului.
Puteți urmări în GitHub codul pentru controllere cu explicații și exemple de utilizare. Modul de lucru cu un controller este intuitiv. Pe lângă decorarea metodelor publice cu adnotări, trebuie să fie injectate serviciile care implementează logica aplicației.
Nu implementați logica direct în controller. Fiecare componentă trebuie să îndeplinească o funcționalitate specifică. Dacă un repository doar lucrează cu baza de date, iar serviciile implementează logica aplicației, atunci un controller trebuie doar să trateze cererea și să împacheteze răspunsul.
Mai jos aveți un exemplu pentru un controller care pe ruta "api/userfile/download/{id}" la metoda GET va trimite un fișier către client sau un răspuns de eroare.
@RestController
@RequestMapping("/api/userfile")
public class UserFileController {
private final UserFileService userFileService;
public UserFileController(UserFileService userFileService) {
this.userFileService = userFileService;
}
@GetMapping("/download/{id}")
public ResponseEntity<Resource> download(@PathVariable UUID id) {
FileResponse file = userFileService.getFileDownload(id);
if (file == null) {
return ResponseEntity.notFound().build();
}
ByteArrayResource resource = new ByteArrayResource(file.getContent());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(file.getContent().length)
.body(resource);
}
}
Operatii CRUD
Pentru o mai mare claritate, vom implementa operațiile CRUD (Create Read Update Delete) în fiecare controller. În general, pentru fiecare entitate din baza de date, sau cel puțin pentru un subset care să fie expus utilizatorului, se pot efectua operații de bază. Acestea sunt cele patru mari operații CRUD:
- Create - sunt operații de adăugare, de obicei sunt executate prin cereri de tip POST.
- Read - sunt operații de citire a obiectelor, de obicei sunt cereri de tip GET. Se pot citi un singur obiect sau mai multe. Dacă datele care se pot extrage sunt foarte multe, listele de obiecte trebuie să fie extrase paginat pentru a nu încărca atât serverul cât și clientul cu date în mod inutil.
- Update - sunt operații de modificare a datelor pe server, de obicei sunt cereri de tip PUT.
- Delete - sunt operații de ștergere, fie de ștergere completă a datelor sau doar de invalidare (soft delete), de obicei sunt cereri de tip DELETE.
Pentru transferul efectiv al datelor, nu se folosesc entități de bază de date în mod direct, ci se utilizează DTO-uri (Data Transfer Objects), proiectând entitățile către acestea din motive de securitate, performanță și pentru a preveni erori. DTO-urile sunt doar obiecte clasice care nu sunt gestionate de ORM și prin intermediul cărora se pot transfera informațiile din entități în diversele locuri din aplicație și către exterior.
Respectați pe cât posibil convenția asocierea acestor tipuri de operații cu metodele HTTP, este o bună practică pentru ca persoanele care vor folosi API-ul să-l înțeleagă mai ușor.
Swagger/OpenAPI Specifications
Avantajul de a folosi Spring Boot este integrarea facilă a documentației OpenAPI prin Springdoc OpenAPI. OpenAPI este un standard pentru descrierea unui API REST.
Avantajele folosirii OpenAPI sunt:
- Ușurința de a înțelege API-ul
- Automatizarea interconectării între clienți și API
- UI interactiv pentru testarea API-ului
- Generarea automată de documentație
Pentru a adăuga suport OpenAPI în Spring Boot, se adaugă dependința in pom.xml
:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.0.2</version>
</dependency>
Apoi, accesarea Swagger UI se poate face la http://localhost:8080/swagger-ui.html
.
Exemplu de controller cu adnotări Swagger:
@RestController
@RequestMapping("/api/users")
public class UserController {
@Operation(
summary = "Obține utilizator după ID",
description = "Returnează detaliile unui utilizator pe baza ID-ului specificat.",
responses = {
@ApiResponse(responseCode = "200", description = "Utilizator găsit",
content = @Content(schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "404", description = "Utilizatorul nu a fost găsit")
}
)
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable String id) {
User mockUser = new User(id, "John Doe", "john.doe@example.com");
return ResponseEntity.ok(mockUser);
}
}
Daca in semnatura controller-ului specificati tipul de entitate returnata, swagger va va genera automat documentatia cu tipul de entitate.
Daca returnati ResponseEntiry<?>
, swagger-ul nu o sa stie tipul de entitate pe care vreti sa il intoarceti asa ca trebuie sa specificati asta manual, prin adnotari specifice swagger.