2. Il server Spring 4
![]() |
Nell'architettura sopra riportata, ci occuperemo ora della realizzazione del servizio web / JSON costruito con il framework Spring 4. Lo scriveremo in diverse fasi:
- prima i livelli [business] e [DAO] (Data Access Object). Qui useremo Spring Data;
- poi il servizio web JSON senza autenticazione. Qui useremo Spring MVC;
- poi aggiungeremo il componente di autenticazione utilizzando Spring Security.
Inizieremo spiegando la struttura del database sottostante l'applicazione.
2.1. Il database
![]() |
Il database, di seguito denominato [ dbrdvmedecins], è un database MySQL5 con le seguenti tabelle:
![]() |
Gli appuntamenti sono gestiti dalle seguenti tabelle:
- [doctors]: contiene l'elenco dei medici dello studio;
- [clients]: contiene l'elenco dei pazienti dello studio;
- [slots]: contiene le fasce orarie di ciascun medico;
- [rv]: contiene l'elenco degli appuntamenti dei medici.
Le tabelle [roles], [users] e [users_roles] sono relative all'autenticazione. Per ora non ce ne occuperemo.
Le relazioni tra le tabelle che gestiscono gli appuntamenti sono le seguenti:
![]() |
- una fascia oraria appartiene a un medico – un medico ha 0 o più fasce orarie;
- un appuntamento collega sia un cliente che un medico tramite la fascia oraria del medico;
- un cliente ha 0 o più appuntamenti;
- una fascia oraria è associata a 0 o più appuntamenti (in giorni diversi).
2.1.1. La tabella [MEDECINS]
Contiene informazioni sui medici gestiti dall'applicazione [RdvMedecins].
![]() | ![]() |
- ID: numero ID che identifica il medico — chiave primaria della tabella
- VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
- LAST_NAME: il cognome del medico
- FIRST_NAME: nome del medico
- TITOLO: il titolo (Sig.ra, Sig.ra, Sig.)
2.1.2. La tabella [CLIENTS]
I clienti dei vari medici sono memorizzati nella tabella [CLIENTS]:
![]() | ![]() |
- ID: numero identificativo del cliente - chiave primaria della tabella
- VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
- COGNOME: il cognome del cliente
- NOME: il nome del cliente
- TITOLO: il loro titolo (Sig.ra, Sig.ra, Sig.)
2.1.3. La tabella [SLOTS]
Elenca le fasce orarie in cui sono disponibili gli appuntamenti:
![]() |
![]() |
- ID: numero identificativo della fascia oraria - chiave primaria della tabella (riga 8)
- VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
- DOCTOR_ID: numero ID che identifica il medico a cui appartiene questa fascia oraria – chiave esterna sulla colonna DOCTORS(ID).
- START_TIME: ora di inizio della fascia oraria
- MSTART: minuti di inizio della fascia oraria
- HFIN: ora di fine della fascia oraria
- MFIN: minuti di fine della fascia oraria
La seconda riga della tabella [SLOTS] (vedi [1] sopra) indica, ad esempio, che la fascia oraria n. 2 inizia alle 8:20 e termina alle 8:40 e appartiene al medico n. 1 (dott.ssa Marie PELISSIER).
2.1.4. La tabella [RV]
Elenca gli appuntamenti prenotati per ciascun medico:
![]() |
- ID: identificatore univoco dell'appuntamento – chiave primaria
- GIORNO: giorno dell'appuntamento
- SLOT_ID: fascia oraria dell'appuntamento – chiave esterna sul campo [ID] della tabella [SLOTS] – determina sia la fascia oraria che il medico coinvolto.
- CLIENT_ID: ID del cliente per il quale è stata effettuata la prenotazione – chiave esterna sul campo [ID] della tabella [CLIENTS]
Questa tabella ha un vincolo di unicità sui valori delle colonne unite (GIORNO, SLOT_ID):
Se una riga nella tabella [RV] ha il valore (DAY1, SLOT_ID1) per le colonne (DAY, SLOT_ID), questo valore non può comparire in nessun altro punto. In caso contrario, ciò significherebbe che sono stati prenotati due appuntamenti contemporaneamente per lo stesso medico. Dal punto di vista della programmazione Java, il driver JDBC del database genera un'eccezione SQLException quando ciò si verifica.
La riga con ID pari a 3 (vedi [1] sopra) indica che è stato prenotato un appuntamento per lo slot n. 20 e il cliente n. 4 il 23/08/2006. La tabella [SLOTS] ci dice che lo slot n. 20 corrisponde alla fascia oraria 16:20 – 16:40 e appartiene al medico n. 1 (Sig.ra Marie PELISSIER). La tabella [CLIENTS] ci dice che il cliente n. 4 è la sig.ra Brigitte BISTROU.
2.2. Introduzione a Spring Data
Implementeremo il livello [DAO] del progetto utilizzando Spring Data, un ramo dell'ecosistema Spring.
![]() |
Il sito web di Spring offre numerosi tutorial per iniziare a utilizzare Spring [http://spring.io/guides]. Ne useremo uno per introdurre Spring Data. A tal fine, utilizzeremo Spring Tool Suite (STS).
![]() |
- In [1], importiamo uno dei tutorial da [spring.io/guides];
![]() |
- In [2], selezioniamo il tutorial [Accessing Data Jpa], che illustra come accedere a un database utilizzando Spring Data;
- In [3], selezioniamo un progetto configurato da Maven;
- in [4], il tutorial è disponibile in due forme: [initial], che è una versione vuota da compilare seguendo il tutorial, oppure [complete], che è la versione finale del tutorial. Scegliamo quest'ultima;
- In [5], è possibile scegliere di visualizzare il tutorial in un browser;
- In [6], il progetto finale.
2.2.1. La configurazione Maven del progetto
Le dipendenze Maven del progetto sono configurate nel file [pom.xml]:
<groupId>org.springframework</groupId>
<artifactId>gs-accessing-data-jpa</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
<properties>
<!-- use UTF-8 for everything -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>hello.Application</start-class>
</properties>
- righe 5–9: definiscono un progetto Maven padre. Questo progetto definisce la maggior parte delle dipendenze del progetto. Potrebbero essere sufficienti, nel qual caso non vengono aggiunte dipendenze aggiuntive, oppure potrebbero non esserlo, nel qual caso vengono aggiunte le dipendenze mancanti;
- righe 12–15: definiscono una dipendenza da [spring-boot-starter-data-jpa]. Questo artefatto contiene le classi Spring Data;
- Righe 16–19: definiscono una dipendenza dal DBMS H2, che consente di creare e gestire database in memoria.
Diamo un'occhiata alle classi fornite da queste dipendenze:
![]() | ![]() | ![]() |
Ce ne sono molti:
- alcuni appartengono all'ecosistema Spring (quelli che iniziano con spring);
- altri appartengono all'ecosistema Hibernate (hibernate, jboss), di cui qui utilizziamo l'implementazione JPA;
- altri sono librerie di test (JUnit, Hamcrest);
- altri sono librerie di logging (log4j, logback, slf4j);
Le terremo tutte. Per un'applicazione di produzione, dovremmo tenere solo quelle necessarie.
Alla riga 26 del file [pom.xml], troviamo la riga:
<start-class>hello.Application</start-class>
Questa riga è collegata alle seguenti righe:
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Righe 6–9: Il [spring-boot-maven-plugin] consente di generare il JAR eseguibile dell'applicazione. La riga 26 del file [pom.xml] specifica quindi la classe eseguibile di questo JAR.
2.2.2. Il livello [JPA]
L'accesso al database viene gestito tramite un livello [JPA], la Java Persistence API:
![]() |
![]() |
L'applicazione è semplice e gestisce le entità [Cliente]. La classe [Cliente] fa parte del livello [JPA] ed è la seguente:
package hello;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String firstName;
private String lastName;
protected Customer() {
}
public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
}
}
Un cliente ha un ID [id], un nome [firstName] e un cognome [lastName]. Ogni istanza [Customer] rappresenta una riga in una tabella del database.
- riga 8: annotazione JPA che garantisce che la persistenza delle istanze [Customer] (Creazione, Lettura, Aggiornamento, Eliminazione) sarà gestita da un'implementazione JPA. In base alle dipendenze Maven, possiamo vedere che viene utilizzata l'implementazione JPA/Hibernate;
- Righe 11–12: annotazioni JPA che associano il campo [id] alla chiave primaria della tabella [Customer]. La riga 12 indica che l'implementazione JPA utilizzerà il metodo di generazione della chiave primaria specifico del DBMS in uso, in questo caso H2;
Non ci sono altre annotazioni JPA. Verranno quindi utilizzati i valori predefiniti:
- la tabella [Customer] prenderà il nome dalla classe, ovvero [Customer];
- le colonne di questa tabella prenderanno il nome dai campi della classe: [id, firstName, lastName], tenendo presente che nei nomi delle colonne della tabella non viene fatta distinzione tra maiuscole e minuscole;
Si noti che l'implementazione JPA utilizzata non viene mai denominata.
2.2.3. Il livello [DAO]
![]() |
![]() |
La classe [CustomerRepository] implementa il livello [DAO]. Il suo codice è il seguente:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
Si tratta quindi di un'interfaccia e non di una classe (riga 7). Essa estende l'interfaccia [CrudRepository], un'interfaccia Spring Data (riga 5). Questa interfaccia è parametrizzata da due tipi: il primo è il tipo degli elementi gestiti, in questo caso il tipo [Customer]; il secondo è il tipo della chiave primaria degli elementi gestiti, in questo caso un tipo [Long]. L'interfaccia [CrudRepository] è la seguente:
package org.springframework.data.repository;
import java.io.Serializable;
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> save(Iterable<S> entities);
T findOne(ID id);
boolean exists(ID id);
Iterable<T> findAll();
Iterable<T> findAll(Iterable<ID> ids);
long count();
void delete(ID id);
void delete(T entity);
void delete(Iterable<? extends T> entities);
void deleteAll();
}
Questa interfaccia definisce le operazioni CRUD (Create – Read – Update – Delete) che possono essere eseguite su un tipo JPA T:
- Riga 8: Il metodo
saveviene utilizzato per rendere persistente un'entitàTnel database. Rende persistente l'entità utilizzando la chiave primaria assegnatale dal DBMS. Consente inoltre di aggiornare un'entitàTidentificata dalla sua chiave primariaid. La scelta tra queste due azioni dipende dal valore della chiave primaria id: se è nullo, viene eseguita l'operazione di persistenza; altrimenti, viene eseguita l'operazione di aggiornamento; - riga 10: come sopra, ma per un elenco di entità;
- riga 12: il metodo findOne recupera un'entità T identificata dalla sua chiave primaria id;
- riga 22: il metodo delete consente di eliminare un'entità T identificata dalla sua chiave primaria id;
- righe 24–28: varianti del metodo [delete];
- riga 16: il metodo [findAll] recupera tutte le entità T persistenti;
- riga 18: come sopra, ma limitato alle entità per le quali è stato fornito un elenco di identificatori;
Torniamo all'interfaccia [CustomerRepository]:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
- La riga 9 consente di recuperare un [Cliente] in base al suo [cognome];
E questo è tutto per il livello [DAO]. Non esiste una classe di implementazione per l'interfaccia precedente. Viene generata in fase di esecuzione da [Spring Data]. I metodi dell'interfaccia [CrudRepository] vengono implementati automaticamente. Per i metodi aggiunti all'interfaccia [CustomerRepository], dipende. Torniamo alla definizione di [Customer]:
private long id;
private String firstName;
private String lastName;
Il metodo alla riga 9 viene implementato automaticamente da [Spring Data] poiché fa riferimento al campo [lastName] (riga 3) di [Customer]. Quando incontra un metodo [findBySomething] nell'interfaccia da implementare, Spring Data lo implementa utilizzando la seguente query JPQL (Java Persistence Query Language):
Pertanto, il tipo T deve avere un campo denominato [something]. Di conseguenza, il metodo
sarà implementato con un codice simile al seguente:
return [em].createQuery("select c from Customer c where c.lastName=:value").setParameter("value",lastName).getResultList()
dove [em] si riferisce al contesto di persistenza JPA. Ciò è possibile solo se la classe [Customer] ha un campo denominato [lastName], come in questo caso.
In conclusione, in casi semplici, Spring Data ci permette di implementare il livello [DAO] con un'interfaccia semplice.
2.2.4. Il livello [console]
![]() |
![]() |
La classe [Application] è la seguente:
package hello;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
// save a couple of customers
repository.save(new Customer("Jack", "Bauer"));
repository.save(new Customer("Chloe", "O'Brian"));
repository.save(new Customer("Kim", "Bauer"));
repository.save(new Customer("David", "Palmer"));
repository.save(new Customer("Michelle", "Dessler"));
// fetch all customers
Iterable<Customer> customers = repository.findAll();
System.out.println("Customers found with findAll():");
System.out.println("-------------------------------");
for (Customer customer : customers) {
System.out.println(customer);
}
System.out.println();
// fetch an individual customer by ID
Customer customer = repository.findOne(1L);
System.out.println("Customer found with findOne(1L):");
System.out.println("--------------------------------");
System.out.println(customer);
System.out.println();
// fetch customers by last name
List<Customer> bauers = repository.findByLastName("Bauer");
System.out.println("Customer found with findByLastName('Bauer'):");
System.out.println("--------------------------------------------");
for (Customer bauer : bauers) {
System.out.println(bauer);
}
context.close();
}
}
- Riga 10: indica che la classe viene utilizzata per configurare Spring. Le versioni recenti di Spring possono infatti essere configurate in Java anziché in XML. Entrambi i metodi possono essere utilizzati contemporaneamente. Nel codice di una classe annotata con [Configuration], si trovano normalmente i bean Spring, ovvero le definizioni delle classi da istanziare. Qui non sono definiti bean. È importante notare che quando si lavora con un DBMS, devono essere definiti vari bean Spring:
- un [EntityManagerFactory] che definisce l'implementazione JPA da utilizzare,
- un [DataSource] che definisce la fonte dati da utilizzare,
- un [TransactionManager] che definisce il gestore delle transazioni da utilizzare;
Qui, nessuno di questi bean è definito.
- Riga 11: L'annotazione [EnableAutoConfiguration] è un'annotazione del progetto [Spring Boot] (righe 5–6). Questa annotazione indica a Spring Boot, tramite la classe [SpringApplication] (riga 16), di configurare l'applicazione in base alle librerie presenti nel suo classpath. Poiché le librerie Hibernate si trovano nel classpath, il bean [entityManagerFactory] verrà implementato utilizzando Hibernate. Poiché la libreria DBMS H2 si trova nel classpath, il bean [dataSource] verrà implementato utilizzando H2. Nel bean [dataSource] dobbiamo anche definire il nome utente e la password. In questo caso, Spring Boot utilizzerà l'amministratore H2 predefinito, che non ha password. Poiché la libreria [spring-tx] si trova nel classpath, verrà utilizzato il gestore delle transazioni di Spring.
Inoltre, la directory contenente la classe [Application] verrà scansionata alla ricerca di bean riconosciuti implicitamente da Spring o definiti esplicitamente dalle annotazioni Spring. Pertanto, verranno ispezionate le classi [Customer] e [CustomerRepository]. Poiché la prima ha l'annotazione [@Entity], verrà catalogata come entità da gestire da Hibernate. Poiché la seconda estende l'interfaccia [CrudRepository], verrà registrata come bean Spring.
Esaminiamo le righe 16–17 del codice:
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
- Riga 1: viene eseguito il metodo statico [run] della classe [SpringApplication] nel progetto Spring Boot. Il suo parametro è la classe che presenta un'annotazione [Configuration] o [EnableAutoConfiguration]. A questo punto avrà luogo tutto ciò che è stato spiegato in precedenza. Il risultato è un contesto applicativo Spring, ovvero un insieme di bean gestiti da Spring;
- riga 17: richiediamo un bean che implementi l'interfaccia [CustomerRepository] da questo contesto Spring. Qui, recuperiamo la classe generata da Spring Data per implementare questa interfaccia.
Le operazioni seguenti utilizzano semplicemente i metodi del bean che implementa l'interfaccia [CustomerRepository]. Si noti alla riga 50 che il contesto viene chiuso. L'output della console è il seguente:
- righe 1-8: il logo del progetto Spring Boot;
- riga 9: viene eseguita la classe [hello.Application];
- riga 10: [AnnotationConfigApplicationContext] è una classe che implementa l'interfaccia [ApplicationContext] di Spring. Si tratta di un contenitore di bean;
- riga 11: il bean [entityManagerFactory] è implementato utilizzando la classe [LocalContainerEntityManagerFactory], una classe Spring;
- riga 12: compare [hibernate]. Si tratta dell'implementazione JPA che è stata scelta;
- riga 19: un dialetto Hibernate è la variante SQL da utilizzare con il DBMS. Qui, il dialetto [H2Dialect] indica che Hibernate funzionerà con il DBMS H2;
- righe 22–24: viene creata la tabella [CUSTOMER]. Ciò significa che Hibernate è stato configurato per generare tabelle dalle definizioni JPA, in questo caso la definizione JPA della classe [Customer];
- righe 27–32: log di Hibernate che mostrano l'inserimento di righe nella tabella [CUSTOMER]. Ciò significa che Hibernate è stato configurato per generare log;
- righe 35–39: i cinque clienti inseriti;
- righe 42–44: risultato del metodo [findOne] dell'interfaccia;
- righe 47–50: risultati del metodo [findByLastName];
- righe 51 e seguenti: log relativi alla chiusura del contesto Spring.
2.2.5. Configurazione manuale del progetto Spring Data
Duplichiamo il progetto precedente nel progetto [gs-accessing-data-jpa-2]:
![]() |
In questo nuovo progetto non faremo affidamento sulla configurazione automatica fornita da Spring Boot. La configureremo manualmente. Ciò può essere utile se le configurazioni predefinite non soddisfano le nostre esigenze.
Per prima cosa, specificheremo le dipendenze necessarie nel file [pom.xml]:
<dependencies>
<!-- Spring Core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<!-- Spring transactions -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.5.2.RELEASE</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>1.0.2.RELEASE</version>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.4.Final</version>
</dependency>
<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.178</version>
</dependency>
<!-- Commons DBCP -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
<version>1.6</version>
</dependency>
</dependencies>
- righe 3–17: librerie principali di Spring;
- righe 19–28: librerie Spring per la gestione delle transazioni del database;
- righe 30–34: Spring Data utilizzato per accedere al database;
- righe 36–40: Spring Boot per avviare l'applicazione;
- righe 48–52: il DBMS H2;
- righe 54–63: i database sono spesso utilizzati con pool di connessioni aperte, che evitano di aprire e chiudere ripetutamente le connessioni. In questo caso, l'implementazione utilizzata è quella di [commons-dbcp];
Sempre in [pom.xml], modifichiamo il nome della classe eseguibile:
<properties>
...
<start-class>demo.console.Main</start-class>
</properties>
Nel nuovo progetto, l'entità [Customer] e l'interfaccia [CustomerRepository] rimangono invariate. Modificheremo la classe [Application], che verrà suddivisa in due classi:
- [Config], che sarà la classe di configurazione:
- [Main], che sarà la classe eseguibile;
![]() |
La classe eseguibile [Main] è la stessa di prima, senza le annotazioni di configurazione:
package demo.console;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
...
context.close();
}
}
- riga 12: la classe [Main] non presenta più alcuna annotazione di configurazione;
- riga 16: l'applicazione viene avviata con Spring Boot. Il parametro [Config.class] è la nuova classe di configurazione del progetto;
La classe [Config] che configura il progetto è la seguente:
package demo.config;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
//@ComponentScan(basePackages = { "demo" })
//@EntityScan(basePackages = { "demo.entities" })
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "demo.repositories" })
@Configuration
public class Config {
// h2 data source
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:./demo");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
// the provider JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(true);
hibernateJpaVendorAdapter.setDatabase(Database.H2);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan("demo.entities");
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// Transaction manager
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- riga 22: l'annotazione [@Configuration] rende la classe [Config] una classe di configurazione Spring;
- riga 21: l'annotazione [@EnableJpaRepositories] specifica le directory in cui si trovano le interfacce [CrudRepository] di Spring Data. Queste interfacce diventeranno componenti Spring e saranno disponibili nel suo contesto;
- riga 20: l'annotazione [@EnableTransactionManagement] indica che i metodi delle interfacce [CrudRepository] devono essere eseguiti all'interno di una transazione;
- riga 19: l'annotazione [@EntityScan] specifica le directory in cui devono essere cercate le entità JPA. Qui è stata commentata perché questa informazione è stata fornita esplicitamente alla riga 50. Questa annotazione dovrebbe essere presente se si utilizza la modalità [@EnableAutoConfiguration] e le entità JPA non si trovano nella stessa directory della classe di configurazione;
- riga 18: l'annotazione [@ComponentScan] consente di elencare le directory in cui cercare i componenti Spring. I componenti Spring sono classi contrassegnate con annotazioni Spring quali @Service, @Component, @Controller, ecc. Qui non ce ne sono altri oltre a quelli definiti all'interno della classe [Config], quindi l'annotazione è stata commentata;
- Righe 25–33: definiscono l'origine dati, il database H2. È l'annotazione @Bean alla riga 25 che rende l'oggetto creato da questo metodo un componente gestito da Spring. Il nome del metodo qui può essere qualsiasi cosa. Tuttavia, deve essere denominato [dataSource] se l'EntityManagerFactory alla riga 47 è assente e definita tramite configurazione automatica;
- riga 29: il database si chiamerà [demo] e verrà generato nella cartella del progetto;
- Righe 36–43: definiscono l'implementazione JPA utilizzata, in questo caso un'implementazione Hibernate. Il nome del metodo qui può essere qualsiasi cosa;
- riga 39: nessun log SQL;
- riga 30: il database verrà creato se non esiste;
- righe 46–54: definiscono l'EntityManagerFactory che gestirà la persistenza JPA. Il metodo deve essere denominato [entityManagerFactory];
- riga 47: il metodo riceve due parametri dei tipi dei due bean definiti in precedenza. Questi saranno poi costruiti e iniettati da Spring come parametri del metodo;
- riga 49: imposta l'implementazione JPA da utilizzare;
- riga 50: specifica le directory in cui si trovano le entità JPA;
- riga 51: imposta l'origine dati da gestire;
- righe 57–62: il gestore delle transazioni. Il metodo deve essere denominato [transactionManager]. Riceve il bean delle righe 46–54 come parametro;
- riga 60: il gestore delle transazioni è associato all'EntityManagerFactory;
I metodi precedenti possono essere definiti in qualsiasi ordine.
L'esecuzione del progetto produce gli stessi risultati. Nella cartella del progetto compare un nuovo file, il file del database H2:
![]() |
Finalmente possiamo fare a meno di Spring Boot. Creiamo una seconda classe eseguibile [Main2]:
![]() |
La classe [Main2] contiene il seguente codice:
package demo.console;
import java.util.List;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main2 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
....
context.close();
}
}
- Riga 15: La classe di configurazione [Config] viene ora utilizzata dalla classe Spring [AnnotationConfigApplicationContext]. Come si vede alla riga 5, non vi è più alcuna dipendenza da Spring Boot.
L'esecuzione produce gli stessi risultati di prima.
2.2.6. Creazione di un archivio eseguibile
Per creare un archivio eseguibile del progetto, procedere come segue:
![]() |
- in [1]: creare una configurazione di runtime;
- in [2]: di tipo [Applicazione Java]
- in [3]: specificare il progetto da eseguire (utilizzare il pulsante Sfoglia);
- in [4]: specificare la classe da eseguire;
- in [5]: il nome della configurazione di esecuzione — può essere qualsiasi cosa;
![]() |
- in [6]: esportare il progetto;
- in [7]: come archivio JAR eseguibile;
- in [8]: specificare il percorso e il nome del file eseguibile da creare;
- in [9]: il nome della configurazione di esecuzione creata in [5];
Una volta fatto ciò, aprire una console nella cartella contenente l'archivio eseguibile:
L'archivio viene eseguito come segue:
.....\dist>java -jar gs-accessing-data-jpa-2.jar
I risultati visualizzati nella console sono i seguenti:
2.2.7. Crea un nuovo progetto Spring Data
Per creare un modello di progetto Spring Data, segui questi passaggi:
![]() |
- In [1], crea un nuovo progetto;
- in [2]: seleziona [Spring Starter Project];
- Il progetto generato sarà un progetto Maven. In [3], specificare il nome del gruppo di progetto;
- In [4], specificare il nome dell'artefatto (un file JAR in questo caso) che verrà creato al momento della compilazione del progetto;
- in [5]: specificare il pacchetto della classe eseguibile che verrà creata nel progetto;
- in [6]: il nome Eclipse del progetto – può essere qualsiasi cosa (non deve necessariamente essere uguale a [4]);
- in [7]: specificare che si sta creando un progetto con un livello [JPA]. Le dipendenze richieste per un progetto di questo tipo saranno quindi incluse nel file [pom.xml];
![]() |
- in [8]: il progetto creato;
Il file [pom.xml] include le dipendenze necessarie per un progetto JPA:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- righe 9–12: dipendenze richieste per JPA — includeranno [Spring Data];
- righe 13–17: dipendenze necessarie per i test JUnit integrati con Spring;
La classe eseguibile [Application] non fa nulla ma è preconfigurata:
package istia.st;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
La classe di test [ApplicationTests] non fa nulla, ma è preconfigurata:
package istia.st;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
@Test
public void contextLoads() {
}
}
- riga 9: l'annotazione [@SpringApplicationConfiguration] consente di utilizzare il file di configurazione [Application]. La classe di test potrà così beneficiare di tutti i bean definiti in questo file;
- riga 8: l'annotazione [@RunWith] consente l'integrazione di Spring con JUnit: la classe può essere eseguita come test JUnit. [@RunWith] è un'annotazione JUnit (riga 4), mentre la classe [SpringJUnit4ClassRunner] è una classe Spring (riga 6);
Ora che disponiamo di uno scheletro di applicazione JPA, possiamo completarlo per scrivere il livello di persistenza lato server della nostra applicazione di gestione degli appuntamenti.
2.3. Il progetto server di Eclipse
![]() |
![]() |
I componenti principali del progetto sono i seguenti:
- [pom.xml]: il file di configurazione Maven del progetto;
- [rdvmedecins.entities]: le entità JPA;
- [rdvmedecins.repositories]: interfacce Spring Data per l'accesso alle entità JPA;
- [rdvmedecins.metier]: il livello [business];
- [rdvmedecins.domain]: le entità gestite dal livello [business];
- [rdvmdecins.config]: le classi di configurazione del livello di persistenza;
- [rdvmedecins.boot]: un'applicazione console di base;
2.4. La configurazione Maven
![]() | ![]() | ![]() |
Il file [pom.xml] del progetto è il seguente:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
</dependencies>
<properties>
<!-- use UTF-8 for everything -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>istia.st.spring.data.main.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>org.jboss.repository.releases</id>
<name>JBoss Maven Release Repository</name>
<url>https://repository.jboss.org/nexus/content/repositories/releases</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
- righe 8–12: Il progetto si basa sul progetto padre [spring-boot-starter-parent]. Per le dipendenze già presenti nel progetto padre, non viene specificata alcuna versione. Verrà utilizzata la versione definita nel progetto padre. Le altre dipendenze sono dichiarate come di consueto;
- righe 14–17: per Spring Data;
- righe 18–22: per i test JUnit;
- righe 23–26: driver JDBC per il DBMS MySQL5;
- righe 27–34: pool di connessioni Commons DBCP;
- righe 35–38: libreria Jackson per la gestione di JSON;
- righe 39–43: libreria Google Collections;
La versione 1.1.0.RC1 di [spring-boot-starter-parent] utilizza le seguenti versioni delle librerie:
2.5. Entità JPA
![]() |
Le entità JPA sono gli oggetti che incapsulano le righe nelle tabelle del database.
![]() |
La classe [AbstractEntity] è la classe padre delle entità [Person, Slot, Appointment]. La sua definizione è la seguente:
package rdvmedecins.entities;
import java.io.Serializable;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
@MappedSuperclass
public class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
protected Long id;
@Version
protected Long version;
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
// initialization
public AbstractEntity build(Long id, Long version) {
this.id = id;
this.version = version;
return this;
}
@Override
public boolean equals(Object entity) {
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id == other.id;
}
// getters and setters
..
}
- riga 11: l'annotazione [@MappedSuperclass] indica che la classe annotata è un genitore delle entità JPA [@Entity];
- righe 15–17: definiscono la chiave primaria [id] per ciascuna entità. È l'annotazione [@Id] a rendere il campo [id] una chiave primaria. L'annotazione [@GeneratedValue(strategy = GenerationType.AUTO)] indica che il valore di questa chiave primaria viene generato dal DBMS e che non viene imposta alcuna modalità di generazione;
- Righe 18–19: definiscono la versione di ciascuna entità. L'implementazione JPA incrementerà questo numero di versione ogni volta che l'entità viene modificata. Questo numero viene utilizzato per impedire aggiornamenti simultanei dell'entità da parte di due utenti diversi: due utenti, U1 e U2, leggono l'entità E con un numero di versione pari a V1. U1 modifica E e salva questa modifica nel database: il numero di versione passa quindi a V1+1. U2 modifica a sua volta E e salva questa modifica nel database: riceverà un'eccezione perché la sua versione (V1) differisce da quella nel database (V1+1);
- righe 29–33: il metodo [build] inizializza i due campi di [AbstractEntity]. Questo metodo restituisce un riferimento all'istanza di [AbstractEntity] così inizializzata;
- righe 36–44: il metodo [equals] della classe viene ridefinito: due entità sono considerate uguali se hanno lo stesso nome di classe e lo stesso identificatore id;
L'entità [Person] è la classe padre delle entità [Doctor] e [Client]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
@MappedSuperclass
public class Personne extends AbstractEntity {
private static final long serialVersionUID = 1L;
// attributes of a person
@Column(length = 5)
private String titre;
@Column(length = 20)
private String nom;
@Column(length = 20)
private String prenom;
// default builder
public Personne() {
}
// builder with parameters
public Personne(String titre, String nom, String prenom) {
this.titre = titre;
this.nom = nom;
this.prenom = prenom;
}
// toString
public String toString() {
return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
}
// getters and setters
...
}
- riga 6: l'annotazione [@MappedSuperclass] indica che la classe annotata è una classe padre delle entità JPA [@Entity];
- righe 10–15: una persona ha un titolo (Ms.), un nome (Jacqueline) e un cognome (Tatou). Non vengono fornite informazioni sulle colonne della tabella. Per impostazione predefinita, avranno quindi gli stessi nomi dei campi;
L'entità [Medecin] è la seguente:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "medecins")
public class Medecin extends Personne {
private static final long serialVersionUID = 1L;
// default builder
public Medecin() {
}
// builder with parameters
public Medecin(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
public String toString() {
return String.format("Medecin[%s]", super.toString());
}
}
- riga 6: la classe è un'entità JPA;
- riga 7: associata alla tabella [DOCTORS] nel database;
- riga 8: l'entità [Doctor] deriva dall'entità [Person];
Un medico può essere inizializzato come segue:
Se vogliamo anche assegnargli un ID e una versione, possiamo scrivere:
dove il metodo [build] è quello definito in [AbstractEntity].
L'entità [Client] è la seguente:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "clients")
public class Client extends Personne {
private static final long serialVersionUID = 1L;
// default builder
public Client() {
}
// builder with parameters
public Client(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
// identity
public String toString() {
return String.format("Client[%s]", super.toString());
}
}
- riga 6: la classe è un'entità JPA;
- riga 7: associata alla tabella [CLIENTS] nel database;
- riga 8: l'entità [Client] deriva dall'entità [Person];
L'entità [TimeSlot] è la seguente:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of a RV slot
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// a slot is linked to a doctor
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// foreign key
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
// default builder
public Creneau() {
}
// builder with parameters
public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
this.medecin = medecin;
this.hdebut = hdebut;
this.mdebut = mdebut;
this.hfin = hfin;
this.mfin = mfin;
}
// toString
public String toString() {
return String.format("Créneau[%d, %d, %d, %d:%d, %d:%d]", id, version, idMedecin, hdebut, mdebut, hfin, mfin);
}
// foreign key
public long getIdMedecin() {
return idMedecin;
}
// setters - getters
...
}
- riga 10: la classe è un'entità JPA;
- riga 11: associata alla tabella [CRENEAUX] nel database;
- riga 12: l'entità [Creneau] deriva dall'entità [AbstractEntity] e quindi eredita i campi [id] e [version];
- riga 16: ora di inizio dello slot (14);
- riga 17: minuti di inizio dello slot (20);
- riga 18: ora di fine dello slot (14);
- riga 19: minuti di fine dello slot (40);
- righe 22–24: il medico a cui appartiene lo slot. La tabella [CRENEAUX] ha una chiave esterna sulla tabella [MEDECINS]. Questa relazione è rappresentata dalle righe 22–24;
- Riga 22: l'annotazione [@ManyToOne] indica una relazione molti-a-uno (slot) verso uno (medico). L'attributo [fetch=FetchType.LAZY] specifica che quando un'entità [Creneau] viene richiesta dal contesto di persistenza e deve essere recuperata dal database, l'entità [Medecin] non viene recuperata insieme ad essa. Il vantaggio di questa modalità è che l'entità [Doctor] viene recuperata solo se lo sviluppatore la richiede. Ciò consente di risparmiare memoria e migliora le prestazioni;
- riga 23: specifica il nome della colonna della chiave esterna nella tabella [CRENEAUX];
- Righe 27–28: la chiave esterna nella tabella [MEDECINS];
- riga 27: la colonna [ID_MEDECIN] è già stata utilizzata alla riga 23. Ciò significa che può essere modificata in due modi diversi, cosa non consentita dallo standard JPA. Aggiungiamo quindi gli attributi [insertable = false, updatable = false], il che significa che la colonna può essere solo letta;
L'entità [Rv] è la seguente:
package rdvmedecins.entities;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of an Rv
@Temporal(TemporalType.DATE)
private Date jour;
// an appointment is linked to a customer
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// an appointment is linked to a time slot
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// foreign keys
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
// default builder
public Rv() {
}
// with parameters
public Rv(Date jour, Client client, Creneau creneau) {
this.jour = jour;
this.client = client;
this.creneau = creneau;
}
// toString
public String toString() {
return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
}
// foreign keys
public long getIdCreneau() {
return idCreneau;
}
public long getIdClient() {
return idClient;
}
// getters and setters
...
}
- riga 14: la classe è un'entità JPA;
- riga 15: associata alla tabella [RV] nel database;
- riga 16: l'entità [Rv] deriva dall'entità [AbstractEntity] e quindi eredita i campi [id] e [version];
- riga 21: la data dell'appuntamento;
- riga 20: il tipo Java [Date] contiene sia una data che un'ora. Qui specifichiamo che viene utilizzata solo la data;
- righe 24–26: il cliente per il quale è stato fissato questo appuntamento. La tabella [RV] ha una chiave esterna sulla tabella [CLIENTS]. Questa relazione è rappresentata dalle righe 24–26;
- righe 29–31: la fascia oraria dell'appuntamento. La tabella [RV] ha una chiave esterna sulla tabella [CRENEAUX]. Questa relazione è rappresentata dalle righe 29–31;
- righe 34–35: la chiave esterna [idClient];
- righe 36–37: la chiave esterna [idCreneau];
2.6. Il livello [DAO]
![]() |
Implementeremo il livello [DAO] utilizzando Spring Data:
![]() |
Il livello [DAO] è implementato utilizzando quattro interfacce Spring Data:
- [ClientRepository]: fornisce l'accesso alle entità JPA [Client];
- [CreneauRepository]: fornisce l'accesso alle entità JPA [Creneau];
- [MedecinRepository]: fornisce l'accesso alle entità JPA [Medecin];
- [RvRepository]: fornisce l'accesso alle entità JPA [Rv];
L'interfaccia [MedecinRepository] è la seguente:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Medecin;
public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
- Riga 7: L'interfaccia [MedecinRepository] eredita semplicemente i metodi dall'interfaccia [CrudRepository] senza aggiungerne altri;
L'interfaccia [ClientRepository] è la seguente:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Client;
public interface ClientRepository extends CrudRepository<Client, Long> {
}
- Riga 7: L'interfaccia [ClientRepository] eredita semplicemente i metodi dall'interfaccia [CrudRepository] senza aggiungerne altri;
L'interfaccia [CreneauRepository] è la seguente:
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Creneau;
public interface CreneauRepository extends CrudRepository<Creneau, Long> {
// list of physician slots
@Query("select c from Creneau c where c.medecin.id=?1")
Iterable<Creneau> getAllCreneaux(long idMedecin);
}
- riga 8: l'interfaccia [CreneauRepository] eredita i metodi dell'interfaccia [CrudRepository];
- righe 10-11: il metodo [getAllCreneaux] recupera le fasce orarie disponibili di un medico;
- riga 11: il parametro è l'ID del medico. Il risultato è un elenco di fasce orarie sotto forma di oggetto [Iterable<Creneau>];
- riga 10: l'annotazione [@Query] viene utilizzata per specificare la query JPQL (Java Persistence Query Language) che implementa il metodo. Il parametro [?1] verrà sostituito dal parametro [idMedecin] del metodo;
L'interfaccia [RvRepository] è la seguente:
package rdvmedecins.repositories;
import java.util.Date;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Rv;
public interface RvRepository extends CrudRepository<Rv, Long> {
@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
}
- Riga 10: L'interfaccia [RvRepository] eredita i metodi dell'interfaccia [CrudRepository];
- Righe 12–13: Il metodo [getRvMedecinJour] recupera gli appuntamenti di un medico per un determinato giorno;
- riga 13: I parametri sono l'ID del medico e il giorno. Il risultato è un elenco di appuntamenti sotto forma di un oggetto [Iterable<Rv>];
- riga 12: l'annotazione [@Query] consente di specificare la query JPQL che implementa il metodo. Il parametro [?1] sarà sostituito dal parametro [idMedecin] del metodo, mentre il parametro [?2] sarà sostituito dal parametro [jour] del metodo. La seguente query JPQL non è sufficiente:
perché i campi della classe Rv, di tipo [Client] e [Creneau], vengono recuperati in modalità [FetchType.LAZY], il che significa che devono essere esplicitamente richiesti per essere ottenuti. Ciò avviene nella query JPQL utilizzando la sintassi [left join fetch entity], che richiede l'esecuzione di un join con la tabella a cui fa riferimento la chiave esterna per recuperare l'entità di riferimento;
2.7. Il livello [business]
![]() |
![]() |
- [IMetier] è l'interfaccia del livello [business] e [Metier] ne è l'implementazione;
- [DoctorDailySchedule] e [DoctorDailySlot] sono due entità di business;
2.7.1. Le entità
L'entità [DoctorTimeSlot] associa una fascia oraria a qualsiasi appuntamento prenotato all'interno di quella fascia:
package rdvmedecins.domain;
import java.io.Serializable;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// fields
private Creneau creneau;
private Rv rv;
// manufacturers
public CreneauMedecinJour() {
}
public CreneauMedecinJour(Creneau creneau, Rv rv) {
this.creneau=creneau;
this.rv=rv;
}
// toString
@Override
public String toString() {
return String.format("[%s %s]", creneau, rv);
}
// getters and setters
...
}
- riga 12: la fascia oraria;
- riga 13: l'appuntamento, se presente – null in caso contrario;
L'entità [AgendaMedecinJour] rappresenta l'agenda di un medico per un determinato giorno, ovvero l'elenco dei suoi appuntamenti:
package rdvmedecins.domain;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.entities.Medecin;
public class AgendaMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// fields
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
// manufacturers
public AgendaMedecinJour() {
}
public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
this.medecin = medecin;
this.jour = jour;
this.creneauxMedecinJour = creneauxMedecinJour;
}
public String toString() {
StringBuffer str = new StringBuffer("");
for (CreneauMedecinJour cr : creneauxMedecinJour) {
str.append(" ");
str.append(cr.toString());
}
return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
}
// getters and setters
...
}
- riga 13: il medico;
- riga 14: il giorno del calendario;
- riga 15: le loro fasce orarie disponibili, con o senza appuntamento;
2.7.2. Il servizio
L'interfaccia del livello [aziendale] è la seguente:
package rdvmedecins.metier;
import java.util.Date;
import java.util.List;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
public interface IMetier {
// customer list
public List<Client> getAllClients();
// list of doctors
public List<Medecin> getAllMedecins();
// list of physician slots
public List<Creneau> getAllCreneaux(long idMedecin);
// list of doctor's appointments on a given day
public List<Rv> getRvMedecinJour(long idMedecin, Date jour);
// find a customer identified by its id
public Client getClientById(long id);
// find a customer identified by its id
public Medecin getMedecinById(long id);
// find an Rv identified by its id
public Rv getRvById(long id);
// find a time slot identified by its id
public Creneau getCreneauById(long id);
// add a RV to the list
public Rv ajouterRv(Date jour, Creneau créneau, Client client);
// delete a RV
public void supprimerRv(Rv rv);
// job
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);
}
I commenti spiegano il ruolo di ciascun metodo.
L'implementazione dell'interfaccia [IMetier] è la seguente classe [Metier]:
package rdvmedecins.metier;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.domain.CreneauMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.repositories.ClientRepository;
import rdvmedecins.repositories.CreneauRepository;
import rdvmedecins.repositories.MedecinRepository;
import rdvmedecins.repositories.RvRepository;
import com.google.common.collect.Lists;
@Service("métier")
public class Metier implements IMetier {
// repositories
@Autowired
private MedecinRepository medecinRepository;
@Autowired
private ClientRepository clientRepository;
@Autowired
private CreneauRepository creneauRepository;
@Autowired
private RvRepository rvRepository;
// interface implementation
@Override
public List<Client> getAllClients() {
return Lists.newArrayList(clientRepository.findAll());
}
@Override
public List<Medecin> getAllMedecins() {
return Lists.newArrayList(medecinRepository.findAll());
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return Lists.newArrayList(creneauRepository.getAllCreneaux(idMedecin));
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return Lists.newArrayList(rvRepository.getRvMedecinJour(idMedecin, jour));
}
@Override
public Client getClientById(long id) {
return clientRepository.findOne(id);
}
@Override
public Medecin getMedecinById(long id) {
return medecinRepository.findOne(id);
}
@Override
public Rv getRvById(long id) {
return rvRepository.findOne(id);
}
@Override
public Creneau getCreneauById(long id) {
return creneauRepository.findOne(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau créneau, Client client) {
return rvRepository.save(new Rv(jour, client, créneau));
}
@Override
public void supprimerRv(Rv rv) {
rvRepository.delete(rv.getId());
}
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
...
}
}
- riga 24: l'annotazione [@Service] è un'annotazione Spring che rende la classe annotata un componente gestito da Spring. È possibile assegnare o meno un nome a un componente. Questo è denominato [business];
- riga 25: la classe [Metier] implementa l'interfaccia [IMetier];
- riga 28: l'annotazione [@Autowired] è un'annotazione Spring. Il valore del campo annotato in questo modo verrà inizializzato (iniettato) da Spring con il riferimento a un componente Spring del tipo o del nome specificato. Qui, l'annotazione [@Autowired] non specifica un nome. Pertanto, verrà eseguita l'iniezione basata sul tipo;
- riga 29: il campo [medecinRepository] verrà inizializzato con il riferimento a un componente Spring di tipo [MedecinRepository]. Questo sarà il riferimento alla classe generata da Spring Data per implementare l'interfaccia [MedecinRepository] che abbiamo già presentato;
- righe 30–35: questo processo viene ripetuto per le altre tre interfacce discusse;
- righe 39–41: implementazione del metodo [getAllClients];
- riga 40: utilizziamo il metodo [findAll] dell'interfaccia [ClientRepository]. Questo metodo restituisce un tipo [Iterable<Client>], che convertiamo in un [List<Client>] utilizzando il metodo statico [Lists.newArrayList]. La classe [Lists] è definita nella libreria Google Guava. Nel file [pom.xml], questa dipendenza è stata importata:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
- righe 38–86: i metodi dell'interfaccia [IMetier] sono implementati utilizzando classi del livello [DAO];
Solo il metodo alla riga 88 è specifico del livello [business]. È stato collocato qui perché esegue una logica di business che va oltre il semplice accesso ai dati. Senza questo metodo, non ci sarebbe motivo di creare un livello [business]. Il metodo [getAgendaMedecinJour] è il seguente:
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
// list of doctor's time slots
List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
// list of bookings for the same doctor on the same day
List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
// a dictionary is created from the Rvs taken
Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
for (Rv resa : reservations) {
hReservations.put(resa.getCreneau().getId(), resa);
}
// create the agenda for the requested day
AgendaMedecinJour agenda = new AgendaMedecinJour();
// the doctor
agenda.setMedecin(getMedecinById(idMedecin));
// the day
agenda.setJour(jour);
// reservation slots
CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
agenda.setCreneauxMedecinJour(creneauxMedecinJour);
// filling reservation slots
for (int i = 0; i < creneauxHoraires.size(); i++) {
// line i agenda
creneauxMedecinJour[i] = new CreneauMedecinJour();
// time slot
Creneau créneau = creneauxHoraires.get(i);
long idCreneau = créneau.getId();
creneauxMedecinJour[i].setCreneau(créneau);
// is the slot free or reserved?
if (hReservations.containsKey(idCreneau)) {
// the slot is occupied - we note the resa
Rv resa = hReservations.get(idCreneau);
creneauxMedecinJour[i].setRv(resa);
}
}
// we return the result
return agenda;
}
Si invitano i lettori a leggere i commenti. L'algoritmo è il seguente:
- recuperare tutte le fasce orarie per il medico specificato;
- recuperare tutti i suoi appuntamenti per il giorno specificato;
- con queste informazioni, possiamo determinare se una fascia oraria è disponibile o prenotata;
2.8. Configurazione del progetto
![]() |
La classe [DomainAndPersistenceConfig] configura l'intero progetto:
package rdvmedecins.config;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.orm.jpa.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
// the MySQL data source
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
return dataSource;
}
// provider JPA - not required if you're happy with the default values used by Spring boot
// here we define it to enable / disable logs SQL
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
return hibernateJpaVendorAdapter;
}
// the EntityManagerFactory and TransactionManager are defined with default values by Spring boot
}
- Riga 45: Non definiremo i bean [EntityManagerFactory] e [TransactionManager]. Ci affideremo invece all'annotazione [@EnableAutoConfiguration] di Spring Boot (riga 17);
- Righe 24–32: Definiamo l'origine dati MySQL 5. Si tratta di un bean che Spring Boot generalmente non è in grado di configurare automaticamente;
- Righe 36–43: Configuriamo anche l'implementazione JPA per impostare l'attributo [showSql] di Hibernate su false (riga 39). Per impostazione predefinita, è impostato su true;
- Per ora, gli unici componenti gestiti da Spring sono i bean alle righe 25 e 37, oltre ai bean [EntityManagerFactory] e [TransactionManager] tramite configurazione automatica. Dobbiamo aggiungere i bean dei livelli [business] e [DAO];
- La riga 16 aggiunge al contesto Spring le interfacce del pacchetto [rdvmdecins.repositories] che ereditano dall'interfaccia [CrudRepository];
- La riga 18 aggiunge al contesto Spring tutte le classi nel pacchetto [rdvmedecins] e le sue sottoclassi che hanno un'annotazione Spring. Nel pacchetto [rdvmdecins.metier], la classe [Metier] con la sua annotazione [@Service] verrà individuata e aggiunta al contesto Spring;
- Riga 45: un bean [entityManagerFactory] verrà definito di default da Spring Boot. Dobbiamo indicare a questo bean dove si trovano le entità JPA che deve gestire. La riga 19 svolge questa funzione;
- riga 20: specifica che i metodi delle interfacce che ereditano dall'interfaccia [CrudRepository] devono essere eseguiti all'interno di una transazione;
2.9. Test per il livello [business]
La classe [rdvmedecins.tests.Metier] è una classe di test Spring/JUnit 4:
package rdvmedecins.tests;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Metier {
@Autowired
private IMetier métier;
@Test
public void test1(){
// customer display
List<Client> clients = métier.getAllClients();
display("Liste des clients :", clients);
// physician display
List<Medecin> medecins = métier.getAllMedecins();
display("Liste des médecins :", medecins);
// display doctor's slots
Medecin médecin = medecins.get(0);
List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
// list of doctor's appointments on a given day
Date jour = new Date();
display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// add a RV
Rv rv = null;
Creneau créneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
rv = métier.ajouterRv(jour, créneau, client);
// check
Rv rv2 = métier.getRvById(rv.getId());
Assert.assertEquals(rv, rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// add a RV in the same slot on the same day
// must trigger an exception
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
Boolean erreur = false;
try {
rv = métier.ajouterRv(jour, créneau, client);
System.out.println("Rv ajouté");
} catch (Exception ex) {
Throwable th = ex;
while (th != null) {
System.out.println(ex.getMessage());
th = th.getCause();
}
// we note the error
erreur = true;
}
// check for errors
Assert.assertTrue(erreur);
// RV list
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// calendar display
AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
System.out.println(agenda);
Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
// delete a RV
System.out.println("Suppression du Rv ajouté");
métier.supprimerRv(rv);
// check
rv2 = métier.getRvById(rv.getId());
Assert.assertNull(rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
}
// utility method - displays items in a collection
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- riga 22: l'annotazione [@SpringApplicationConfiguration] consente di utilizzare il file di configurazione [DomainAndPersistenceConfig] descritto in precedenza. La classe di test beneficia così di tutti i bean definiti da questo file;
- riga 23: l'annotazione [@RunWith] consente l'integrazione di Spring con JUnit: la classe può essere eseguita come un test JUnit. [@RunWith] è un'annotazione JUnit (riga 9), mentre la classe [SpringJUnit4ClassRunner] è una classe Spring (riga 12);
- Righe 26–27: Iniezione di un riferimento al livello [business] nella classe di test;
- molti test sono semplicemente test visivi:
- righe 32–33: elenco dei clienti;
- righe 35–36: elenco dei medici;
- righe 39–40: elenco delle fasce orarie di un medico;
- riga 43: elenco degli appuntamenti di un medico;
- riga 50: aggiungi un nuovo appuntamento. Il metodo [addAppt] restituisce l'appuntamento con informazioni aggiuntive, il suo ID chiave primaria;
- riga 53: questa chiave primaria viene utilizzata per cercare l'appuntamento nel database;
- riga 54: verifichiamo che l'appuntamento cercato e quello trovato siano gli stessi. Ricordiamo che il metodo [equals] dell'entità [Rv] è stato ridefinito: due appuntamenti sono uguali se hanno lo stesso id. In questo caso, ciò ci mostra che l'appuntamento aggiunto è stato effettivamente inserito nel database;
- Righe 61–73: proviamo ad aggiungere lo stesso appuntamento una seconda volta. Questo dovrebbe essere rifiutato dal DBMS perché esiste un vincolo di unicità:
CREATE TABLE IF NOT EXISTS `rv` (
`ID` bigint(20) NOT NULL AUTO_INCREMENT,
`JOUR` date NOT NULL,
`ID_CLIENT` bigint(20) NOT NULL,
`ID_CRENEAU` bigint(20) NOT NULL,
`VERSION` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`ID`),
UNIQUE KEY `UNQ1_RV` (`JOUR`,`ID_CRENEAU`),
KEY `FK_RV_ID_CRENEAU` (`ID_CRENEAU`),
KEY `FK_RV_ID_CLIENT` (`ID_CLIENT`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci AUTO_INCREMENT=60 ;
La riga 8 sopra specifica che la combinazione [DAY, SLOT_ID] deve essere univoca, il che impedisce che due appuntamenti vengano programmati nella stessa fascia oraria dello stesso giorno.
- riga 73: verifichiamo che si sia effettivamente verificata un'eccezione;
- riga 77: recuperiamo il calendario del medico per il quale abbiamo appena aggiunto un appuntamento;
- riga 79: verifichiamo che l'appuntamento aggiunto sia effettivamente presente nel suo calendario;
- riga 82: eliminiamo l'appuntamento aggiunto;
- riga 84: recuperiamo l'appuntamento cancellato dal database;
- riga 85: controlliamo di aver recuperato un puntatore nullo, a indicare che l'appuntamento che stavamo cercando non esiste;
Il test viene eseguito con successo:
![]() |
2.10. Il programma da console
![]() |
Il programma da console è semplice. Mostra come recuperare una chiave esterna:
package rdvmedecins.boot;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
public class Boot {
// the boot
public static void main(String[] args) {
// prepare the configuration
SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
app.setLogStartupInfo(false);
// launch it
ConfigurableApplicationContext context = app.run(args);
// business
IMetier métier = context.getBean(IMetier.class);
try {
// add a RV to the list
Date jour = new Date();
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
Client client = (Client) new Client().build(1L, 1L);
Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
Rv rv = métier.ajouterRv(jour, créneau, client);
System.out.println(String.format("Rv ajouté = %s", rv));
// check
créneau = métier.getCreneauById(1L);
long idMedecin = créneau.getIdMedecin();
display("Liste des rendez-vous", métier.getRvMedecinJour(idMedecin, jour));
} catch (Exception ex) {
System.out.println("Exception : " + ex.getCause());
}
// closing the Spring context
context.close();
}
// utility method - displays items in a collection
private static <T> void display(String message, Iterable<T> elements) {
System.out.println(message);
for (T element : elements) {
System.out.println(element);
}
}
}
Il programma aggiunge un appuntamento e poi verifica che sia stato aggiunto.
- riga 19: la classe [SpringApplication] utilizzerà la classe di configurazione [DomainAndPersistenceConfig];
- riga 20: soppressione dei log di avvio dell'applicazione;
- riga 22: viene eseguita la classe [SpringApplication]. Restituisce un contesto Spring, ovvero l'elenco dei bean registrati;
- riga 24: viene recuperato un riferimento al bean che implementa l'interfaccia [IMetier]. Si tratta quindi di un riferimento al livello [business];
- righe 27–31: aggiungi un nuovo appuntamento per oggi, per il cliente n. 1 nello slot n. 1. Il cliente e lo slot sono stati creati da zero per dimostrare che vengono utilizzati solo gli identificatori. Abbiamo inizializzato la versione qui, ma avremmo potuto utilizzare qualsiasi valore. Qui non viene utilizzata;
- riga 34: vogliamo sapere quale medico ha lo slot n. 1. Per farlo, dobbiamo interrogare il database per lo slot n. 1. Poiché siamo in modalità [FetchType.LAZY], il medico non viene restituito con lo slot. Tuttavia, ci siamo assicurati di includere un campo [idMedecin] nell'entità [Creneau] per recuperare la chiave primaria del medico;
- riga 35: recuperiamo la chiave primaria del medico;
- riga 36: visualizziamo l'elenco degli appuntamenti del medico;
L'output della console è il seguente:
2.11. Introduzione a Spring MVC
![]() |
Affronteremo ora la realizzazione del livello web. Questo livello consiste principalmente in metodi che gestiscono URL specifici e rispondono con una riga di testo in formato JSON (JavaScript Object Notation). Questo livello web è un'interfaccia web talvolta denominata API web. Implementeremo questa interfaccia utilizzando Spring MVC, un altro componente dell'ecosistema Spring. Inizieremo esaminando una delle guide disponibili all'indirizzo [http://spring.io].
2.11.1. Il progetto demo
![]() |
- in [1], importiamo una delle guide di Spring;
![]() |
- in [2], selezioniamo l'esempio [Rest Service];
- in [3], selezioniamo il progetto Maven;
- in [4], selezioniamo la versione finale della guida;
- in [5], confermiamo;
- in [6], il progetto importato;
I servizi web accessibili tramite URL standard che restituiscono testo JSON sono spesso chiamati servizi REST (REpresentational State Transfer). In questo documento, mi riferirò semplicemente al servizio che stiamo per realizzare come servizio web/JSON. Un servizio è detto RESTful se segue determinate regole. Non ho cercato di attenermi a queste regole.
Esaminiamo ora il progetto importato, iniziando dalla sua configurazione Maven.
2.11.2. Configurazione Maven
Il file [pom.xml] è il seguente:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<url>http://repo.spring.io/release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>http://repo.spring.io/release</url>
</pluginRepository>
</pluginRepositories>
</project>
- righe 10–14: come nel progetto [Spring Data], è presente il progetto padre [Spring Boot];
- righe 17–20: l'artefatto [spring-boot-starter-web] include le librerie necessarie per un progetto Spring MVC. In particolare, include un server Tomcat incorporato. L'applicazione verrà eseguita su questo server;
- righe 21–24: la libreria Jackson gestisce il JSON: converte un oggetto Java in una stringa JSON e viceversa;
Questa configurazione include un gran numero di librerie:
![]() | ![]() |
Sopra vediamo i tre archivi del server Tomcat.
2.11.3. L'architettura di un servizio REST Spring
Spring MVC implementa il modello architettonico MVC (Model–View–Controller) come segue:
![]() |
L'elaborazione di una richiesta del client procede come segue:
- richiesta - gli URL richiesti hanno il formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... Il [Dispatcher Servlet] è la classe Spring che gestisce gli URL in entrata. Esso "instradano" l'URL all'azione che dovrebbe gestirlo. Queste azioni sono metodi di classi specifiche chiamate [Controller]. La C in MVC in questo caso è la catena [Dispatcher Servlet, Controller, Action]. Se non è stata configurata alcuna azione per gestire l'URL in entrata, il [Dispatcher Servlet] risponderà che l'URL richiesto non è stato trovato (errore 404 NOT FOUND);
- l'elaborazione
- l'azione selezionata può utilizzare i parametri che il [Dispatcher Servlet] le ha passato. Questi possono provenire da diverse fonti:
- il percorso [/param1/param2/...] dell'URL,
- i parametri dell'URL [p1=v1&p2=v2],
- dai parametri inviati dal browser con la sua richiesta;
- durante l'elaborazione della richiesta dell'utente, l'azione potrebbe aver bisogno del livello [business] [2b]. Una volta elaborata la richiesta del client, questa può innescare varie risposte. Un esempio classico è:
- una pagina di errore se la richiesta non è stata elaborata correttamente
- una pagina di conferma in caso contrario
- l'azione indica di visualizzare una vista specifica [3]. Questa vista mostrerà i dati noti come modello di vista. Questa è la M in MVC. L'azione creerà questo modello M [2c] e indicherà di visualizzare una vista V [3];
- risposta - la vista V selezionata utilizza il modello M costruito dall'azione per inizializzare le parti dinamiche della risposta HTML che deve inviare al client, quindi invia questa risposta.
Per un servizio web / JSON, l'architettura precedente viene leggermente modificata:
![]() |
- in [4a], il modello, che è una classe Java, viene convertito in una stringa JSON da una libreria JSON;
- in [4b], questa stringa JSON viene inviata al browser;
2.11.4. Il controller C
![]() |
L'applicazione importata dispone del seguente controller:
package hello;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@RequestMapping("/greeting")
public @ResponseBody
Greeting greeting(@RequestParam(value = "name", required = false, defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}
- riga 9: l'annotazione [@Controller] rende la classe [GreetingController] un controller Spring, il che significa che i suoi metodi sono registrati per gestire gli URL;
- riga 15: l'annotazione [@RequestMapping] specifica l'URL gestito dal metodo, in questo caso l'URL [/greeting]. Vedremo in seguito che questo URL può essere parametrizzato e che è possibile recuperare questi parametri;
- riga 16: l'annotazione [@ResponseBody] indica che il metodo non genera un modello per una vista (JSP, JSF, Thymeleaf, ecc.) da inviare al browser del cliente, ma genera invece la risposta al browser stesso. Qui, produce un oggetto di tipo [Greeting] (riga 18). Sebbene non sia immediatamente evidente in questo contesto, questo oggetto verrà prima convertito in JSON prima di essere inviato al browser. È la presenza di una libreria JSON nelle dipendenze del progetto che induce Spring Boot a configurare automaticamente il progetto in questo modo;
- Riga 17: Il metodo [greeting] ha un parametro [String name]. L'annotazione [@RequestParam(value = "name", required = false, defaultValue = "World"] indica che questo parametro deve essere inizializzato con un parametro denominato [name] (@RequestParam(value = "name"). Questo può essere un parametro GET o POST. Questo parametro non è obbligatorio (required = false). In questo caso, il parametro [name] del metodo verrà inizializzato con il valore [World] (defaultValue = "World").
2.11.5. Il modello M
Il modello M prodotto dal metodo precedente è il seguente oggetto [Greeting]:
![]() |
package hello;
public class Greeting {
private final long id;
private final String content;
public Greeting(long id, String content) {
this.id = id;
this.content = content;
}
public long getId() {
return id;
}
public String getContent() {
return content;
}
}
La trasformazione JSON di questo oggetto creerà la stringa {"id":n,"content":"text"}. Alla fine, la stringa JSON prodotta dal metodo del controller avrà la forma:
oppure
2.11.6. Configurazione del progetto
![]() |
Il progetto è configurato dalla seguente classe [Application]:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- Riga 11: È interessante notare che questa classe è eseguibile con un metodo [main] specifico per le applicazioni console. È proprio così. La classe [SpringApplication] alla riga 12 avvierà il server Tomcat presente nelle dipendenze e distribuirà il servizio REST su di esso;
- riga 4: possiamo vedere che la classe [SpringApplication] appartiene al progetto [Spring Boot];
- riga 12: il primo parametro è la classe che configura il progetto, il secondo contiene eventuali parametri aggiuntivi;
- riga 8: l'annotazione [@EnableAutoConfiguration] indica a Spring Boot di configurare il progetto;
- riga 7: l'annotazione [@ComponentScan] fa sì che la directory contenente la classe [Application] venga scansionata alla ricerca di componenti Spring. Ne verrà trovato uno: la classe [GreetingController], che ha l'annotazione [@Controller], rendendola un componente Spring;
2.11.7. Esecuzione del progetto
Eseguiamo il progetto:
![]() |
Otteniamo i seguenti log della console:
____ _ __ _ _
- riga 12: il server Tomcat si avvia sulla porta 8080 (riga 11);
- riga 16: il servlet [DispatcherServlet] è presente;
- riga 19: il metodo [GreetingController.greeting] è stato individuato;
Per testare l'applicazione web, richiediamo l'URL [http://localhost:8080/greeting]:
![]() | ![]() |
Riceviamo la stringa JSON prevista. Potrebbe essere interessante visualizzare le intestazioni HTTP inviate dal server. Per farlo, useremo il plugin di Chrome chiamato [Advanced Rest Client] (vedi Appendici):
![]() |
- in [1], l'URL richiesto;
- in [2], viene utilizzato il metodo GET;
- in [3], la risposta JSON;
- in [4], il server ha indicato che avrebbe inviato una risposta in formato JSON;
- in [5], richiediamo lo stesso URL ma questa volta utilizzando una richiesta POST;
- in [7], le informazioni vengono inviate al server in formato [urlencoded];
- in [6], il parametro name con il suo valore;
- in [8], il browser comunica al server che sta inviando informazioni [urlencoded];
- in [9], la risposta JSON del server;
2.11.8. Creazione di un archivio eseguibile
È possibile creare un archivio eseguibile al di fuori di Eclipse. La configurazione necessaria si trova nel file [pom.xml]:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.Application</start-class>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- Le righe 9–12 definiscono il plugin che creerà l'archivio eseguibile;
- La riga 3 definisce la classe eseguibile del progetto;
Ecco come procedere:
![]() |
- in [1]: eseguire un goal di Maven;
- in [2]: ci sono due goal: [clean] per eliminare la cartella [target] dal progetto Maven, [package] per rigenerarla;
- in [3]: la cartella [target] generata si troverà in questa cartella;
- in [4]: il target è generato;
Nei log che compaiono nella console, è importante vedere il plugin [spring-boot-maven-plugin]. Questo è il plugin che genera l'archivio eseguibile.
Utilizzando una console, accedere alla cartella generata:
- riga 5: l'archivio generato;
Questo archivio viene eseguito come segue:
Ora che l'applicazione web è in esecuzione, è possibile accedervi utilizzando un browser:
![]() |
2.11.9. Distribuzione dell'applicazione su un server Tomcat
Sebbene Spring Boot sia molto pratico in modalità di sviluppo, è probabile che un'applicazione di produzione venga distribuita su un vero server Tomcat. Ecco come procedere:
Modifica il file [pom.xml] come segue:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<packaging>war</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
....
</project>
È necessario apportare modifiche in due punti:
- riga 9: è necessario specificare che si intende generare un file WAR (Web Archive);
- Righe 26–30: è necessario aggiungere una dipendenza dall'artefatto [spring-boot-starter-tomcat]. Questo artefatto aggiunge tutte le classi Tomcat alle dipendenze del progetto;
- Riga 29: questo artefatto è [provided], il che significa che gli archivi corrispondenti non saranno inclusi nel file WAR generato. Questi archivi si troveranno invece sul server Tomcat dove verrà eseguita l'applicazione;
È inoltre necessario configurare l'applicazione web. In assenza di un file [web.xml], ciò avviene utilizzando una classe che estende [SpringBootServletInitializer]:
![]() |
La classe [ApplicationInitializer] è la seguente:
package hello;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
public class ApplicationInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
- riga 6: la classe [ApplicationInitializer] estende la classe [SpringBootServletInitializer];
- riga 9: il metodo [configure] viene sovrascritto (riga 8);
- riga 10: viene fornita la classe che configura il progetto;
Per eseguire il progetto, procedere come segue:
![]() |
- in [1], eseguire il progetto su uno dei server registrati nell'IDE Eclipse;
- in [2], selezionare [tc Server Developer], che è l'opzione predefinita. Si tratta di una variante di Tomcat;
Una volta fatto ciò, è possibile inserire l'URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] in un browser:
![]() |
Ora sappiamo come generare un archivio WAR. Andando avanti, continueremo a lavorare con Spring Boot e il suo archivio JAR eseguibile.
2.11.10. Creazione di un nuovo progetto web
Per creare un nuovo progetto web, segui questi passaggi:
![]() |
- in [1]: File / Nuovo / Progetto Spring Starter
- in [2]: seleziona [Web]. Non selezionare alcuna libreria di viste perché in un servizio web / JSON non ci sono viste;
- Il progetto creato sarà un progetto Maven. In [3], inserisci il nome del gruppo per l'artefatto Maven da creare; in [4], inserisci il nome dell'artefatto;
- in [5], inserisci il nome di un pacchetto in cui Spring inserirà la classe di configurazione del progetto;
- in [6], assegnare un nome al progetto Eclipse: può essere diverso da [4];
![]() |
2.12. Il livello [web]
![]() |
![]() |
Realizzeremo il livello web in diverse fasi:
- Fase 1: un livello web funzionante senza autenticazione;
- Fase 2: implementazione dell'autenticazione con Spring Security;
- Fase 3: implementazione del CORS [Il Cross-origin resource sharing (CORS) è un meccanismo che consente di richiedere molte risorse (ad esempio font, JavaScript, ecc.) presenti in una pagina web da un dominio diverso da quello di origine della risorsa. (Wikipedia)]. Il client per il nostro servizio web sarà un client web Angular che non appartiene necessariamente allo stesso dominio del nostro servizio web. Per impostazione predefinita, non può accedere al servizio web a meno che quest'ultimo non lo autorizzi a farlo. Vedremo come;
2.12.1. Configurazione di Maven
Il file [pom.xml] del progetto è il seguente:
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.mvc</groupId>
<artifactId>rdvmedecins-webapi-v1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-webapi-v1</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
- righe 7–11: il progetto Maven principale;
- righe 13–16: dipendenze per un progetto Spring MVC;
- righe 17–21: dipendenze relative ai livelli [logica di business, DAO, JPA];
2.12.2. L'interfaccia del servizio web
![]() |
- In [1] sopra, il browser può richiedere solo un numero limitato di URL con una sintassi specifica;
- in [4], riceve una risposta JSON;
Le risposte del nostro servizio web avranno tutte lo stesso formato, corrispondente alla rappresentazione JSON di un oggetto di tipo [Response] come segue:
package rdvmedecins.web.models;
public class Reponse {
// ----------------- properties
// operation status
private int status;
// the answer JSON
private Object data;
// ---------------constructeurs
public Reponse() {
}
public Reponse(int status, Object data) {
this.status = status;
this.data = data;
}
// methods
public void incrStatusBy(int increment) {
status += increment;
}
// ----------------------getters and setters
...
}
- riga 7: codice di errore della risposta 0: OK, qualsiasi altro valore: KO;
- riga 9: il corpo della risposta;
Di seguito presentiamo gli screenshot che illustrano l'interfaccia del servizio web / JSON:
Elenco di tutti i pazienti dello studio medico [/getAllClients]
![]() |
Elenco di tutti i medici dello studio medico [/getAllMedecins]
![]() |
Elenco degli orari disponibili di un medico [/getAllCreneaux/{idMedecin}]
![]() |
Elenco degli appuntamenti di un medico [/getRvMedecinJour/{idMedecin}/{yyyy-mm-dd}
![]() |
Agenda giornaliera del medico [/getAgendaMedecinJour/{idMedecin}/{yyyy-mm-dd}]
![]() |
Per aggiungere o eliminare un appuntamento, utilizziamo l'estensione di Chrome [Advanced Rest Client] poiché queste operazioni vengono eseguite tramite una richiesta POST.
Aggiungi un appuntamento [/ addRv]
![]() |
- in [0], l'URL del servizio web;
- in [1], viene utilizzato il metodo POST;
- in [2], il testo JSON delle informazioni inviate al servizio web nel formato {day, clientId, slotId};
- in [3], il client specifica al servizio web che sta inviando informazioni in formato JSON;
La risposta è quindi la seguente:
![]() |
- in [4]: il client invia l'intestazione indicando che i dati che sta inviando sono in formato JSON;
- in [5]: il servizio web risponde che sta inviando anch'esso JSON;
- in [6]: la risposta JSON del servizio web. Il campo [data] contiene la rappresentazione JSON dell'appuntamento aggiunto;
È possibile verificare la presenza del nuovo appuntamento:
![]() |
Elimina un appuntamento [/deleteApp]
![]() |
- in [1], l'URL del servizio web;
- in [2], viene utilizzato il metodo POST;
- in [3], il testo JSON delle informazioni inviate al servizio web nella forma {idRv};
- in [4], il client specifica al servizio web che sta inviando dati JSON;
La risposta è quindi la seguente:
![]() |
- in [5]: il campo [status] è impostato su 0, a indicare che l'operazione è andata a buon fine;
È possibile verificare l'eliminazione dell'appuntamento:
![]() |
Come mostrato sopra, l'appuntamento della paziente [Sig.ra GERMAN] non è più presente nell'elenco.
Il servizio web consente inoltre di recuperare le entità in base al loro ID:
![]() |
![]() |
![]() |
![]() |
Tutti questi URL sono gestiti dal controller [RdvMedecinsController], che presenteremo ora.
2.12.3. Lo scheletro del controller [ RdvMedecinsController]
![]() |
Il controller [RdvMedecinsController] è il seguente:
package rdvmedecins.web.controllers;
import java.text.ParseException;
...
@RestController
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
private List<String> messages;
@PostConstruct
public void init() {
// application error messages
messages = application.getMessages();
}
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
public Reponse getAllMedecins() {
...
}
// customer list
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
public Reponse getAllClients() {
...
}
// list of physician slots
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
...
}
// list of doctor's appointments
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin,
@PathVariable("jour") String jour) {
...
}
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
public Reponse getClientById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
public Reponse getMedecinById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
public Reponse getRvById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
public Reponse getCreneauById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
...
}
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
...
}
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(
@PathVariable("idMedecin") long idMedecin,
@PathVariable("jour") String jour) {
...
}
}
- riga 6: l'annotazione [@RestController] rende la classe [RdvMedecinsController] un controller Spring. Inoltre, garantisce che i metodi che gestiscono gli URL generino una risposta che viene automaticamente convertita in JSON;
- righe 9–10: un oggetto di tipo [ApplicationModel] verrà iniettato qui da Spring;
- riga 13: l'annotazione [@PostConstruct] contrassegna un metodo da eseguire immediatamente dopo l'istanziazione della classe. Quando questo metodo viene eseguito, gli oggetti iniettati da Spring sono disponibili;
- Tutti i metodi restituiscono un oggetto di tipo [Response] come segue:
package rdvmedecins.web.models;
public class Reponse {
// ----------------- properties
// operation status
private int status;
// the answer
private Object data;
...
}
Questo oggetto viene serializzato in JSON prima di essere inviato al browser del client;
- riga 20: l'annotazione [@RequestMapping] imposta le condizioni per la chiamata del metodo. Qui, il metodo gestisce una richiesta GET dall'URL [/getAllMedecins]. Se questo URL fosse richiesto tramite un POST, verrebbe rifiutato e Spring MVC invierebbe un codice di errore HTTP al client web;
- riga 32: l'URL è configurato con {idMedecin}. Questo parametro viene recuperato utilizzando l'annotazione [@PathVariable] alla riga 33;
- riga 33: il singolo parametro [long idMedecin] riceve il suo valore dal parametro {idMedecin} nell'URL [@PathVariable("idMedecin")]. Il parametro nell'URL e quello nel metodo possono avere nomi diversi. Si noti che [@PathVariable("idMedecin")] è di tipo String (l'intero URL è una String), mentre il parametro [long idMedecin] è di tipo [long]. La conversione di tipo viene eseguita automaticamente. Se questa conversione di tipo fallisce, viene restituito un codice di errore HTTP;
- riga 65: l'annotazione [@RequestBody] si riferisce al corpo della richiesta. In una richiesta GET, non c'è quasi mai un corpo (ma è possibile includerne uno). In una richiesta POST, di solito ce n'è uno (ma è possibile ometterlo). Per l'URL [ajouterRv], il client web invia la seguente stringa JSON nel suo POST:
La sintassi [@RequestBody PostAjouterRv post] (riga 65), combinata con il fatto che il metodo si aspetta JSON [consumes = "application/json; charset=UTF-8"] (riga 64), farà sì che la stringa JSON inviata dal client web venga deserializzata in un oggetto di tipo [PostAjouter]. Questo oggetto è definito come segue:
package rdvmedecins.web.models;
public class PostAjouterRv {
// pOST DATA
private String jour;
private long idClient;
private long idCreneau;
// getters and setters
...
}
Anche in questo caso, le conversioni di tipo necessarie avverranno automaticamente;
- Le righe 69–70 contengono un meccanismo simile per l'URL [/deleteRv]. La stringa JSON inviata è la seguente:
e il tipo [PostSupprimerRv] è il seguente:
package rdvmedecins.web.models;
public class PostSupprimerRv {
// pOST DATA
private long idRv;
// getters and setters
...
}
2.12.4. Modelli di servizi web
![]() |
Abbiamo già presentato i modelli [Response, PostAddAppointment, PostDeleteAppointment]. Il modello [ApplicationModel] è il seguente:
package rdvmedecins.web.models;
import java.util.Date;
...
@Component
public class ApplicationModel implements IMetier {
// the [business] layer
@Autowired
private IMetier métier;
// data from the [business] layer
private List<Medecin> médecins;
private List<Client> clients;
// error messages
private List<String> messages;
@PostConstruct
public void init() {
// we get the doctors and the customers
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
// getter
public List<String> getMessages() {
return messages;
}
// ------------------------- [business] layer interface
@Override
public List<Client> getAllClients() {
return clients;
}
@Override
public List<Medecin> getAllMedecins() {
return médecins;
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return métier.getAllCreneaux(idMedecin);
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return métier.getRvMedecinJour(idMedecin, jour);
}
@Override
public Client getClientById(long id) {
return métier.getClientById(id);
}
@Override
public Medecin getMedecinById(long id) {
return métier.getMedecinById(id);
}
@Override
public Rv getRvById(long id) {
return métier.getRvById(id);
}
@Override
public Creneau getCreneauById(long id) {
return métier.getCreneauById(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
return métier.ajouterRv(jour, creneau, client);
}
@Override
public void supprimerRv(Rv rv) {
métier.supprimerRv(rv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
return métier.getAgendaMedecinJour(idMedecin, jour);
}
}
- riga 6: l'annotazione [@Component] rende la classe [ApplicationModel] un componente Spring. Come tutti i componenti Spring visti finora (ad eccezione di @Controller), verrà istanziato un solo oggetto di questo tipo (singleton);
- riga 7: la classe [ApplicationModel] implementa l'interfaccia [IMetier];
- righe 10–11: Spring inietta un riferimento al livello [business];
- riga 19: l'annotazione [@PostConstruct] garantisce che il metodo [init] venga eseguito immediatamente dopo l'istanziazione della classe [ApplicationModel];
- righe 23–24: gli elenchi di medici e clienti vengono recuperati dal livello [business];
- riga 26: se si verifica un'eccezione, memorizziamo i messaggi dello stack dell'eccezione nel campo alla riga 17;
La classe [ApplicationModel] avrà due scopi:
- come cache per memorizzare gli elenchi di medici e pazienti (clienti);
- come interfaccia unica per i controller;
L'architettura del livello web si evolve come segue:
![]() |
- in [2b], i metodi del/i controller comunicano con il singleton [ApplicationModel];
Questa strategia offre flessibilità nella gestione della cache. Attualmente, gli slot degli appuntamenti dei medici non vengono memorizzati nella cache. Per memorizzarli, è sufficiente modificare la classe [ApplicationModel]. Ciò non ha alcun impatto sul controller, che continuerà a utilizzare il metodo [List<Creneau> getAllCreneaux(long idMedecin)] come prima. È l'implementazione di questo metodo in [ApplicationModel] che verrà modificata.
2.12.5. La classe Static
La classe [Static] contiene un insieme di metodi di utilità statici che non hanno aspetti "aziendali" o "web":
![]() |
Il codice è il seguente:
package rdvmedecins.web.helpers;
import java.text.SimpleDateFormat;
...
public class Static {
public Static() {
}
// list of exception error messages
public static List<String> getErreursForException(Exception exception) {
// retrieve the list of exception error messages
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
erreurs.add(cause.getMessage());
cause = cause.getCause();
}
return erreurs;
}
// mappers Object --> Map
// --------------------------------------------------------
....
}
- riga 12: il metodo [Static.getErrorsForException] utilizzato (riga 8 sotto) nel metodo [init] della classe [ApplicationModel]:
@PostConstruct
public void init() {
// we get the doctors and the customers
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
Il metodo costruisce un oggetto [List<String>] contenente i messaggi di errore [exception.getMessage()] di un'eccezione [exception] e quelli delle sue eccezioni interne [exception.getCause()].
La classe [Static] contiene altri metodi di utilità che rivedremo quando li incontreremo.
Ora descriveremo in dettaglio la gestione degli URL del servizio web. Tre classi principali sono coinvolte in questo processo:
- il controller [RdvMedecinsController];
- la classe dei metodi di utilità [Static];
- la classe cache [ApplicationModel];
![]() |
2.12.6. Il metodo [init] del controller
Il controller [RdvMedecinsController] (vedere la sezione 2.12.3) dispone di un metodo [init] che viene eseguito immediatamente dopo la sua istanziazione:
@Autowired
private ApplicationModel application;
private List<String> messages;
@PostConstruct
public void init() {
// application error messages
messages = application.getMessages();
}
- Riga 8: I messaggi di errore memorizzati nella cache dell'applicazione [ApplicationModel] vengono salvati localmente nel campo alla riga 3. Ciò consente ai metodi di determinare se l'applicazione si è inizializzata correttamente.
2.12.7. L'URL [/getAllMedecins]
L'URL [/getAllDoctors] è gestito dal seguente metodo nel controller [RdvMedecinsController]:
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
public Reponse getAllMedecins() {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// list of doctors
try {
return new Reponse(0, application.getAllMedecins());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
- riga 5: verifichiamo se l'applicazione si è inizializzata correttamente (messages == null). In caso contrario, restituiamo una risposta con status = -1 e data = messages;
- riga 10: in caso contrario, restituiamo l'elenco dei medici con uno stato pari a 0. Il metodo [application.getAllMedecins()] non genera un'eccezione poiché restituisce semplicemente un elenco memorizzato nella cache. Tuttavia, manterremo questa gestione delle eccezioni nel caso in cui i medici non fossero più presenti nella cache;
Non abbiamo ancora illustrato il caso in cui l'applicazione non sia riuscita a inizializzarsi correttamente. Arrestiamo il DBMS MySQL5, avviamo il servizio web e quindi richiediamo l'URL [/getAllMedecins]:

Otteniamo effettivamente un errore. In circostanze normali, otteniamo la seguente vista:
![]() |
2.12.8. L'URL [/getAllClients]
L'URL [/getAllClients] è gestito dal seguente metodo nel [RdvMedecinsController]:
// customer list
@RequestMapping(value = "/getAllClients")
public Reponse getAllClients() {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// customer list
try {
return new Reponse(0, application.getAllClients());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
È simile al metodo [getAllMedecins] che abbiamo già studiato. I risultati ottenuti sono i seguenti:
![]() |
2.12.9. L'URL [/getAllSlots/{doctorId}]
L'URL [/getAllSlots/{doctorId}] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:
// list of physician slots
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// we get the doctor back
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// doctor's slots
List<Creneau> créneaux = null;
try {
créneaux = application.getAllCreneaux(médecin.getId());
} catch (Exception e1) {
return new Reponse(3, Static.getErreursForException(e1));
}
// we return the answer
return new Reponse(0, Static.getListMapForCreneaux(créneaux));
}
- riga 9: il medico identificato dal parametro [id] viene richiesto da un metodo locale:
private Reponse getMedecin(long id) {
// we get the doctor back
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// existing doctor?
if (médecin == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, médecin);
}
Questo metodo restituisce un valore di stato compreso nell'intervallo [0,1,2]. Torniamo al codice del metodo [getAllSlots]:
- righe 10–12: se status ≠ 0, restituisci immediatamente la risposta;
- riga 13: recuperiamo il medico;
- riga 17: recuperiamo le fasce orarie di questo medico;
- riga 22: restituiamo un oggetto [Static.getListMapForCreneaux(slots)] come risposta;
Rivediamo la definizione della classe [Creneau]:
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of a RV slot
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// a slot is linked to a doctor
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// foreign key
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
...
}
- riga 13: il medico viene recuperato in modalità [FetchType.LAZY];
Ricordiamo la query JPQL che implementa il metodo [getAllCreneaux] nel livello [DAO]:
@Query("select c from Creneau c where c.medecin.id=?1")
La notazione [c.medecin.id] impone un join tra le tabelle [CRENEAUX] e [MEDECINS]. Di conseguenza, la query restituisce tutti gli slot di appuntamento del medico, con il medico incluso in ciascuno di essi. Quando serializziamo questi slot in JSON, la stringa JSON del medico appare in ciascuno di essi. Ciò non è necessario. Quindi, invece di serializzare un oggetto [Creneau], serializzeremo un oggetto [Map] contenente solo i campi desiderati.
Torniamo al codice che abbiamo esaminato in precedenza:
// we return the answer
return new Reponse(0, Static.getListMapForCreneaux(créneaux));
Il metodo [Static.getListMapForCreneaux] è il seguente:
// List<Creneau> --> List<Map>
public static List<Map<String, Object>> getListMapForCreneaux(List<Creneau> créneaux) {
// liste de dictionnaires <String,Object>
List<Map<String, Object>> liste = new ArrayList<Map<String, Object>>();
for (Creneau créneau : créneaux) {
liste.add(Static.getMapForCreneau(créneau));
}
// on rend la liste
return liste;
}
e il metodo [Static.getMapForCreneau] è il seguente:
// Creneau --> Map
public static Map<String, Object> getMapForCreneau(Creneau créneau) {
// qq chose à faire ?
if (créneau == null) {
return null;
}
// dictionnaire <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", créneau.getId());
hash.put("hDebut", créneau.getHdebut());
hash.put("mDebut", créneau.getMdebut());
hash.put("hFin", créneau.getHfin());
hash.put("mFin", créneau.getMfin());
// on rend le dictionnaire
return hash;
}
- riga 8: creiamo un dizionario;
- righe 9–13: aggiungiamo i campi che vogliamo mantenere nella stringa JSON. Il campo [doctor] non è incluso;
- riga 15: restituiamo questo dizionario;
I risultati ottenuti sono i seguenti:
![]() |
oppure questi se la fascia oraria non esiste:
![]() |
oppure questi in caso di errore durante l'accesso al database:
![]() |
2.12.10. L'URL [/getRvMedecinJour/{idMedecin}/{jour}]
L'URL [/getRvMedecinJour/{idMedecin}/{jour}] è gestito dal seguente metodo nel controller [RdvMedecinsController]:
// list of doctor's appointments
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// check the date
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(3, null);
}
// we get the doctor back
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// list of appointments
List<Rv> rvs = null;
try {
rvs = application.getRvMedecinJour(médecin.getId(), jourAgenda);
} catch (Exception e1) {
return new Reponse(4, Static.getErreursForException(e1));
}
// we return the answer
return new Reponse(0, Static.getListMapForRvs(rvs));
}
- Riga 31: Restituiamo un oggetto List<Map<String, Object>> invece di un oggetto List<Rv>. Ricordiamo la definizione della classe [Rv]:
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of an Rv
@Temporal(TemporalType.DATE)
private Date jour;
// an appointment is linked to a customer
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// an appointment is linked to a time slot
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// foreign keys
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
...
}
- riga 11: il client viene recuperato utilizzando la modalità [FetchType.LAZY];
- riga 18: lo slot viene recuperato utilizzando la modalità [FetchType.LAZY];
Ricordiamo la query JPQL che recupera gli appuntamenti:
@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
I join vengono eseguiti in modo esplicito per recuperare i campi [client] e [creneau]. Inoltre, grazie al join [cr.medecin.id=?1], avremo anche il medico. Il medico apparirà quindi nella stringa JSON per ogni appuntamento. Tuttavia, queste informazioni duplicate non sono necessarie. Torniamo al codice del metodo:
- riga 31: costruiamo noi stessi il dizionario da serializzare in JSON;
Il dizionario costruito per un appuntamento è il seguente:
// Rv --> Map
public static Map<String, Object> getMapForRv(Rv rv) {
// anything to do?
if (rv == null) {
return null;
}
// dictionary <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", rv.getId());
hash.put("client", rv.getClient());
hash.put("creneau", getMapForCreneau(rv.getCreneau()));
// we return the dictionary
return hash;
}
- Riga 11: Recuperiamo il dizionario dall'oggetto [Creneau] che abbiamo presentato in precedenza;
I risultati ottenuti sono i seguenti:
![]() |
oppure questi con un giorno errato:
![]() |
oppure questi con un medico errato:
![]() |
2.12.11. L'URL [/getAgendaMedecinJour/{idMedecin}/{jour}]
L'URL [/getAgendaMedecinJour/{idMedecin}/{jour}] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// check the date
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(3, new String[] { String.format("jour [%s] invalide", jour) });
}
// we get the doctor back
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// get your diary back
AgendaMedecinJour agenda = null;
try {
agenda = application.getAgendaMedecinJour(médecin.getId(), jourAgenda);
} catch (Exception e1) {
return new Reponse(4, Static.getErreursForException(e1));
}
// ok
return new Reponse(0, Static.getMapForAgendaMedecinJour(agenda));
}
}
- La riga 30 restituisce un oggetto di tipo List<Map<String, Object>>.
Il metodo [Static.getMapForAgendaMedecinJour] è il seguente:
// AgendaMedecinJour --> Map
public static Map<String, Object> getMapForAgendaMedecinJour(AgendaMedecinJour agenda) {
// anything to do?
if (agenda == null) {
return null;
}
// dictionary <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("medecin", agenda.getMedecin());
hash.put("jour", new SimpleDateFormat("yyyy-MM-dd").format(agenda.getJour()));
List<Map<String, Object>> créneaux = new ArrayList<Map<String, Object>>();
for (CreneauMedecinJour créneau : agenda.getCreneauxMedecinJour()) {
créneaux.add(getMapForCreneauMedecinJour(créneau));
}
hash.put("creneauxMedecin", créneaux);
// we return the dictionary
return hash;
}
Il dizionario costruito ha tre campi:
- [doctor]: il medico a cui appartiene l'appuntamento. Abbiamo mantenuto questa informazione perché compare una sola volta, mentre nei casi precedenti veniva ripetuta in ogni stringa JSON;
- [day]: il giorno del calendario;
- [doctorSlots]: l'elenco degli slot disponibili del medico, inclusi eventuali appuntamenti programmati per quello slot;
Il metodo [getMapForCreneauMedecinJour] utilizzato alla riga 13 è il seguente:
// CreneauMedecinJour --> map
public static Map<String, Object> getMapForCreneauMedecinJour(CreneauMedecinJour créneau) {
// anything to do?
if (créneau == null) {
return null;
}
// dictionary <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("creneau", getMapForCreneau(créneau.getCreneau()));
hash.put("rv", getMapForRv(créneau.getRv()));
// we return the dictionary
return hash;
}
- righe 9-10: utilizziamo i dizionari già discussi per i tipi [Creneau] e [Rv], che quindi non contengono alcun oggetto [Medecin];
I risultati ottenuti sono i seguenti:
![]() |
oppure questi se il giorno non è corretto:
![]() |
oppure questi se l'ID del medico non è valido:
![]() |
2.12.12. L'URL [/getMedecinById/{id}]
L'URL [/getMedecinById/{id}] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
public Reponse getMedecinById(@PathVariable("id") long id) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// we get the doctor back
return getMedecin(id);
}
Riga 8, il metodo [getMedecin] è il seguente:
private Reponse getMedecin(long id) {
// we get the doctor back
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// existing doctor?
if (médecin == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, médecin);
}
I risultati sono i seguenti:
![]() |
oppure questi, se l'ID del medico non è corretto:
![]() |
2.12.13. L'URL [/getClientById/{id}]
L'URL [/getClientById/{id}] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
public Reponse getClientById(@PathVariable("id") long id) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// we get the customer back
return getClient(id);
}
Riga 8, il metodo [getClient] è il seguente:
private Reponse getClient(long id) {
// we get the customer back
Client client = null;
try {
client = application.getClientById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// existing customer?
if (client == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, client);
}
I risultati sono i seguenti:
![]() |
oppure questi se l'ID cliente non è corretto:
![]() |
2.12.14. L'URL [/getCreneauById/{id}]
L'URL [/getCreneauById/{id}] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
public Reponse getCreneauById(@PathVariable("id") long id) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// we get the slot back
Reponse réponse = getCreneau(id);
if (réponse.getStatus() == 0) {
réponse.setData(Static.getMapForCreneau((Creneau) réponse.getData()));
}
// result
return réponse;
}
Riga 8, il metodo [getCreneau] è il seguente:
private Reponse getCreneau(long id) {
// we get the slot back
Creneau créneau = null;
try {
créneau = application.getCreneauById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// existing niche?
if (créneau == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, créneau);
}
I risultati ottenuti sono i seguenti:
![]() |
oppure questi se il numero dello slot non è corretto:
![]() |
2.12.15. L'URL [/getRvById/{id}]
L'URL [/getRvById/{id}] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
public Reponse getRvById(@PathVariable("id") long id) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// recovering the rv
Reponse réponse = getRv(id);
if (réponse.getStatus() == 0) {
réponse.setData(Static.getMapForRv2((Rv) réponse.getData()));
}
// result
return réponse;
}
Riga 8, il metodo [getRv] è il seguente:
private Reponse getRv(long id) {
// we recover the Rv
Rv rv = null;
try {
rv = application.getRvById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// Existing Rv?
if (rv == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, rv);
}
Riga 10, il metodo [Static.getMapForRv2] è il seguente:
// Rv --> Map
public static Map<String, Object> getMapForRv2(Rv rv) {
// qq chose à faire ?
if (rv == null) {
return null;
}
// dictionnaire <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", rv.getId());
hash.put("idClient", rv.getIdClient());
hash.put("idCreneau", rv.getIdCreneau());
// on rend le dictionnaire
return hash;
}
I risultati sono i seguenti:
![]() |
oppure questi se l'ID dell'appuntamento non è corretto:
![]() |
2.12.16. L'URL [/ajouterRv]
L'URL [/ajouterRv] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// retrieve posted values
String jour = post.getJour();
long idCreneau = post.getIdCreneau();
long idClient = post.getIdClient();
// check the date
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(6, null);
}
// we get the slot back
Reponse réponse = getCreneau(idCreneau);
if (réponse.getStatus() != 0) {
return réponse;
}
Creneau créneau = (Creneau) réponse.getData();
// we get the customer back
réponse = getClient(idClient);
if (réponse.getStatus() != 0) {
réponse.incrStatusBy(2);
return réponse;
}
Client client = (Client) réponse.getData();
// we add the Rv
Rv rv = null;
try {
rv = application.ajouterRv(jourAgenda, créneau, client);
} catch (Exception e1) {
return new Reponse(5, Static.getErreursForException(e1));
}
// we return the answer
return new Reponse(0, Static.getMapForRv(rv));
}
Non c'è nulla qui che non abbiamo già visto prima. Alla riga 41, restituiamo l'appuntamento che è stato aggiunto alla riga 36.
I risultati ottenuti con [Advanced Rest Client] sono i seguenti:
![]() |
oppure in questo modo se, ad esempio, forniamo un numero di slot inesistente:
![]() |
![]() |
2.12.17. L'URL [/deleteAppointment]
L'URL [/deleteAppointment] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// retrieve posted values
long idRv = post.getIdRv();
// recovering the rv
Reponse réponse = getRv(idRv);
if (réponse.getStatus() != 0) {
return réponse;
}
// rv deletion
try {
application.supprimerRv(idRv);
} catch (Exception e1) {
return new Reponse(3, Static.getErreursForException(e1));
}
// ok
return new Reponse(0, null);
}
I file <a id="supprimerrv"></a> risultanti sono i seguenti:
![]() |
oppure questi se l'ID dell'appuntamento non esiste:
![]() |
Abbiamo finito con il controller. Ora vediamo come configurare il progetto.
2.12.18. Configurazione del servizio web
![]() |
La classe di configurazione [AppConfig] è la seguente:
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class })
public class AppConfig {
}
- Riga 9: impostiamo la modalità su [AutoConfiguration] in modo che Spring Boot possa configurare il progetto in base ai file che trova nel classpath del progetto;
- riga 10: specifichiamo che i componenti Spring devono essere cercati nel pacchetto [rdvmedecins.web] e nei suoi sottopacchetti. In questo modo verranno individuati i seguenti componenti:
- [@RestController RdvMedecinsController] nel pacchetto [rdvmedecins.web.controllers];
- [@Component ApplicationModel] nel pacchetto [rdvmedecins.web.models];
- Riga 11: importiamo la classe [DomainAndPersistenceConfig], che configura il progetto [rdvmedecins-metier-dao] per fornire l'accesso ai bean di quel progetto;
2.12.19. La classe eseguibile del servizio web
![]() |
La classe [Boot] è la seguente:
package rdvmedecins.web.boot;
import org.springframework.boot.SpringApplication;
import rdvmedecins.web.config.AppConfig;
public class Boot {
public static void main(String[] args) {
SpringApplication.run(AppConfig.class, args);
}
}
Riga 10: Il metodo statico [SpringApplication.run] viene eseguito con la classe di configurazione del progetto [AppConfig] come primo parametro. Questo metodo configurerà automaticamente il progetto, avvierà il server Tomcat incorporato nelle dipendenze e distribuirà il controller [RdvMedecinsController] su di esso.
I log durante l'esecuzione sono i seguenti:
- riga 17: il server Tomcat si avvia;
- righe 23-31: i livelli [logica di business, DAO, JPA] si inizializzano;
- riga 34: è stato individuato il metodo che gestisce l'URL [/getRvMedecinJour/{idMedecin}/{jour}]. Questo processo di individuazione dei metodi del controller si ripete fino alla riga 44;
- riga 52: il servlet Spring MVC [DispatcherServlet] è pronto a rispondere alle richieste dei client web;
Ora disponiamo di un servizio web funzionante che può essere interrogato da un client web. Ci occuperemo ora della sicurezza di questo servizio: vogliamo che solo determinate persone possano gestire gli appuntamenti dei medici. Per farlo, utilizzeremo il framework Spring Security, un componente dell'ecosistema Spring.
2.13. Introduzione a Spring Security
Importeremo nuovamente una guida Spring seguendo i passaggi da 1 a 3 riportati di seguito:
![]() |
![]() |
Il progetto comprende i seguenti elementi:
- nella cartella [templates] troverete le pagine HTML del progetto;
- [Application]: è la classe eseguibile del progetto;
- [MvcConfig]: è la classe di configurazione Spring MVC;
- [WebSecurityConfig]: è la classe di configurazione di Spring Security;
2.13.1. Configurazione Maven
Il progetto [3] è un progetto Maven. Esaminiamo il suo file [pom.xml] per vedere le sue dipendenze:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
- righe 1–5: il progetto è un progetto Spring Boot;
- righe 8–11: dipendenza dal framework [Thymeleaf], che consente la creazione di pagine HTML dinamiche. Questo framework può sostituire le JSP (Java Server Pages), che fino a poco tempo fa erano il framework di visualizzazione predefinito per Spring MVC;
- righe 12–15: dipendenza dal framework Spring Security;
2.13.2. Viste Thymeleaf
![]() |
La vista [home.html] è la seguente:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click <a th:href="@{/hello}">here</a> to see a greeting.
</p>
</body>
</html>
- Gli attributi [th:xx] sono attributi Thymeleaf. Vengono interpretati da Thymeleaf prima che la pagina HTML venga inviata al client. Il client non li vede;
- riga 12: l'attributo [th:href="@{/hello}"] genererà l'attributo [href] del tag <a>. Il valore [@{/hello}] genererà il percorso [<context>/hello], dove [context] è il contesto dell'applicazione web;
Il codice HTML generato è il seguente:
- riga 10: il contesto dell'applicazione è la radice /;
La vista [hello.html] è la seguente:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
</body>
</html>
- riga 9: l'attributo [th:inline="text"] genererà il testo del tag <h1>. Questo testo contiene un'espressione $ che deve essere valutata. L'elemento [[${#httpServletRequest.remoteUser}]] è il valore dell'attributo [RemoteUser] della richiesta HTTP corrente. Si tratta del nome dell'utente che ha effettuato l'accesso;
- riga 10: un modulo HTML. L'attributo [th:action="@{/logout}"] genererà l'attributo [action] del tag [form]. Il valore [@{/logout}] genererà il percorso [<context>/logout], dove [context] è il contesto dell'applicazione web;
Il codice HTML generato è il seguente:
- riga 8: la traduzione di Ciao [[${#httpServletRequest.remoteUser}]]!;
- riga 9: la traduzione di @{/logout};
- riga 11: un campo nascosto denominato (attributo name) _csrf;
La vista finale [login.html] è la seguente:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<div th:if="${param.error}">Invalid username and password.</div>
<div th:if="${param.logout}">You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<label> User Name : <input type="text" name="username" />
</label>
</div>
<div>
<label> Password: <input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Sign In" />
</div>
</form>
</body>
</html>
- Riga 9: l'attributo [th:if="${param.error}"] garantisce che il tag <div> venga generato solo se l'URL che visualizza la pagina di login contiene il parametro [error] (http://context/login?error);
- riga 10: l'attributo [th:if="${param.logout}"] garantisce che il tag <div> venga generato solo se l'URL che visualizza la pagina di accesso contiene il parametro [logout] (http://context/login?logout);
- righe 11–23: un modulo HTML;
- riga 11: il modulo verrà inviato all'URL [<context>/login], dove <context> è il contesto dell'applicazione web;
- riga 13: un campo di immissione denominato [username];
- riga 17: un campo di immissione denominato [password];
Il codice HTML generato è il seguente:
Si noti alla riga 21 che Thymeleaf ha aggiunto un campo nascosto denominato [_csrf].
2.13.3. Configurazione Spring MVC
![]() |
La classe [MvcConfig] configura il framework Spring MVC:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
- riga 7: l'annotazione [@Configuration] rende la classe [MvcConfig] una classe di configurazione;
- riga 8: la classe [MvcConfig] estende la classe [WebMvcConfigurerAdapter] per sovrascrivere determinati metodi;
- riga 10: ridefinizione di un metodo della classe padre;
- righe 11–16: il metodo [addViewControllers] consente di associare gli URL alle viste HTML. In questa sede vengono effettuate le seguenti associazioni:
vista | |
/templates/home.html | |
/templates/hello.html | |
/modelli/login.html |
Il suffisso [html] e la cartella [templates] sono i valori predefiniti utilizzati da Thymeleaf. Possono essere modificati tramite la configurazione. La cartella [templates] deve trovarsi nella radice del classpath del progetto:
![]() |
Nel punto [1] sopra, le cartelle [main] e [resources] sono entrambe cartelle sorgente. Ciò significa che il loro contenuto si troverà nella radice del classpath del progetto. Pertanto, nel punto [2], le cartelle [hello] e [templates] si troveranno nella radice del classpath.
2.13.4. Configurazione di Spring Security
![]() |
La classe [WebSecurityConfig] configura il framework Spring Security:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
- riga 9: l'annotazione [@Configuration] rende la classe [WebSecurityConfig] una classe di configurazione;
- riga 10: l'annotazione [@EnableWebSecurity] rende la classe [WebSecurityConfig] una classe di configurazione di Spring Security;
- riga 11: la classe [WebSecurity] estende la classe [WebSecurityConfigurerAdapter] per sovrascrivere determinati metodi;
- riga 12: ridefinizione di un metodo della classe padre;
- righe 13–16: il metodo [configure(HttpSecurity http)] viene sovrascritto per definire i diritti di accesso per i vari URL dell'applicazione;
- riga 14: il metodo [http.authorizeRequests()] consente di associare diritti di accesso agli URL. In tale contesto vengono effettuate le seguenti associazioni:
regola | codice | |
accesso senza autenticazione | | |
solo accesso autenticato |
- Riga 15: definisce il metodo di autenticazione. L'autenticazione viene eseguita tramite un modulo URL [/login] accessibile a tutti [http.formLogin().loginPage("/login").permitAll()]. Anche il logout è accessibile a tutti.
- righe 19-21: ridefiniscono il metodo [configure(AuthenticationManagerBuilder auth)] che gestisce gli utenti;
- riga 20: l'autenticazione viene eseguita utilizzando utenti hardcoded [auth.inMemoryAuthentication()]. Un utente viene definito qui con il login [user], la password [password] e il ruolo [USER]. Agli utenti con lo stesso ruolo possono essere concesse le stesse autorizzazioni;
2.13.5. Classe eseguibile
![]() |
La classe [Application] è la seguente:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {
public static void main(String[] args) throws Throwable {
SpringApplication.run(Application.class, args);
}
}
- Riga 8: l'annotazione [@EnableAutoConfiguration] indica a Spring Boot (riga 3) di eseguire la configurazione che lo sviluppatore non ha impostato esplicitamente;
- riga 9: rende la classe [Application] una classe di configurazione Spring;
- riga 10: indica al sistema di scansionare la directory contenente la classe [Application] per cercare i componenti Spring. Le due classi [MvcConfig] e [WebSecurityConfig] verranno quindi individuate poiché presentano l'annotazione [@Configuration];
- riga 13: il metodo [main] della classe eseguibile;
- riga 14: il metodo statico [SpringApplication.run] viene eseguito con la classe di configurazione [Application] come parametro. Abbiamo già incontrato questo processo e sappiamo che il server Tomcat incorporato nelle dipendenze Maven del progetto verrà avviato e il progetto verrà distribuito su di esso. Abbiamo visto che sono stati gestiti quattro URL [/, /home, /login, /hello] e che alcuni erano protetti da diritti di accesso.
2.13.6. Test dell'applicazione
Iniziamo richiedendo l'URL [/], che è uno dei quattro URL accettati. È associato alla vista [/templates/home.html]:
![]() |
L'URL richiesto [/] è accessibile a tutti. Ecco perché siamo riusciti a recuperarlo. Il link [qui] è il seguente:
Quando clicchiamo sul link, verrà richiamato l'URL [/hello]. Questo è protetto:
regola | codice | |
accesso senza autenticazione | | |
solo accesso autenticato |
Per accedervi è necessario essere autenticati. Spring Security reindirizzerà quindi il browser del client alla pagina di autenticazione. In base alla configurazione mostrata, si tratta della pagina all'URL [/login]. Questa pagina è accessibile a tutti:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Quindi otteniamo [1]:
![]() |
Il codice sorgente della pagina ottenuta è il seguente:
- Alla riga 7 compare un campo nascosto che non è presente nella pagina originale [login.html]. È stato aggiunto da Thymeleaf. Questo codice, noto come CSRF (Cross-Site Request Forgery), è progettato per eliminare una vulnerabilità di sicurezza. Questo token deve essere rinviato a Spring Security insieme all'autenticazione affinché venga accettato;
Ricordiamo che solo la coppia utente/password viene riconosciuta da Spring Security. Se inseriamo qualcos'altro in [2], otteniamo la stessa pagina con un messaggio di errore in [3]. Spring Security ha reindirizzato il browser all'URL [http://localhost:8080/login?error]. La presenza del parametro [error] ha attivato la visualizzazione del tag:
<div th:if="${param.error}">Invalid username and password.</div>
Ora, inseriamo i valori previsti per utente/password [4]:
![]() |
- in [4], effettuiamo il login;
- in [5], Spring Security ci reindirizza all'URL [/hello] perché quello è l'URL che abbiamo richiesto quando siamo stati reindirizzati alla pagina di accesso. L'identità dell'utente è stata visualizzata dalla seguente riga di [hello.html]:
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
La pagina [5] mostra il seguente modulo:
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
Quando si fa clic sul pulsante [Esci], viene inviata una richiesta POST all'URL [/logout]. Come l'URL [/login], questo URL è accessibile a tutti:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Nella nostra mappatura URL/vista, non abbiamo definito nulla per l'URL [/logout]. Cosa succederà? Proviamo:
![]() |
- In [6], clicchiamo sul pulsante [Sign Out];
- in [7], vediamo che siamo stati reindirizzati all'URL [http://localhost:8080/login?logout]. Spring Security ha richiesto questo reindirizzamento. La presenza del parametro [logout] nell'URL ha fatto sì che nella vista venisse visualizzata la seguente riga:
<div th:if="${param.logout}">You have been logged out.</div>
2.13.7. Conclusione
Nell'esempio precedente, avremmo potuto scrivere prima l'applicazione web e poi proteggerla in un secondo momento. Spring Security è non intrusivo. È possibile implementare la sicurezza per un'applicazione web che è già stata scritta. Inoltre, abbiamo scoperto i seguenti punti:
- è possibile definire una pagina di autenticazione;
- l'autenticazione deve essere accompagnata dal token CSRF emesso da Spring Security;
- se l'autenticazione fallisce, si viene reindirizzati alla pagina di autenticazione con un parametro di errore aggiuntivo nell'URL;
- se l'autenticazione ha esito positivo, si viene reindirizzati alla pagina richiesta al momento dell'autenticazione. Se si richiede la pagina di autenticazione direttamente senza passare attraverso una pagina intermedia, Spring Security reindirizza all'URL [/] (questo caso non è stato dimostrato);
- Si effettua il logout richiedendo l'URL [/logout] con una richiesta POST. Spring Security reindirizza quindi alla pagina di autenticazione con il parametro "logout" nell'URL;
Tutte queste conclusioni si basano sul comportamento predefinito di Spring Security. Questo comportamento può essere modificato tramite configurazione sovrascrivendo determinati metodi della classe [WebSecurityConfigurerAdapter].
Il tutorial precedente ci sarà di scarso aiuto per il prosieguo. Utilizzeremo infatti:
- un database per memorizzare gli utenti, le loro password e i loro ruoli;
- l'autenticazione basata su header HTTP;
Ci sono pochissimi tutorial disponibili per quello che vogliamo fare qui. La soluzione che proporremo è una combinazione di frammenti di codice trovati qua e là.
2.14. Implementazione della sicurezza per il servizio di appuntamenti online
2.14.1. Il database
Il database [rdvmedecins] viene aggiornato per tenere conto degli utenti, delle loro password e dei loro ruoli. Sono state aggiunte tre nuove tabelle:

Tabella [USERS]: utenti
- ID: chiave primaria;
- VERSION: colonna di versioning delle righe;
- IDENTITY: identificatore descrittivo dell'utente;
- LOGIN: nome utente;
- PASSWORD: la sua password;
Nella tabella USERS, le password non sono memorizzate in chiaro:
![]() |
L'algoritmo utilizzato per crittografare le password è l'algoritmo BCRYPT.
Tabella [ROLES]: ruoli
- ID: chiave primaria;
- VERSION: colonna di versioning per la riga;
- NAME: nome del ruolo. Per impostazione predefinita, Spring Security si aspetta nomi nel formato ROLE_XX, ad esempio ROLE_ADMIN o ROLE_GUEST;
![]() |
Tabella [USERS_ROLES]: tabella di join USERS/ROLES
Un utente può avere più ruoli e un ruolo può includere più utenti. Si tratta di una relazione molti-a-molti rappresentata dalla tabella [USERS_ROLES].
- ID: chiave primaria;
- VERSION: colonna di versioning delle righe;
- USER_ID: identificatore utente;
- ROLE_ID: identificatore di un ruolo;
![]() |
Poiché stiamo modificando il database, è necessario modificare tutti i livelli del progetto [logica di business, DAO, JPA]:
![]() |
2.14.2. Il nuovo progetto Eclipse per [logica di business, DAO, JPA]
Duplichiamo il progetto iniziale [rdvmedecins-business-dao] in [rdvmedecins-business-dao-v2]:
![]() |
- in [1]: il nuovo progetto;
- in [2]: le modifiche introdotte dall'implementazione della sicurezza sono state raggruppate in un unico pacchetto [rdvmedecins.security]. Questi nuovi elementi appartengono ai livelli [JPA] e [DAO], ma per semplicità li ho raggruppati in un unico pacchetto.
2.14.3. Le nuove entità [JPA]
![]() |
Il livello JPA definisce tre nuove entità:
![]() |
La classe [User] rappresenta la tabella [USERS]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {
private static final long serialVersionUID = 1L;
// properties
private String identity;
private String login;
private String password;
// manufacturer
public User() {
}
public User(String identity, String login, String password) {
this.identity = identity;
this.login = login;
this.password = password;
}
// identity
@Override
public String toString() {
return String.format("User[%s,%s,%s]", identity, login, password);
}
// getters and setters
....
}
- riga 9: la classe estende la classe [AbstractEntity] già utilizzata per le altre entità;
- righe 13–15: non vengono specificati nomi di colonne perché hanno gli stessi nomi dei campi associati;
La classe [Role] rispecchia la tabella [ROLES]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {
private static final long serialVersionUID = 1L;
// properties
private String name;
// manufacturers
public Role() {
}
public Role(String name) {
this.name = name;
}
// identity
@Override
public String toString() {
return String.format("Role[%s]", name);
}
// getters and setters
...
}
La classe [UserRole] rappresenta la tabella [USERS_ROLES]:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {
private static final long serialVersionUID = 1L;
// a UserRole refers to a User
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
// a UserRole refers to a Role
@ManyToOne
@JoinColumn(name = "ROLE_ID")
private Role role;
// getters and setters
...
}
- righe 15–17: definiscono la chiave esterna dalla tabella [USERS_ROLES] alla tabella [USERS];
- righe 19-21: implementano la chiave esterna dalla tabella [USERS_ROLES] alla tabella [ROLES];
2.14.4. Modifiche al livello [DAO]
![]() |
Il livello [DAO] è stato potenziato con tre nuovi [Repository]:
![]() |
L'interfaccia [UserRepository] gestisce l'accesso alle entità [User]:
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Role;
import rdvmedecins.entities.User;
public interface UserRepository extends CrudRepository<User, Long> {
// list of user roles identified by id
@Query("select ur.role from UserRole ur where ur.user.id=?1")
Iterable<Role> getRoles(long id);
// list of user roles identified by login and password
@Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
Iterable<Role> getRoles(String login, String password);
// search for a user via login
User findUserByLogin(String login);
}
- riga 9: l'interfaccia [UserRepository] estende l'interfaccia [CrudRepository] di Spring Data (riga 4);
- righe 12-13: il metodo [getRoles(User user)] recupera tutti i ruoli per un utente identificato dal proprio [id]
- righe 16-17: come sopra, ma per un utente identificato dal proprio login e password;
L'interfaccia [RoleRepository] gestisce l'accesso alle entità [Role]:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface RoleRepository extends CrudRepository<Role, Long> {
// search for a role by name
Role findRoleByName(String name);
}
- riga 5: l'interfaccia [RoleRepository] estende l'interfaccia [CrudRepository];
- riga 8: è possibile cercare un ruolo in base al suo nome;
L'interfaccia [userRoleRepository] gestisce l'accesso alle entità [UserRole]:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
}
- riga 5: l'interfaccia [UserRoleRepository] estende semplicemente l'interfaccia [CrudRepository] senza aggiungere alcun nuovo metodo;
2.14.5. Classi di gestione di utenti e ruoli
![]() |
Spring Security richiede la creazione di una classe che implementi la seguente interfaccia [UsersDetail]:
![]() |
Questa interfaccia è implementata qui dalla classe [AppUserDetails]:
package rdvmedecins.security;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class AppUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
// properties
private User user;
private UserRepository userRepository;
// manufacturers
public AppUserDetails() {
}
public AppUserDetails(User user, UserRepository userRepository) {
this.user = user;
this.userRepository = userRepository;
}
// -------------------------interface
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : userRepository.getRoles(user.getId())) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getLogin();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// getters and setters
...
}
- riga 10: la classe [AppUserDetails] implementa l'interfaccia [UserDetails];
- righe 15-16: la classe incapsula un utente (riga 15) e il repository che fornisce dettagli su quell'utente (riga 16);
- righe 22–25: il costruttore che istanzia la classe con un utente e il relativo repository;
- righe 28–35: implementazione del metodo [getAuthorities] dell'interfaccia [UserDetails]. Deve costruire una collezione di elementi di tipo [GrantedAuthority] o di un tipo derivato. Qui utilizziamo il tipo derivato [SimpleGrantedAuthority] (riga 32), che incapsula il nome di uno dei ruoli dell'utente della riga 15;
- righe 31–33: iteriamo attraverso l'elenco dei ruoli dell'utente della riga 15 per costruire un elenco di elementi di tipo [SimpleGrantedAuthority];
- righe 38–40: implementiamo il metodo [getPassword] dell'interfaccia [UserDetails]. Restituiamo la password dell'utente della riga 15;
- righe 38–40: implementiamo il metodo [getUserName] dell'interfaccia [UserDetails]. Restituiamo il nome utente dell'utente della riga 15;
- righe 47–50: l'account dell'utente non scade mai;
- righe 52–55: l'account dell'utente non viene mai bloccato;
- righe 57–60: le credenziali dell'utente non scadono mai;
- righe 62–65: l'account dell'utente è sempre attivo;
Spring Security richiede inoltre l'esistenza di una classe che implementi l'interfaccia [AppUserDetailsService]:
![]() |
Questa interfaccia è implementata dalla seguente classe [AppUserDetails]:
package rdvmedecins.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class AppUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
// search for user via login
User user = userRepository.findUserByLogin(login);
// found?
if (user == null) {
throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
}
// render user details
return new AppUserDetails(user, userRepository);
}
}
- riga 9: la classe sarà un componente Spring, quindi sarà disponibile nel suo contesto;
- righe 12–13: il componente [UserRepository] verrà iniettato qui;
- righe 16–25: implementazione del metodo [loadUserByUsername] dell'interfaccia [UserDetailsService] (riga 10). Il parametro è il login dell'utente;
- riga 18: l'utente viene cercato utilizzando il suo nome utente;
- righe 20–22: se l'utente non viene trovato, viene generata un'eccezione;
- riga 24: viene costruito e restituito un oggetto [AppUserDetails]. Si tratta infatti di un oggetto di tipo [UserDetails] (riga 16);
2.14.6. Test del livello [DAO]
![]() |
Per prima cosa, creiamo una classe eseguibile [CreateUser] in grado di creare un utente con un ruolo:
package rdvmedecins.security;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.security.Role;
import rdvmedecins.security.RoleRepository;
import rdvmedecins.security.User;
import rdvmedecins.security.UserRepository;
import rdvmedecins.security.UserRole;
import rdvmedecins.security.UserRoleRepository;
public class CreateUser {
public static void main(String[] args) {
// syntax: login password roleName
// three parameters are required
if (args.length != 3) {
System.out.println("Syntaxe : [pg] user password role");
System.exit(0);
}
// parameters are retrieved
String login = args[0];
String password = args[1];
String roleName = String.format("ROLE_%s", args[2].toUpperCase());
// spring context
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
UserRepository userRepository = context.getBean(UserRepository.class);
RoleRepository roleRepository = context.getBean(RoleRepository.class);
UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
// does the role already exist?
Role role = roleRepository.findRoleByName(roleName);
// if it doesn't exist, we create it
if (role == null) {
role = roleRepository.save(new Role(roleName));
}
// does the user already exist?
User user = userRepository.findUserByLogin(login);
// if it doesn't exist, we create it
if (user == null) {
// hash the password with bcrypt
String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
// save user
user = userRepository.save(new User(login, login, crypt));
// we create the relationship with the role
userRoleRepository.save(new UserRole(user, role));
} else {
// the user already exists - does he/she have the required role?
boolean trouvé = false;
for (Role r : userRepository.getRoles(user.getId())) {
if (r.getName().equals(roleName)) {
trouvé = true;
break;
}
}
// if not found, we create the relationship with the role
if (!trouvé) {
userRoleRepository.save(new UserRole(user, role));
}
}
// closing Spring context
context.close();
}
}
- riga 17: la classe richiede tre argomenti che definiscono un utente: login, password e ruolo;
- righe 25–27: i tre parametri vengono recuperati;
- riga 29: il contesto Spring viene creato dalla classe di configurazione [DomainAndPersistenceConfig]. Questa classe esisteva già nel progetto precedente. Deve essere aggiornata come segue:
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
- Riga 1: è necessario specificare che ora nel pacchetto [rdvmedecins.security] sono presenti componenti [Repository];
- riga 4: è necessario specificare che ora nel pacchetto [rdvmedecins.security] sono presenti entità JPA;
Torniamo al codice per la creazione di un utente:
- righe 30–32: recuperiamo i riferimenti dei tre oggetti [Repository] che potrebbero essere utili per la creazione dell'utente;
- riga 34: verifichiamo se il ruolo esiste già;
- righe 36–38: in caso contrario, lo creiamo nel database. Avrà un nome del tipo [ROLE_XX];
- riga 40: controlliamo se il login esiste già;
- righe 42-49: se il nome utente non esiste, lo creiamo nel database;
- riga 44: crittografiamo la password. Qui utilizziamo la classe [BCrypt] di Spring Security (riga 4). Abbiamo quindi bisogno degli archivi per questo framework. Il file [pom.xml] include una nuova dipendenza:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- Riga 46: l'utente viene salvato nel database;
- riga 48: così come la relazione che lo collega al suo ruolo;
- righe 51–57: se l'account esiste già, verifichiamo se il ruolo che vogliamo assegnargli è già tra i suoi ruoli;
- Righe 59–61: se il ruolo cercato non viene trovato, viene creata una riga nella tabella [USERS_ROLES] per collegare l'utente al proprio ruolo;
- Non abbiamo previsto alcuna protezione contro potenziali eccezioni. Si tratta di una classe di supporto per la creazione rapida di un utente con un ruolo.
Quando la classe viene eseguita con gli argomenti [x x guest], si ottengono i seguenti risultati nel database:
Tabella [USERS]
![]() |
Tabella [RUOLI]
![]() |
Tabella [USERS_ROLES]
![]() |
Consideriamo ora la seconda classe [UsersTest], che è un test JUnit:
![]() |
package rdvmedecins.security;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rdvmedecins.config.DomainAndPersistenceConfig;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {
@Autowired
private UserRepository userRepository;
@Autowired
private AppUserDetailsService appUserDetailsService;
@Test
public void findAllUsersWithTheirRoles() {
Iterable<User> users = userRepository.findAll();
for (User user : users) {
System.out.println(user);
display("Roles :", userRepository.getRoles(user.getId()));
}
}
@Test
public void findUserByLogin() {
// user [admin] is retrieved
User user = userRepository.findUserByLogin("admin");
// we check that his password is [admin]
Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
// check admin / admin role
List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
Assert.assertEquals(1L, roles.size());
Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
}
@Test
public void loadUserByUsername() {
// user [admin] is retrieved
AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
// we check that his password is [admin]
Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
// check admin / admin role
@SuppressWarnings("unchecked")
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
Assert.assertEquals(1L, authorities.size());
Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
}
// utility method - displays items in a collection
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- righe 27–34: test visivo. Visualizziamo tutti gli utenti insieme ai loro ruoli;
- righe 36–46: verifichiamo che l'utente [admin] abbia la password [admin] e il ruolo [ROLE_ADMIN] utilizzando [UserRepository];
- riga 41: [admin] è la password in chiaro. Nel database, è crittografata utilizzando l'algoritmo BCrypt. Il metodo [BCrypt.checkpw] verifica che la password in chiaro crittografata corrisponda a quella presente nel database;
- righe 48–59: verifichiamo che l'utente [admin] abbia la password [admin] e il ruolo [ROLE_ADMIN] utilizzando [appUserDetailsService];
I test vengono eseguiti con successo con i seguenti log:
2.14.7. Conclusione provvisoria
Le classi necessarie per Spring Security sono state aggiunte con modifiche minime al progetto originale. Ricapitolando:
- aggiunta di una dipendenza da Spring Security nel file [pom.xml];
- creazione di tre tabelle aggiuntive nel database;
- creazione di entità JPA e componenti Spring nel pacchetto [rdvmedecins.security];
Questo scenario molto favorevole deriva dal fatto che le tre tabelle aggiunte al database sono indipendenti da quelle esistenti. Avremmo potuto persino collocarle in un database separato. Ciò è stato possibile perché abbiamo deciso che un utente esiste indipendentemente dai medici e dai clienti. Se questi ultimi fossero stati potenziali utenti, avremmo dovuto creare collegamenti tra la tabella [USERS] e le tabelle [MEDECINS] e [CLIENTS]. Ciò avrebbe avuto un impatto significativo sul progetto esistente.
2.14.8. Il progetto Eclipse per il livello [web]
![]() |
Il precedente progetto [rdvmedecins-webapi] è duplicato nel progetto [rdvmedecins-webapi-v2] [1]:
![]() |
Le uniche modifiche da apportare riguardano il pacchetto [rdvmedecins.web.config], dove è necessario configurare Spring Security. Abbiamo già incontrato una classe di configurazione di Spring Security:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
Seguiremo la stessa procedura:
- riga 11: definire una classe che estende la classe [WebSecurityConfigurerAdapter];
- riga 13: definire un metodo [configure(HttpSecurity http)] che definisca i diritti di accesso ai vari URL del servizio web;
- riga 19: definire un metodo [configure(AuthenticationManagerBuilder auth)] che definisce gli utenti e i loro ruoli;
La configurazione di Spring Security è gestita dalla classe [SecurityConfig]:
package rdvmedecins.web.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import rdvmedecins.security.AppUserDetailsService;
@EnableAutoConfiguration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AppUserDetailsService appUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder registry) throws Exception {
// authentication is performed by bean [appUserDetailsService]
// the password is encrypted using the Bcrypt hash algorithm
registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF
http.csrf().disable();
// the password is transmitted by the header Authorization: Basic xxxx
http.httpBasic();
// only the ADMIN role can use the application
http.authorizeRequests() //
.antMatchers("/", "/**") // all URL
.hasRole("ADMIN");
}
}
- righe 14-15: abbiamo riutilizzato le annotazioni dell'esempio;
- righe 17-18: viene iniettata la classe [AppUserDetails], che fornisce l'accesso agli utenti dell'applicazione;
- righe 20-21: il metodo [configure(HttpSecurity http)] definisce gli utenti e i loro ruoli. Accetta come parametro un tipo [AuthenticationManagerBuilder]. Questo parametro è arricchito da due informazioni:
- un riferimento a [appUserDetailsService] dalla riga 18, che fornisce l'accesso agli utenti registrati. Si noti qui che il fatto che siano memorizzati in un database non è esplicitamente dichiarato. Potrebbero quindi trovarsi in una cache, essere forniti da un servizio web, ecc.
- il tipo di crittografia utilizzato per la password. Ricordiamo che abbiamo utilizzato l'algoritmo BCrypt;
- righe 27–40: il metodo [configure(HttpSecurity http)] definisce i diritti di accesso agli URL del servizio web;
- riga 30: abbiamo visto nel progetto introduttivo che, per impostazione predefinita, Spring Security gestisce un token CSRF (Cross-Site Request Forgery) che l'utente che desidera autenticarsi deve inviare al server. Qui, questo meccanismo è disabilitato;
- riga 32: abilitiamo l'autenticazione tramite header HTTP. Il client deve inviare il seguente header HTTP:
dove code è la codifica Base64 della stringa login:password. Ad esempio, la codifica Base64 della stringa admin:admin è YWRtaW46YWRtaW4=. Pertanto, un utente con login [admin] e password [admin] invierà la seguente intestazione HTTP per autenticarsi:
- Righe 34–36: indicano che tutti gli URL del servizio web sono accessibili agli utenti con il ruolo [ROLE_ADMIN]. Ciò significa che un utente senza questo ruolo non può accedere al servizio web;
La classe [AppConfig], che configura l'intera applicazione, viene aggiornata come segue:
![]() |
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class })
public class AppConfig {
}
- La modifica è stata apportata alla riga 11: specifica che ora ci sono due file di configurazione da utilizzare: [DomainAndPersistenceConfig] e [SecurityConfig].
2.14.9. Test del servizio web
Testeremo il servizio web utilizzando il client Chrome [Advanced Rest Client]. Dovremo specificare l'intestazione di autenticazione HTTP:
dove [codice] è la stringa [login:password] codificata in Base64. Per generare questo codice, è possibile utilizzare il seguente programma:
![]() |
package rdvmedecins.helpers;
import org.springframework.security.crypto.codec.Base64;
public class Base64Encoder {
public static void main(String[] args) {
// we expect two arguments: login password
if (args.length != 2) {
System.out.println("Syntaxe : login password");
System.exit(0);
}
// we retrieve the two arguments
String chaîne = String.format("%s:%s", args[0], args[1]);
// encode the string
byte[] data = Base64.encode(chaîne.getBytes());
// displays its Base64 encoding
System.out.println(new String(data));
}
}
Se eseguiamo questo programma con i due argomenti [admin admin]:
![]() |
otteniamo il seguente risultato:
Ora che sappiamo come generare l'intestazione di autenticazione HTTP, avviamo il servizio web, ora protetto. Quindi, utilizzando il client Chrome [Advanced Rest Client], richiediamo l'elenco di tutti i medici:
![]() |
- in [1], richiediamo l'URL dei medici;
- in [2], utilizzando un metodo GET;
- in [3], forniamo l'intestazione di autenticazione HTTP. Il codice [YWRtaW46YWRtaW4=] è la codifica Base64 della stringa [admin:admin];
- in [4], inviamo la richiesta HTTP;
La risposta del server è la seguente:
![]() |
- in [1], l'intestazione di autenticazione HTTP;
- in [2], il server restituisce una risposta JSON;
- in [3], l'elenco dei medici.
Ora proviamo a inviare una richiesta HTTP con un'intestazione di autenticazione errata. La risposta è la seguente:
![]() |
- in [1] e [3]: l'intestazione di autenticazione HTTP;
- in [2]: la risposta del servizio web;
Ora proviamo con l'utente / user. Esiste ma non ha accesso al servizio web. Se eseguiamo il programma di codifica Base64 con i due argomenti [user user]:
![]() |
otteniamo il seguente risultato:
![]() |
- in [1] e [3]: l'intestazione di autenticazione HTTP;
- in [2]: la risposta del servizio web. È diversa da quella precedente, che era [401 Non autorizzato]. In questo caso, l'utente si è autenticato con successo ma non dispone delle autorizzazioni sufficienti per accedere all'URL;
2.15. Conclusione
Rivediamo l'architettura complessiva della nostra applicazione client/server:
![]() |
Un servizio web sicuro è ora operativo. Vedremo che dovrà essere modificato a causa di problemi che sorgeranno durante lo sviluppo del client Angular JS. Ma aspetteremo di incontrare il problema per risolverlo. Ora realizzeremo il client Angular che fornirà un'interfaccia web per la gestione degli appuntamenti dei medici.

















































































































































