springBootApplicazione

prima applicazione

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:

  1. il metodo ha un un parametro parte che viene passato alla funzione e inserito nella query sopra utilizzando la sintassi :parte
  2. 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.