Web Service: Todolist
In questa lezione creeremo un Web Service con SpringBoot, usando la specifica REST API.
Obiettivo della lezione è comprendere i meccanismi principali di funzionamento di SpringBoot applicandoli ad un esempio concreto.
Andremo a creare una API REST che offrirà questi servizi:
- GET /: mostra tutte le todo esistenti
- GET /{id} : mostra una todo con id
- POST /: crea una nuova todo
- PUT /{id}: modifica una TODO
- DELETE /{id}: elimina una TODO
La singola todo avrà questa struttura:
- id: generato lato server dal db (un intero che si autoincrementa)
- title: titolo
- completed: booleano che indica se è completata
Il Web Service riceve ed invia i dati in formato JSON.
Richiamiamo velocemente lo schema già visto di architettura di un Web Service:

Nello specifico la nostra applicazione avrà:
- TodoController: gestisce le richieste HTTP
- TodoService: gestisce le azioni CRUD (create read update delete)
- TodoRepository: gestisce l’accesso al db
- Todo: rappresenta la singola Todo salvata nel DB
- TodoDTO: oggetto di interscambio dati tra i layer e verso l’esterno
Controller
package it.cipiaceinfo.todolist.todo;
import java.util.List;
import org.springframework.http.ResponseEntity;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PutMapping;
@RestController
@RequestMapping
public class TodoController {
private TodoService todoService;
public TodoController(TodoService todoService) {
this.todoService = todoService;
}
@GetMapping()
public List<TodoDTO> getAll() {
List<TodoDTO> todos = todoService.getAllTodo();
return todos;
}
@GetMapping("/{id}")
public TodoDTO getTodo(@PathVariable Long id) {
return todoService.getTodo(id);
}
@PostMapping()
public TodoDTO createTodo(@RequestBody Todo todo) {
return todoService.create(todo);
}
@PutMapping("/{id}")
public TodoDTO updateTodo(@PathVariable Long id, @RequestBody Todo todo) {
return todoService.update(id, todo);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTodo(@PathVariable Long id) {
todoService.delete(id);
return ResponseEntity.noContent().build();
}
}Vediamo le annotazioni:
- @RestController: sottotipo di controller che gestisce le richieste REST, ogni metodo quindi restituisce un body di risposta HTTP (di default JSON);
- @RequestMapping: indica che la classe contiene metodi che intercettano url, può indicare la base URL delle richieste;
- @GetMapping, @PostMapping, @DeleteMapping, @PutMapping: mapping specifici per i quattro metodi REST, con annessa porzione dell’URL. Per alcune di queste sono indicate anche porzioni parametriche (variabili nel path), che sono poi intercettate nella firma del metodo sottostante
- @PathVariable: annotazione che indica che il parametro è in realtà una path variable della URL
- @RequestBody: annotazione che indica che il parametro è il body della richiesta
Service
package it.cipiaceinfo.todolist.todo;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class TodoService {
private final TodoRepository todoRepository;
public TodoService(TodoRepository todoRepository) {
this.todoRepository = todoRepository;
}
public List<TodoDTO> getAllTodo() {
return todoRepository.findAll().stream().map(TodoDTO::new).toList();
}
public TodoDTO getTodo(Long id) {
return todoRepository.findById(id)
.map(TodoDTO::new)
.orElseThrow (() -> new RuntimeException("Non trovato"));
}
public TodoDTO create(Todo todo) {
return new TodoDTO(todoRepository.save(todo));
}
public TodoDTO update(Long id, Todo todoDTO) {
Todo todo = todoRepository.findById(id)
.orElseThrow (() -> new RuntimeException("Non trovato"));
todo.setCompleted(todoDTO.isCompleted());
return new TodoDTO(todoRepository.save(todo));
}
public void delete(Long id) {
todoRepository.deleteById(id);
}
}
Il codice è abbastanza esplicativo, con alcuni punti di attenzione:
- TodoService traduce l’entità Todo nell’oggetto TodoDTO.
L’oggetto TodoDTO è questo:
package it.cipiaceinfo.todolist.todo;
public class TodoDTO {
private Long id;
private String title;
private boolean completed;
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public boolean isCompleted() {
return completed;
}
protected TodoDTO() {}
public TodoDTO(Todo todo) {
this.id = todo.getId();
this.title = todo.getTitle();
this.completed = todo.isCompleted();
}
}
Un DTO (Data Transfer Object) è una classe Java che di norma non contiene logica, e viene utilizzata come semplice struttura dati. Si sceglie di usare questa struttura e non direttamente l’oggetto Todo (anche se contiene le stesse proprietà) perché l’oggetto Todo in realtà è una entità del database che viene “mappata” e contiene quindi anche i metodi per essere salvata sul database. E’ una buona prassi passare al controller invece oggetti che contengono solo dati, in modo da evitare il rischio che il controller possa direttamente salvarli e quindi “saltare” il Service. In questo progetto è ovviamente ridondante ma è sempre meglio farlo anche nei progetti più semplici, per abituarsi ad una buona progettazione.
- Vediamo il codice seguente:
return todoRepository.findById(id)
.map(TodoDTO::new)
.orElseThrow (() -> new RuntimeException("Non trovato"));L’oggetto restituito da TodoRepository è come vedremo un Optional<Todo>. Gli Optional sono contenitori generici di oggetti che possono essere nulli, e possono essere trattati come Stream (infatti si vede la funzione map) ma vanno comunque gestiti anche i casi null
Model Entity
L’oggetto Todo è una @Entity:
package it.cipiaceinfo.todolist.todo;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Todo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private boolean completed;
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public boolean isCompleted() {
return completed;
}
public void setCompleted(boolean completed) {
this.completed = completed;
}
protected Todo() {}
public Todo(Long id, String title) {
this.id = id;
this.title = title;
}
}
Una @Entity è una classe gestita dalla libreria JPA di Java EE, che utilizza nello specifico Hibernate. Questa libreria è un OR/M (Object Relation Mapper), un pattern architetturale che gestisce in modo automatico il salvataggio delle entità nel database.
La @Entity è l’oggetto che viene utilizzato da Hibernate per caricare/salvare i dati sul database, tramite i @Repository (vedi sotto). Inoltre se l’oggetto viene modificato, Hibernate ne può tenere traccia per il futuro salvataggio.
Una Entity ha quattro stati possibili:
- TRANSIENT: l’oggetto è stato appena creato, e non è ancora salvato sul DB. Eventuali modifiche all’oggetto non sono gestite.
- MANAGED: l’oggetto è sotto il controllo di Hibernate ed è quindi salvato sul DB. Se viene modificato, al prossimo salvataggio viene automaticamente salvato.
- DETACHED: l’oggetto è stato “staccato” da Hibernate, le modifiche non sono più tracciate nè salvate.
- REMOVED: l’oggetto è stato eliminato dal DB, ma la classe esiste ancora.
Di fatto nella maggior parte dei progetti sono importanti i primi due stati. In ogni caso per evitare confusione, come detto sopra si usano i DTO per evitare di usare entità che potrebbero essere modificate e quindi salvate sul DB.
Repository
Le entità sono oggetti “passivi”, sono in realtà controllati dai Repository JPA.
Un Repository JPA è una interfaccia contenitore di una Entity che consente di esporre le operazioni CRUD senza scrivere SQL. Le operazioni principali che offre sono:
- save(entity)
- findById(id)
- findALl(id)
- delete(id)
- count()
- saveAll()
Il repository effettivo che viene istanziato riceve due tipi di dato: il tipo della entity che viene gestito, e il tipo dell’indice da utilizzare (l’id della entity).
package it.cipiaceinfo.todolist.todo;
import org.springframework.stereotype.Repository;
import org.springframework.data.jpa.repository.JpaRepository;
@Repository
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
In pratica Hibernate quando creiamo l’annotazione @Repository e creiamo un JpaRepository, crea automaticamente la tabella nel database, le query per la gestione di caricamento, salvataggio, modifica, ecc. ed offre automaticamente i metodi sopra indicati.
E’ ovviamente possibile aggiungere altri metodi, se servono per il progetto. Di fatto in questo progetto è bastato scrivere la struttura della Entity e questa semplice dichiarazione di classe per avere tutto quel che serve. Questo significa che per progetti semplici Spring Boot non richiede che il programmatore abbia conoscenze di database.
Database
Un database tuttavia serve. Hibernate/JPA è compatibile con le principali tecnologie di database:
- Mysql
- PostgreSQL
- Sql server
- Oracle
ed altri.
Nel nostro progetto, per semplicità, useremo il database H2, un dbms relazionale che si installa automaticamente, permette di salvare i dati in memoria o su disco, ed è adatto per piccoli progetti. E’ perfettamente integrato in Hibernate.
In questo progetto useremo la configurazione su disco, in modo da rendere persistenti i dati tra una esecuzione e l’altra.
Configurazione
Il progetto deve essere ancora configurato per funzionare.
Modifichiamo il pom.xml per avere queste dipendenze:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>e modifichiamo application.properties
spring.application.name=todo
spring.datasource.url=jdbc:h2:file:./data/todo-db
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
server.port=6501Come si vede sono state inserite le credenziali di H2 (utente sa e senza password), le logiche di persistenza (update quindi nessuna cancellazione tra una sessione e l’altra) e la cartella dove salvare il db.
Esecuzione
Se tutto è stato fatto correttamente, eseguire il progetto da VS Code oppure a mano:
mvn clean
mvn compile
mvn spring-boot:runTrattandosi di un Web Service, useremo CURL, uno strumento da linea di comando per inviare e ricevere dati da Web Service (fa anche molto altro).
curl http://localhost:6501risposta:
[]Non abbiamo infatti todo.
curl -X POST http://localhost:6501 -H "Content-Type: application/json" \
-d '{"title": "studiare Spring Boot"}'
Risposta:
{"completed":false,"id":65,"title":"studiare Spring Boot"}L’oggetto è stato creato.
Creiamone uno nuovo:
curl -X POST http://localhost:6501 -H "Content-Type: application/json" -d '{"title": "studiare Hibernate"}'
Se ora richiediamo di nuovo la lista avremo:
[{"completed":false,"id":65,"title":"studiare Spring Boot"},
{"completed":false,"id":66,"title":"studiare Hibernate"}]Proviamo ora a marcare un oggetto come completato:
curl -X PUT http://localhost:6501/66 -H "Content-Type: application/json" -d '{"completed": true}'La lista ora sarà
[{"completed":false,"id":65,"title":"studiare Spring Boot"},
{"completed":true,"id":66,"title":"studiare Hibernate"}]Infine cancelliamo il primo todo:
curl -X DELETE http://localhost:6501/65E la lista sarà ora:
[{"completed":true,"id":66,"title":"studiare Hibernate"}]Conclusioni
In questa lezione abbiamo visto come creare un semplice Web Service.
Abbiamo analizzato la struttura di una Web Application Spring Boot, e la sua suddivisione di controllers, services e repository/model.
Abbiamo approfondito come funziona la Dependency Injection e la persistenza con Jpa Hibernate.
Abbiamo infine visto l’esecuzione ed il test dell’applicazione con curl.
