Programma
Il programma vero e proprio è composto da 4 classi, la struttura è ovviamente semplificata (tutti i dati gestiti dal programma sono rappresentati da un solo oggetto) ma ci sono tutti i ruoli presenti in applicazioni più grandi: il modello dei dati (cioè gli oggetti da gestire), la rappresentazione dei dati completamente gestita da Spring in JSON sia in input che in output e un controllore delle operazioni!
Il modello dei dati
package it.aspix.todolist; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity(name = "todo") public class ToDo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Integer id; String lista; String cosa; Boolean fatto; /* il metodi set/get vanno inseriti per esercizio, magari usando Eclipse */ }
Un normalissimo oggetto Java con alcune accortezze. Concentriamoci adesso
sulle annotazioni (quelle strane cose che iniziano con @) che permettono
a Spring di gestire l'intero progetto. Ogni annotazione è legata all'istruzione che la segue:
@Entity
è relativa alla definizione della classe e in questo caso sta
a marcare che ToDo
è un oggetto che JPA dovrà gestire in relazione
al database relazionale che stiamo usando (H2 nel nostro caso, ma se fosse un altro
non cambierebbe nulla), l'atrributo name
indica il nome della tabella nel database,
in genere non è necessario ma in questo caso si. Per default un oggetto "ToDo"
dovrebbe trovarsi nel db in una tabella chiamata "to_do" siccome così non è dobbiamo
specificare il nome della tabella, altrimenti non sarebbe stato necessario
(il camelCase viene tradotto con gli "_").
@Id
marca l'attributo che è chiave primaria e il
successivo @GeneratedValue
con il suo attributo strategy
indicano
che la colonna sul db è di tipo auto_increment.
Una ultima cosa da notare: l'attributo id
è di tipo Integer
e non int
, questo perché vogliamo che l'attributo possa essere null
(fa comodo quando si fanno le ricerche),
questo non sarebbe possibile con il tipo int, stesso discorso vale per
Boolean fatto
.
Interfaccia verso il db
Questa è probabilmente la classe più strana... in effetti non è neanche una classe ma una interfaccia: poi tutto il lavoro lo fa Spring.
package it.aspix.todolist; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; public interface ToDoRepository extends JpaRepository<ToDo, Integer>{ @Query(value="SELECT todo.* "+ "FROM todo "+ "WHERE todo.cosa ILIKE :parte", nativeQuery=true) List<ToDo> cerca(@Param("parte") String parteDelCosa); }
L'unica parte davvero essenziale è la sola dichiarazione dell'interfaccia,
va fatta sempre in questo modo per ogni oggetto che vogliamo recuperare/salvare
nel db, JpaRepository
è un tipo generico e deve avere come
parametri due cose: il tipo dell'oggetto che stiamo considerando e il tipo della sua chiave.
In questa interfaccia è presente però anche la funzione cerca()
che ci serve per poter fare un tipo particolare di ricerca: una ricerca per approssimazione.
L'annotazione @Query
serve per specificare la query da eseguire e il suo
parametro nativeQuery
impostato a true nel nostro caso è essenziale perché
stiamo scrivendo la query in SQL (e per uno specifico motore di database,
se usiamo una nsintassi valida solo per uno specifico database in caso che volessimo
cambiarlo dovremmo riscrivere la query),
se non lo avessimo messo avremmo dovuto
usare la sintassi JPA nella query che è leggermente diversa da quella di SQL
(e per questo non la trattiamo qui).
Due cose da notare sul come è scritta la query:
- il metodo ha un un parametro
parte
che viene passato alla funzione e inserito nella query sopra utilizzando la sintassi:parte
- vengono selezionati tutti i campi dell'oggetto in questione scrivendo
SELECT todo.*
Il gestore delle azioni possibili
In questa classe definiamo cosa la nostra applicazione può fare, lo facciamo dicendo a quali metodi di HTTP e a quali URL risponde. Ogni funzione della classe riponde ad un particolare metodo+percorso di HTTP, il tutto viene specificato tramite annotazioni prima delle funzioni Java.
I dati vengono letti in input e inviati al client in output in JSON sebbene non ci sia traccia di questo perché fa tutto Spring Boot senza nessun intervento da parte nostra.
package it.aspix.todolist; import java.util.List; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Example; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; @RestController public class ToDoManager { @Autowired ToDoRepository repoToDo; @GetMapping("/esempio1") public List<ToDo> elenco() { List<ToDo> k = repoToDo.findAll(); return k; } @GetMapping("/todos") public List<ToDo> cerca( @RequestParam(required = false) String lista, @RequestParam(required = false) Boolean fatto ) { ToDo t = new ToDo(); t.setLista(lista); t.setFatto(fatto); Example<ToDo> example = Example.of(t); return repoToDo.findAll( example ); } @GetMapping("/esempio2") public List<ToDo> cercaParte( @RequestParam String cosa){ return repoToDo.cerca("%"+cosa+"%"); } @GetMapping("todo/{id}") public Optional<ToDo> prendiPerChiave( @PathVariable int id ) { return repoToDo.findById(id); } @PostMapping("/todo") public void inserisci(@RequestBody ToDo td) { repoToDo.save( td ); } @DeleteMapping("todo/{id}") public void cancellaPerChiave( @PathVariable int id ) { repoToDo.deleteById(id); } @PutMapping("todo/{id}") public void modifica( @PathVariable int id, @RequestBody ToDo td ) { Optional<ToDo> k = repoToDo.findById(id); if(k.isPresent()) { ToDo t = k.get(); t.setLista(td.getLista()); t.setFatto(td.isFatto()); t.setCosa(td.getCosa()); repoToDo.save(t); } else { throw new ResponseStatusException(HttpStatus.NOT_FOUND); } } }
Al soilito tante cose vengono risolte dalle annotazioni: @RestController
indica che questa classe offre servizi di tipo REST mentre @Autowired
fa una cosa un pochino più complicata. ToDoRepository repoToDo
dichiara una
variabile ma non c'è traccia di costruzione dell'oggetto: questo perché Spring
usa una tecnica chiamata "Dependency Injection" che in pratica significa che l'oggetto
del titpo giusto lo crea Spring e quando la nostra classe funzionerà avrà una
istanza di ToDoRepository da poter utilizzare.
Vediamo i singoli metodi uno per uno, poniamo per semplicità che il server andrà in esecuzione sulla macchina locale alla porta usuale (8080).
elenco()
@GetMapping("/esempio1") public List<ToDo> elenco() { List<ToDo> k = repoToDo.findAll(); return k; }
@GetMapping
indica due cose: che la funzione elenco() risponde al metodo
GET di HTTTP e che risponde alla URL "/todolist"... più facile a farsi che a dirsi
"http://localhost:8080/todolist" e otterremo un elenco di tutte le cose da fare.
Il metodo in questione ritorna una lista di oggetti di tipo ToDo poi ci penserà
Spring a rappresentarli come JSON. Per recuperare gli oggetti dal DB usa il metodo
findAll() del nostro repoToDo
(NB: che non abbiamo mai scritto,
fa tutto Spring).
cerca1()
@GetMapping("/todos") public List<ToDo> cerca( @RequestParam(required = false) String lista, @RequestParam(required = false) Boolean fatto ) { ToDo t = new ToDo(); t.setLista(lista); t.setFatto(fatto); Example<ToDo> esempioDaCercare = Example.of(t); return repoToDo.findAll( esempioDaCercare ); }
Il @GetMapping
è analogo al precedente ma questa volta la funzione cerca()
ha due parametri: lista
e fatto
.
@RequestParam
serve per indicare
che ciascuno dei due parametri è opzionalmente presente come parametro della URL,
di nuovo alcuni esempi saranno chiarificatori: potremmo usare
"http://localhost:8080/cerca1?lista=casa" oppure "http://localhost:8080/cerca1" o anche
"http://localhost:8080/cerca1?lista=casa&fatto=false".
Quello che fa la funzione è di chiedere a repoToDo
di fare
una ricerca fornendo un esempio di quello che vogliamo cercare, le prime 4 linee del
metodo servono appunto a creare l'esempio!
Creo l'oggetto t
e imposto due dei suoi campi (eventualmente a null se non
erano presenti nella richiesta), poi con Example.of(t)
creo un "Esempio"
da fornire alla funzione di ricerca.
cerca2()
@GetMapping("/esempio2") public List<ToDo> cercaParte( @RequestParam String cosa ){ return repoToDo.cerca("%"+cosa+"%"); }
Questa funzione risponde ad un richiesta GET alla url "/cerca2" che però
deve avere obbligatoriamente il parametro "cosa" in quanto @RequestParam
questa volta non ha il parametro required = false
. Per svolgere la ricerca
usa la funzione cerca()
che abbiamo dichiarato in ToDoManager.
Un esempio di URL: "http://localhost:8080/cerca2?cosa=pulire"
prendiPerChiave()
@GetMapping("todo/{id}") public Optional<ToDo> prendiPerChiave( @PathVariable int id ) { return repoToDo.findById(id); }
Qui la particolarità è come viene passato il parametro di ricerca:
tramite il percorso nella URL.
@PathVariable
sta appunto ad indicare che "id" è una parte
della URL. Per cercare l'elemento con id=4 dovremo
scrivere "http://localhost:8080/todo/1", poi la funzione esegue la ricerca
seguendo il solito schema.
inserisci()
@PostMapping("/todo") public void inserisci(@RequestBody ToDo td) { repoToDo.save( td ); }
A differenza dei precedenti l'annotazione della funzione è
@PostMapping
il che indica che l'azione HTTP richiesta
deve essere POST e l'altra annotazione @RequestBody
indica che l'oggetto da inserire (uno di tipo ToDo) deve
essere scritto nel corpo ovviamente in formato JSON!
L'unica istruzione della funzione serve a salvare l'oggetto nel DB.
Attenzione che questa richiesta non si può inviare inserendo la URL nella barra del browser perché questa sarebbe una rchiesta GET, bisogna usare uno strumento tipo PostMan o fare una richiesta asincrona, in ogni caso va inserita una rappresentazione JSON di un ToDo nel corpo della richiesta.
cancellaPerChiave()
@DeleteMapping("todo/{id}") public void cancellaPerChiave( @PathVariable int id ) { repoToDo.deleteById(id); }
Questo metodo risponde ad una richiesta DELETE di HTTP e cancella l'elemento il cui id è stato inserito nel path della richiesta.
"http://localhost:8080/todo/2" elimina l'elemento numero 2. Anche per provare questo tipo di richiesta non si può usare la barra degli indirizzi del browser.
modifica()
@PutMapping("todo/{id}") public void modifica( @PathVariable int id, @RequestBody ToDo td ) { Optional<ToDo> k = repoToDo.findById(id); if(k.isPresent()) { ToDo t = k.get(); t.setLista(td.getLista()); t.setFatto(td.isFatto()); t.setCosa(td.getCosa()); repoToDo.save(t); } else { throw new ResponseStatusException(HttpStatus.NOT_FOUND); } }
Sebbene la modalità di comunicazione dovrebbe ormai essere chiara (richiesta PUT: la funzione prima di tutto recupera l'oggetto presente nel db con l'id specificato, in caso che quell'oggetto esiste copia i dati forniti nell'oggetto recuperato dal db e poi lo salva. Se l'oggetto non esiste ritorna una risposta 404.
main!
Anche un programma Spring Boot ha bisogno di un metodo main, noi lo metteremo in una classe separata:
package it.aspix.todolist; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class TodolistApplication { public static void main(String[] args) { SpringApplication.run(TodolistApplication.class, args); } }
Molto breve, il metodo main lancia l'applicazione Spring e la classe stessa
è marcata come @SpringBootApplication
che indica a Spring di
dover fare tutte le configurazioni del caso, cercare i componenti dell'applicazione
(ad esempio la nostra classe ToDoListManager)...
A questo punto non resta che avviare l'applicazione.
CORS
Molte volte un sito chiede risorse ad altri siti, questa pratica viene chiamata
Cross-Origin Resource Sharing e per default non è permessa dai browser
per motivi di sicurezza.
Quando un oggetto come ad esempio XMLHttpRequest
fa una richiesta verso un sito diverso
rispetto a quello da cui è stata caricato
(cioè la URL della richiesta ha un diverso protocollo, indirizzo o porta) il
browser controlla se il sito a cui è stata fatta la richiesta permette che altri siti interagiscano
con lui in base ad un header HTTP eventualmente presente nella risposta:
Access-Control-Allow-Origin
.
Se la richiesta CORS è permessa allora il browser recapita la risposta allo script
altrimenti la richiesta da origine ad un errore.
Spring Boot ci mette a disposizione una annotazione che può essere inserita
prima della classe o dei singoli metodi per permettere al nostro sistema
di servire richieste CORS: @CrossOrigin
(il nome completo è org.springframework.web.bind.annotation.CrossOrigin). Questa
annotazione inserisce nella risposta l'header richiesto per permettere ad uno script
presente in altri siti di chiedere dati al nostro sistema.
L'annotazione permette di essere più selettivi riguardo a chi può farci richieste,
alcuni articoli come
questo su Baeldung
forniscono ulteriori dettagli.
Nomi dei servizi
Dare nomi sensati è importante perché chi scrive i servizi non sarà poi colui che li usa e l'interesse è che gli altri sviluppatori possano utilizzarli facilmente.
Anche qui ci sono delle convenzioni: i nomi devono essere dei sostantivi, singolare se il servizio ritorna un singolo oggetto, plurale se ne ritorna un elenco, non devono essere dei verbi perché l'azione da compiere è specificata dal metodo HTTP utilizzato.
Poniamo il caso che io abbia un servizio che gestisce i dati anagrafici di un insieme di persone, alcuni esempi di azione / end point:
- [GET] /persona/317
- restituisce un singolo oggeto Persona con le informazioni riguardo all'utente il cui id è 317
- [GET] /persone?anno=2009
- restituisce un elenco di Persona che sono nate nel 2009
- [DELETE] /persona/317
- elimina l'oggetto Persona il cui id è 317
- [POST] /persona
- inserisce un nuovo oggetto Persona (che è descritto nel body della richiesta) nel sistema
- [PUT] /persona/44
- rimpiazzare la Persona esistente con id 44 nel sistema (i nuovi dati sono presenti nel body della richiesta)
Una cosa utile in caso di errori:
se durante lo sviluppo qualcosa va storto ma non si riesce a capire
il perché potrebbe far comodo aprire un terminale nella cartella del progetto
e usare direttamente Maven, in particolare i comandi mvn clean
,
mvn compile
e mvn spring-boot:run
(poi per fermare il server CTRL+C)
potrebbero risultare utili.