13. [Cours]: Esporre un database sul web con Spring MVC
Parole chiave: architettura multistrato, Spring, iniezione di dipendenze, servizio web / jSON, client / server
13.1. Support
![]() | ![]() |
I progetti di questo capitolo si trovano nella cartella [support / chap-13]. Lo script SQL [dbintrospringdata.sql] consente di creare la base MySQL necessaria per i test.
13.2. Il ruolo di Spring MVC in un’applicazione web
Inquadriamo Spring MVC nello sviluppo di un’applicazione web. Nella maggior parte dei casi, questa sarà costruita su un’architettura a più livelli come la seguente:
![]() |
- il livello [Web] è il livello a contatto con l’utente dell’applicazione web. Quest’ultimo interagisce con l’applicazione web attraverso pagine web visualizzate da un browser. È in questo livello che si trova Spring MVC ed esclusivamente in questo livello;
- il livello [métier] implementa le regole di gestione dell’applicazione, come il calcolo di uno stipendio o di una fattura. Questo livello utilizza i dati provenienti dall’utente tramite il livello [Web] e quelli provenienti da SGBD tramite il livello [DAO];
- il livello [DAO] (Data Access Objects), il livello [ORM] (Object Relational Mapper) e il driver JDBC gestiscono l'accesso ai dati del livello SGBD. Il livello [ORM] funge da ponte tra gli oggetti gestiti dal livello [DAO] e le righe e le colonne delle tabelle di un database relazionale. La specifica JPA (Java Persistence API) consente di astrarsi dal ORM utilizzato, purché quest'ultimo implementi tali specifiche. Questo sarà il caso in questione e d'ora in poi chiameremo il livello ORM, il livello JPA;
- l'integrazione dei livelli è gestita dal framework Spring;
13.3. Il modello di sviluppo di Spring MVC
Spring MVC implementa il modello di architettura denominato MVC (Modello – Vista – Controller) nel modo seguente:
![]() |
L’elaborazione di una richiesta da parte di un cliente avviene nel modo seguente:
- richiesta – le URL richieste hanno la forma http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... Il [Front Controller] utilizza un file di configurazione o annotazioni Java per «instradare» la richiesta verso il controller corretto e l’azione corretta all’interno di tale controller. A tal fine, utilizza il campo [Action] del URL. Il resto del URL [/param1/param2/...] è costituito da parametri facoltativi che saranno trasmessi all’azione. Il valore C di MVC è in questo caso la stringa [Front Controller, Contrôleur, Action]. Se nessun controller è in grado di elaborare l'azione richiesta, il server Web risponderà che l'azione richiesta non è stata trovata.
- Elaborazione
- L'azione selezionata può utilizzare i parametri parami che le sono stati trasmessi da [Front Controller]. Questi possono provenire da diverse fonti:
- dal percorso [/param1/param2/...] dell’URL,
- dai parametri [p1=v1&p2=v2] dell'URL,
- dai parametri inviati dal browser insieme alla sua richiesta;
- nell'elaborazione della richiesta dell'utente, l'azione potrebbe richiedere il livello [métier] [2b]. Una volta elaborata la richiesta del cliente, questa può generare diverse risposte. Un esempio classico è:
- una pagina di errore se la richiesta non è stata elaborata correttamente
- una pagina di conferma in caso contrario
- l’azione richiede la visualizzazione di una determinata vista [3]. Questa vista visualizzerà i dati denominati modello della vista. È la M di MVC. L’azione creerà questo modello M [2c] e richiederà la visualizzazione di una vista V [3];
- risposta: la vista V selezionata utilizza il modello M creato dall’azione per inizializzare le parti dinamiche della risposta HTML che deve inviare al client, quindi invia tale risposta.
Per un servizio web / jSON, l’architettura precedente viene leggermente modificata:
![]() |
- in [4a], il modello, che è una classe Java, viene trasformato in una stringa jSON da una libreria jSON;
- in [4b], questa stringa jSON viene inviata al browser;
Ora precisiamo il legame tra l'architettura web MVC e l'architettura a livelli. A seconda della definizione che si dà al modello, questi due concetti possono essere correlati oppure no. Prendiamo ad esempio un'applicazione web Spring MVC a un unico livello:
![]() |
Se implementiamo il livello [Web] con Spring MVC, avremo sì un'architettura web MVC, ma non un'architettura a più livelli. In questo caso, il livello [web] si occuperà di tutto: presentazione, logica di business, accesso ai dati. Saranno le azioni a svolgere questo lavoro.
Consideriamo ora un’architettura web multistrato:
![]() |
Il livello [Web] può essere implementato senza framework e senza seguire il modello MVC. Si ha quindi effettivamente un’architettura multistrato, ma il livello Web non implementa il modello MVC.
Ad esempio, nell’ambiente .NET il livello [Web] quisopra può essere implementato con ASP.NET e MVC, ottenendo così un’architettura a livelli con un livello [Web] di tipo MVC. Fatto ciò, è possibile sostituire questo livello ASP.NET MVC con un livello ASP.NET classico (WebForms), mantenendo il resto (business, DAO, ORM) invariato. Si ottiene così un’architettura a livelli con un livello [Web] che non è più di tipo MVC.
In MVC, abbiamo affermato che il modello M era quello della vista V, c.a.d, ovvero l’insieme dei dati visualizzati dalla vista V. Viene fornita un’altra definizione del modello M di MVC:
![]() |
Molti autori ritengono che ciò che si trova a destra del livello [Web] costituisca il modello M del MVC. Per evitare ambiguità, si può parlare:
- del modello di dominio quando si indica tutto ciò che si trova a destra del livello [Web]
- del modello della vista quando si indicano i dati visualizzati da una vista V
Di seguito, il termine «modello M» indicherà esclusivamente il modello di una vista V.
13.4. Un progetto web / jSON con Spring MVC
Il sito [http://spring.io/guides] offre tutorial introduttivi per scoprire l’ecosistema Spring. Ne seguiremo uno per scoprire la configurazione Maven necessaria per un progetto Spring MVC.
13.4.1. Il progetto dimostrativo
![]() |
- in [1], importiamo una delle guide Spring;
![]() |
- in [2], scegliamo l'esempio [Rest Service];
- in [3], si seleziona il progetto Maven;
- in [4], si seleziona la versione finale della guida;
- in [5], si conferma;
- in [6], il progetto importato;
I servizi web accessibili tramite URL standard e che restituiscono testo jSON sono spesso denominati servizi REST (REpresentational State Transfer). Un servizio è detto Restful se rispetta determinate regole.
Esaminiamo ora il progetto importato, iniziando dalla sua configurazione Maven.
13.4.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.2.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</pluginRepository>
</pluginRepositories>
</project>
- righe 6-8: le proprietà del progetto Maven. Manca un tag [<packaging>] che indichi il tipo di file generato dalla compilazione Maven. In assenza di tale tag, viene utilizzato il tipo [jar]. L’applicazione è quindi un’applicazione eseguibile di tipo console, e non un’applicazione web, nel qual caso il packaging sarebbe [war];
- righe 10-14: il progetto Maven ha un progetto padre [spring-boot-starter-parent]. È questo che definisce la maggior parte delle dipendenze del progetto. Queste possono essere sufficienti, nel qual caso non se ne aggiungono altre, oppure no, nel qual caso si aggiungono le dipendenze mancanti;
- righe 17-20: l’artefatto [spring-boot-starter-web] include le librerie necessarie per un progetto Spring MVC di tipo servizio web in cui non vengono generate viste. Questo artefatto include un numero molto elevato di librerie, tra cui quelle di un server Tomcat integrato. È su questo server che verrà eseguita l’applicazione;
Le librerie incluse in questa configurazione sono molto numerose:
![]() | ![]() |
Sopra sono visibili i tre archivi del server Tomcat.
13.4.3. L'architettura di un servizio Spring [web / jSON]
Per un servizio web / jSON, Spring MVC implementa il modello MVC nel modo seguente:
![]() |
- in [4a], il modello, che è una classe Java, viene trasformato in una stringa jSON da una libreria jSON;
- in [4b], questa stringa jSON viene inviata al browser;
13.4.4. Il controller C
![]() |
L'applicazione importata presenta il seguente controller:
package hello;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@RequestMapping("/greeting")
public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}
- riga 9: l'annotazione [@RestController] rende la classe [GreetingController] un controller Spring, ovvero i suoi metodi sono registrati per gestire i URL. Abbiamo già visto l’annotazione simile [@Controller]. Il risultato dei metodi di quel controller era un tipo [String], che rappresentava il nome della vista da visualizzare. Qui è diverso. I metodi di un controller di tipo [@RestController] restituiscono oggetti che vengono serializzati per essere inviati al browser. Il tipo di serializzazione effettuata dipende dalla configurazione di Spring MVC. In questo caso, saranno serializzati in jSON. È la presenza di una libreria jSON tra le dipendenze del progetto che fa sì che Spring Boot, tramite autoconfigurazione, configuri il progetto in questo modo;
- riga 14: l'annotazione [@RequestMapping] indica l'URL che il metodo gestisce, in questo caso l'URL [/greeting];
- riga 15: abbiamo già spiegato l'annotazione [@RequestParam]. Il risultato restituito dal metodo è un oggetto di tipo [Greeting].
- riga 12: un intero lungo di tipo atomico. Ciò significa che supporta la concorrenza di accesso. Diversi thread potrebbero voler incrementare contemporaneamente la variabile [counter]. L’operazione avverrà in modo corretto. Un thread può leggere il valore del contatore solo se il thread che lo sta modificando ha completato la modifica.
13.4.5. Il modello M
Il modello M generato 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":"testo"}. Alla fine, la stringa jSON generata dal metodo del controller avrà la forma:
oppure
13.4.6. Esecuzione
![]() |
La classe [Application.java] è la classe eseguibile del progetto. Il suo codice è il seguente:
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);
}
}
Abbiamo già visto e spiegato questo codice nell'esempio precedente.
13.4.7. Esecuzione del progetto
Eseguiamo il progetto:
![]() |
Si ottengono i seguenti log della console:
- riga 13: il server Tomcat si avvia sulla porta 8080 (riga 12);
- riga 17: è presente il servlet [DispatcherServlet];
- riga 20: il metodo [GreetingController.greeting] è stato individuato;
Per testare l'applicazione web, si richiede l'URL [http://localhost:8080/greeting]:
![]() | ![]() |
Si riceve correttamente la stringa jSON prevista. Potrebbe essere interessante osservare le intestazioni HTTP inviate dal server. A tal fine, utilizzeremo l’estensione di Chrome denominata [Advanced Rest Client] (Chrome / Ctrl-T / Menu [Applications] / [Advanced Rest Client] - cfr. Allegati, paragrafo 22.5):
![]() |
- 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 nel formato jSON;
- in [5], viene richiesta la stessa URL, ma questa volta con un POST;
- in [7], le informazioni vengono inviate al server nella forma [urlencoded];
- in [6], il parametro name con il suo valore;
- in [8], il browser indica al server che gli sta inviando le informazioni [urlencoded];
- in [9], la risposta jSON del server;
13.4.8. Creazione di un archivio eseguibile
Ora creiamo un archivio eseguibile:
![]() |
![]() |
- in [1]: si esegue un target Maven;
- in [2]: ci sono due obiettivi (goals): [clean] per eliminare la cartella [target] dal progetto Maven, [package] per rigenerarla;
- in [3]: la cartella [target] generata verrà creata in questa cartella;
- in [4]: si genera il target;
Nei log che compaiono nella console, è importante che compaia il plugin [spring-boot-maven-plugin]. È proprio questo che genera l'archivio eseguibile.
Utilizzando una console, ci si posiziona nella cartella generata:
D:\Temp\wksSTS\gs-rest-service\target>dir
...
11/06/2014 15:30 <DIR> classes
11/06/2014 15:30 <DIR> generated-sources
11/06/2014 15:30 11 073 572 gs-rest-service-0.1.0.jar
11/06/2014 15:30 3 690 gs-rest-service-0.1.0.jar.original
11/06/2014 15:30 <DIR> maven-archiver
11/06/2014 15:30 <DIR> maven-status
...
- riga 5: l'archivio generato;
Questo archivio viene eseguito nel modo seguente:
D:\Temp\wksSTS\gs-rest-service-complete\target>java -jar gs-rest-service-0.1.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.1.0.RELEASE)
2014-06-11 15:32:47.088 INFO 4972 --- [ main] hello.Application
: Starting Application on Gportpers3 with PID 4972 (D:\Temp\wk
sSTS\gs-rest-service-complete\target\gs-rest-service-0.1.0.jar started by ST in
D:\Temp\wksSTS\gs-rest-service-complete\target)
...
Ora che l'applicazione web è avviata, è possibile accedervi tramite un browser:
![]() |
13.4.9. Distribuire l'applicazione su un server Tomcat
Come fatto per il progetto precedente, modifichiamo il file [pom.xml] nel modo 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>
<packaging>war</packaging>
...
</project>
- riga 9: occorre specificare che verrà generato un archivio WAR (Web ARchive);
È inoltre necessario configurare l’applicazione web. In assenza del file [web.xml], ciò avviene tramite una classe che eredita da [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 ridefinito (riga 8);
- riga 10: si specifica la classe che configura il progetto;
Per eseguire il progetto, si può procedere come segue:
![]() |
- in [1-2], si esegue il progetto su uno dei server registrati in IDE Eclipse;
Fatto ciò, è possibile richiamare l’URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] in un browser:
![]() |
13.4.10. Conclusione
Abbiamo presentato un tipo di progetto Spring in cui l’applicazione web invia un flusso al browser. Ora svilupperemo un'applicazione web / jSON per rendere accessibile sul web il database [dbintrospringdata] studiato nel tutorial [Introduction à Spring Data].
13.5. Pubblicazione del database [dbintrospringdata] sul web
13.5.1. Architettura del servizio web / jSON
Implementeremo la seguente architettura:
![]() |
I livelli [DAO] e [JPA] sono implementati dall’applicazione descritta nel tutorial [Introduction à Spring Data].
13.5.2. Installazione del database
![]() |
Lo script SQL [dbintrospringdata.sql] consente di creare il database MySQL necessario per i test.
13.5.3. Il progetto Eclipse del servizio web / jSON
Il progetto Eclipse del servizio web / jSON è il seguente:
![]() |
Si tratta di un progetto Maven il cui file [pom.xml] è il seguente:
<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>istia.st.webjson</groupId>
<artifactId>intro-server-webjson01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>intro-server-webjson01</name>
<description>démo spring mvc</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>istia.st.springdata</groupId>
<artifactId>intro-spring-data-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- righe 11-15: il progetto Maven padre già utilizzato per il livello [DAO];
- righe 18-22: la dipendenza dal livello [DAO];
- righe 23-26: la dipendenza dall'artefatto [spring-boot-starter-web]. Questo artefatto include tutte le dipendenze necessarie per la creazione di un servizio web / jSON. Include però anche librerie non necessarie. Sarebbe quindi necessaria una configurazione più precisa. Tuttavia, questa configurazione è utile per iniziare;
- righe 28-30: la dipendenza dall'artefatto [spring-boot-starter] consente di gestire le annotazioni Spring Boot;
Le dipendenze introdotte da questa configurazione sono le seguenti:
![]() |
- in [1], si nota che Eclipse ha rilevato la dipendenza dall’archivio del progetto [intro-spring-data-01];
Le dipendenze sopra indicate sono sia quelle del livello [DAO] che del livello [web].
13.5.3.1. Configurazione del livello [web]
Il livello [web] è configurato tramite un file [AppConfig]:
![]() |
La classe [WebConfig] configura il livello [web]:
package spring.webjson.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
// -------------------------------- configurazione del livello [web]
@Autowired
private ApplicationContext context;
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// filtri jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// mappatore jSON
ObjectMapper mapper = new ObjectMapper();
// filtri
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// risultato
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// mappatore jSON
ObjectMapper mapper = new ObjectMapper();
// filtri
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// risultato
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// mappatore jSON
ObjectMapper mapper = new ObjectMapper();
// filtri
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// risultato
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// mappatore jSON
ObjectMapper mapper = new ObjectMapper();
// filtri
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// risultato
return mapper;
}
}
- riga 18: l'annotazione [@EnableWebMvc] genera configurazioni automatiche per il framework Spring MVC;
- riga 19: la classe [WebConfig] estende la classe Spring [WebMvcConfigurerAdapter] per ridefinirne alcuni bean (righe 26-40);
- righe 22-23: iniezione del contesto Spring;
- righe 25-29: definizione del servlet del framework Spring MVC, che instrada le richieste HTTP verso il controller e il metodo corretti. [DispatcherServlet] è una classe di Spring;
- righe 31-34: si specifica che questa servlet gestisce tutte le URL;
- righe 36-39: è la presenza di questo bean che attiverà il server Tomcat presente negli archivi del progetto. Attenderà le richieste sulla porta 8080;
- righe 42-91: bean che verranno utilizzati per gestire i filtri jSON;
- righe 42-45: un mappatore jSON senza filtri;
- righe 47-57: il mappatore jSON che consente di visualizzare una categoria con i relativi prodotti. Si noti che quando si richiede una categoria con i relativi prodotti, è necessario configurare sia il filtro jSON della classe [Categorie] sia quello della classe [Produit]. È sempre così. Quando si serializza/deserializza una classe in jSON, è necessario configurare il filtro jSON della classe e quelli di tutte le dipendenze da includere in essa;
- righe 59-69: il mappatore jSON che consente di avere un prodotto con la sua categoria;
- righe 71-80: il mappatore jSON che consente di ottenere una categoria senza i relativi prodotti;
- righe 82-91: il mappatore jSON che consente di ottenere un prodotto senza la relativa categoria;
La classe [AppConfig] configura l'intera applicazione, ovvero i livelli [web] e [DAO]:
package spring.webjson.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import spring.data.config.DaoConfig;
@ComponentScan(basePackages = { "spring.webjson" })
@Import({ DaoConfig.class, WebConfig.class})
public class AppConfig {
}
- riga 9: si importano i bean del livello [DAO] e quelli del livello [web];
- riga 8: indica in quali pacchetti si trovano altri bean Spring;
Si noti che in nessun punto abbiamo utilizzato l’annotazione [@EnableAutoConfiguration]. Abbiamo preferito gestire la configurazione autonomamente.
13.5.4. Il modello dell’applicazione
![]() |
La classe [ApplicationModel] è la seguente:
package spring.webjson.models;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
@Component
public class ApplicationModel implements IDao {
// il livello [DAO]
@Autowired
private IDao dao;
@Override
public void addProduits(List<Produit> produits) {
dao.addProduits(produits);
}
@Override
public void deleteAllProduits() {
dao.deleteAllProduits();
}
@Override
public void updateProduits(List<Produit> produits) {
dao.updateProduits(produits);
}
@Override
public List<Produit> getAllProduits() {
return dao.getAllProduits();
}
@Override
public void addCategories(List<Categorie> categories) {
dao.addCategories(categories);
}
@Override
public void deleteAllCategories() {
dao.deleteAllCategories();
}
@Override
public void updateCategories(List<Categorie> categories) {
dao.updateCategories(categories);
}
@Override
public List<Categorie> getAllCategories() {
return dao.getAllCategories();
}
@Override
public Produit getProduitByIdWithCategorie(Long idProduit) {
return dao.getProduitByIdWithCategorie(idProduit);
}
@Override
public Produit getProduitByNameWithCategorie(String nom) {
return dao.getProduitByNameWithCategorie(nom);
}
@Override
public Categorie getCategorieByIdWithProduits(Long idCategorie) {
return dao.getCategorieByIdWithProduits(idCategorie);
}
@Override
public Categorie getCategorieByNameWithProduits(String nom) {
return dao.getCategorieByNameWithProduits(nom);
}
@Override
public Produit getProduitByIdWithoutCategorie(Long idProduit) {
return dao.getProduitByIdWithoutCategorie(idProduit);
}
@Override
public Categorie getCategorieByIdWithoutProduits(Long idCategorie) {
return dao.getCategorieByIdWithoutProduits(idCategorie);
}
@Override
public Produit getProduitByNameWithoutCategorie(String nom) {
return dao.getProduitByNameWithoutCategorie(nom);
}
@Override
public Categorie getCategorieByNameWithoutProduits(String nom) {
return dao.getCategorieByNameWithoutProduits(nom);
}
}
- riga 12: la classe è un singleton Spring;
- riga 13: che implementa l'interfaccia [IDao] del livello [DAO];
- righe 16-17: iniezione di un riferimento nel livello [DAO];
- righe 19-99: implementazione dell'interfaccia [IDao];
L'architettura del livello web si evolve come segue:
![]() |
- in [2b], i metodi del o dei controller comunicano con il singleton [ApplicationModel];
Questa strategia offre flessibilità nella gestione di un'eventuale cache. La classe [ApplicationModel] può essere utilizzata per memorizzare informazioni ottenute dal livello [DAO] o dati di configurazione. Ciò può risultare utile quando non si ha il controllo del livello [DAO]. Questa strategia di cache può evolversi nel tempo. Le modifiche non avranno alcun impatto sul codice del o dei controller.
13.5.5. Il controller
![]() |
![]() |
Qui abbiamo un solo controller, la classe [MyController].
13.5.5.1. Le URL esposte
Le classi URL esposte da questo controller sono le seguenti:
| Aggiunge i prodotti al database. Questi vengono salvati. La risposta è la stringa jSON, ovvero l'elenco dei prodotti aggiunti con la loro chiave primaria. |
| Elimina tutti i prodotti dal database. |
| Aggiorna i prodotti nel database. Questi vengono pubblicati. La risposta è la stringa jSON dell'elenco dei prodotti aggiornati. |
| Ottiene la stringa jSON da tutti i prodotti. |
| Aggiunge le categorie al database. Queste vengono pubblicate. Il risultato è la stringa jSON contenente l'elenco delle categorie aggiunte con la rispettiva chiave primaria. Se le categorie contengono prodotti, anche questi vengono aggiunti al database. |
| Elimina tutte le categorie dal database, insieme a tutti i prodotti in esse contenuti. Al termine, il database risulterà vuoto. |
| Aggiorna le categorie nel database. Queste vengono inviate. La risposta è l'elenco delle categorie aggiornate. Se le categorie contengono prodotti, anche questi vengono aggiornati nel database. Restituisce la stringa jSON delle categorie modificate; |
| Ottiene la stringa jSON da tutte le categorie. |
| Recupera la stringa jSON relativa a un prodotto identificato dal suo ID, insieme alla sua categoria. |
| Recupera la stringa jSON relativa a un prodotto identificato dal suo ID, senza indicarne la categoria. |
| Recupera la stringa jSON relativa a un prodotto identificato dal suo nome, insieme alla sua categoria. |
| Recupera la stringa jSON relativa a un prodotto identificato dal nome, senza indicarne la categoria. |
| Recupera la stringa jSON relativa a una categoria indicata dal suo ID, insieme ai relativi prodotti. |
| Recupera la stringa jSON relativa a una categoria specificata per nome, insieme ai relativi prodotti. |
| Recupera la stringa jSON relativa a una categoria specificata per nome, esclusi i relativi prodotti. |
| Recupera la stringa jSON di una categoria indicata dal suo ID, esclusi i relativi prodotti. |
I URL riportati corrispondono ai metodi dell'interfaccia [IDao] del livello [DAO]. I metodi del servizio web / jSON sono tutti basati sullo stesso modello. Ne esamineremo alcuni.
13.5.5.2. Lo scheletro del controller
La struttura di base del controller è la seguente:
package spring.webjson.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.CharStreams;
import spring.data.dao.DaoException;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
import spring.webjson.models.ApplicationModel;
import spring.webjson.models.Response;
@Controller
public class MyController {
// dipendenze Spring
@Autowired
private ApplicationModel application;
// filtri jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
// la classe [MyController] è un singleton e viene istanziata solo una volta il bean
public MyController() {
// System.out.println("MyController");
}
@RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addProduits(HttpServletRequest request) throws JsonProcessingException {
...
}
- riga 28: l’annotazione [@Controller] rende la classe un componente Spring;
- righe 32-33: iniezione di un riferimento alla classe [ApplicationModel];
- righe 36-50: iniezioni di riferimenti ai mappatori jSON;
- riga 58: l'interfaccia URL esposta è [/addProduits]. Il client deve utilizzare un metodo [POST] per effettuare la richiesta (method = RequestMethod.POST). Deve inviare il valore inviato sotto forma di stringa jSON (consumes = "application/json; charset=UTF-8"). Il metodo stesso restituisce la risposta al cliente (riga 59). Si tratterà di una stringa di caratteri (riga 60). L'intestazione HTTP [Content-type : application/json; charset=UTF-8] verrà inviata al client per indicargli che riceverà una stringa jSON (riga 58);
- riga 60: il metodo [addProduits] restituisce la stringa jSON dall'elenco dei prodotti aggiunti nel database;
13.5.5.3. La risposta dei metodi del controller
Tutti i metodi del controller restituiscono la seguente risposta di tipo [Response]:
![]() |
package spring.webjson.service;
import java.util.List;
public class Response<T> {
// ----------------- proprietà
// stato dell'operazione
private int status;
// eventuali messaggi di errore
private List<String> messages;
// il corpo della risposta
private T body;
// costruttori
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getter e setter
...
}
- riga 5: la risposta incapsula un tipo T;
- riga 13: la risposta di tipo T;
- righe 9-11: è possibile che un metodo generi un'eccezione. In tal caso, restituirà una risposta con:
- riga 9: status!=0;
- riga 11: l'elenco degli errori riscontrati;
13.5.5.4. L'URL [/addProduits]
L'URL [/addProduits] viene elaborato dal seguente metodo:
@RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addProduits(HttpServletRequest request) throws JsonProcessingException {
// risposta
Response<List<Produit>> response;
try {
// si recupera il valore inviato
String body = CharStreams.toString(request.getReader());
List<Produit> produits = jsonMapperProduitWithoutCategorie.readValue(body, new TypeReference<List<Produit>>() {
});
// si ristabilisce il collegamento tra prodotti e categorie
for (Produit produit : produits) {
produit.setCategorie(application.getCategorieByIdWithoutProduits(produit.getIdCategorie()));
}
// si salvano i prodotti
application.addProduits(produits);
response = new Respon se<List<Produit>>(0, null, produits);
} catch (DaoException e1) {
response = new Response<List<Produit>>(1000, e1.getErreurs(), null);
} catch (Exception e2) {
response = new Response<List<Produit>>(1000, getErreursForException(e2), null);
}
// risposta jSON
return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
- riga 3: il metodo accetta come parametro [HttpServletRequest request], che racchiude tutte le informazioni relative alla richiesta del cliente;
- riga 5: la risposta che verrà inviata al cliente: un elenco di prodotti;
- riga 8: si recupera il valore inviato via POST. La classe [CharStreams] appartiene alla libreria [Google Guava], il cui riferimento è stato aggiunto nel file [pom.xml]. Si ottiene la stringa jSON inviata dal cliente. È necessario deserializzarla per poterla utilizzare;
- righe 8-10: viene eseguita la deserializzazione. Si ottiene un elenco di prodotti in cui ogni prodotto ha un campo [categorie=null];
- righe 12-14: si reinizializza il campo [categorie] di tutti i prodotti dell’elenco. A tal fine, si utilizza il campo [idCategorie] del prodotto, che a sua volta è inizializzato;
- riga 16: i prodotti vengono inseriti nel database;
- riga 17: l'oggetto [response] viene inizializzato con l'elenco dei prodotti;
- righe 18-19: caso in cui il metodo incontri un'eccezione del livello [DAO]. Si inizializza la risposta con [status=1000] (codice di errore) [messages=e1.getMessages()], ovvero si trasmette al client l'elenco degli errori riscontrati sul lato server;
- righe 20-21: caso in cui il metodo incontri un altro tipo di eccezione. Si inizializza la risposta con [status=1000] (codice di errore) [messages=getErreursForException(e)] dove [getErreursForException] è un metodo privato della classe che restituisce l'elenco degli errori associati alle eccezioni presenti nello stack delle eccezioni di e, e [body=null];
- riga 24: viene restituita la stringa jSON della risposta;
13.5.5.5. URL [/getAllProduits]
URL [/getAllProduits] viene elaborata con il seguente metodo:
@RequestMapping(value = "/getAllProduits", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllProduits() throws JsonProcessingException {
// risposta
Response<List<Produit>> response;
try {
response = new Response<List<Produit>>(0, null, application.getAllProduits());
} catch (DaoException e1) {
response = new Response<List<Produit>>(1003, e1.getErreurs(), null);
} catch (Exception e2) {
response = new Response<List<Produit>>(1003, getErreursForException(e2), null);
}
// risposta jSON
return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
- riga 1: l'URL [/getAllProduits] viene richiesta con un'operazione [GET]. Essa produce l'jSON;
- riga 2: il metodo invia direttamente la risposta jSON al cliente;
- riga 5: il metodo invia la stringa jSON di tipo [Response<List<Produit>>];
- riga 7: i prodotti vengono richiesti senza la relativa categoria;
- righe 8-12: in caso di errore, la risposta viene inizializzata con un codice e dei messaggi di errore;
- riga 14: la risposta jSON viene inviata al cliente;
13.5.5.6. Conclusion
Non illustreremo gli altri metodi del controller. Sono simili a uno dei due metodi che abbiamo appena presentato.
13.5.6. La classe di esecuzione del servizio web / jSON
![]() |
La classe [Boot] è la classe eseguibile del progetto:
package spring.webjson.boot;
import org.springframework.boot.SpringApplication;
import spring.webjson.server.config.AppConfig;
public class Boot {
public static void main(String[] args) {
SpringApplication.run(AppConfig.class, args);
}
}
- riga 10: viene eseguito il metodo statico [SpringApplication.run]. La classe [SpringApplication] è una classe del progetto [Spring Boot] (riga 3). Le vengono passati due parametri:
- [AppConfig.class]: la classe che configura l’intera applicazione;
- [args]: eventuali argomenti passati al metodo [main] alla riga 9. Questo parametro non viene utilizzato in questo caso;
Quando si esegue questa classe, si ottengono i seguenti log:
- righe 17-19: avvio del server Tomcat che eseguirà il servizio web / jSON;
- righe 25-33: creazione del livello [DAO];
- righe 32-51: vengono individuati i URL esposti;
13.5.7. Test del servizio web / jSON
Per eseguire i test, generiamo il database MySQL [dbintrospringdata] a partire dallo script SQL [dbintrospringdata.sql]:
![]() |
Fatto ciò, utilizziamo il client [Advanced Rest Client] (cfr. paragrafo 22.5) per interrogare le URL esposte dal servizio web / jSON (il servizio web / jSON deve essere avviato).
![]() |
- in [1-3], richiediamo URL [/getAllCategories] tramite un comando HTTP GET;
Otteniamo la seguente risposta:
![]() |
- in [1], la richiesta HTTP del client;
- in [2], la risposta HTTP del server;
- in [3], lo stato [200 OK] indica che il server ha elaborato correttamente la richiesta;
- in [4], la risposta jSON del server;
La risposta completa jSON è la seguente:
{"status":0,"messages":null,"body":[{"id":415,"version":0,"nom":"categorie0","produits":[{"id":1849,"version":0,"nom":"produit00","idCategorie":415,"prix":100.0,"description":"desc00"},{"id":1850,"version":0,"nom":"produit01","idCategorie":415,"prix":101.0,"description":"desc01"},{"id":1851,"version":0,"nom":"produit02","idCategorie":415,"prix":102.0,"description":"desc02"},{"id":1852,"version":0,"nom":"produit03","idCategorie":415,"prix":103.0,"description":"desc03"},{"id":1853,"version":0,"nom":"produit04","idCategorie":415,"prix":104.0,"description":"desc04"}]},{"id":416,"version":0,"nom":"categorie1","produits":[{"id":1856,"version":0,"nom":"produit12","idCategorie":416,"prix":112.0,"description":"desc12"},{"id":1857,"version":0,"nom":"produit13","idCategorie":416,"prix":113.0,"description":"desc13"},{"id":1858,"version":0,"nom":"produit14","idCategorie":416,"prix":114.0,"description":"desc14"},{"id":1854,"version":0,"nom":"produit10","idCategorie":416,"prix":110.0,"description":"desc10"},{"id":1855,"version":0,"nom":"produit11","idCategorie":416,"prix":111.0,"description":"desc11"}]}]}
- status:0 significa che non si sono verificati errori lato server;
- messaggi: null significa che non ci sono messaggi di errore;
- body: è il corpo della risposta, in questo caso l'elenco delle categorie con i relativi prodotti. Ci sono due categorie, ciascuna con 5 prodotti;
Aggiungeremo alla categoria [categorie1] il prodotto [produit15]. A tal fine utilizzeremo il codice URL [/addCategories], che ha il seguente codice:
@RequestMapping(value = "/addCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addCategories(HttpServletRequest request) throws JsonProcessingException {
Response<List<Categorie>> response;
ObjectMapper mapper = context.getBean(ObjectMapper.class);
// si mantengono le categorie
try {
// si recupera il valore inserito
String body = CharStreams.toString(request.getReader());
mapper.setFilters(jsonFilterCategorieWithProduits);
List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
});
// si ristabilisce il collegamento tra prodotti e categorie
for (Categorie categorie : categories) {
Set<Produit> produits = categorie.getProduits();
if (produits != null) {
for (Produit produit : categorie.getProduits()) {
produit.setCategorie(categorie);
}
}
}
// si salvano le categorie
application.addCategories(categories);
response = new Response<List<Categorie>>(0, null, categories);
} catch (Exception e) {
response = new Response<List<Categorie>>(1004, getErreursForException(e), null);
}
// risposta jSON
return mapper.writeValueAsString(response);
}
- riga 1: il cliente deve inserire un POST e il valore inviato deve essere una stringa jSON;
- righe 9-12: il valore inviato deve essere un elenco di categorie con i relativi prodotti;
Creeremo una categoria [categorie2] con un prodotto [produit21]. La stringa jSON da inviare è quindi la seguente:
[{"id":null,"version":0,"nom":"categorie2","produits":[{"id":null,"version":0,"nom":"produit21","idCategorie":null,"prix":111.0,"description":"desc21"}]}]
La richiesta al servizio web /jSON viene effettuata nel modo seguente:
![]() |
- in [1], l’URL richiesto;
- in [2], viene richiesta tramite un'operazione POST;
- in [3], la stringa jSON inviata;
- in [4], si comunica al server che gli verrà inviato jSON;
La risposta del server è la seguente:
![]() |
- in [1], si vede che sia la categoria che il prodotto hanno ora una chiave primaria, il che indica che sono stati probabilmente inseriti nel database. Lo verificheremo utilizzando URL e [/getCategorieByNameWithProduits/categorie2]:
![]() |
Otteniamo il seguente risultato:
![]() |
Abbiamo effettivamente ottenuto la categoria [categorie2] con il suo unico prodotto [produit21]. È anche possibile richiedere solo il prodotto. A tal fine utilizziamo il codice URL [/getProduitByIdWithoutCategorie/1859]:
![]() |
Otteniamo il seguente risultato:
![]() |
Tutte le operazioni [GET] possono essere eseguite in un semplice browser:
![]() |
Il lettore è invitato a provare le altre operazioni URL del servizio web / json.
13.6. Un client programmato per il servizio web / jSON
Ora che la base [dbintrospringdata] è disponibile sul web, scriveremo un’applicazione che la sfrutti. Avremo quindi la seguente architettura client/server:
![]() |
L’applicazione client avrà due livelli:
- un livello [DAO] [2] per comunicare con l’applicazione web / jSON che espone il database;
- un livello di test JUnit [1] per verificare che il client e il server funzionino correttamente;
13.6.1. Il progetto Eclipse
Il progetto Eclipse del client è il seguente:
![]() |
- la cartella [src/main/java] implementa il livello [DAO];
- la cartella [src/test/java] implementa i test JUnit;
13.6.2. Configurazione Maven del progetto
Il progetto è un progetto Maven configurato dal seguente file [pom.xml]:
<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>istia.st.webjson</groupId>
<artifactId>intro-client-webjson-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>Client console du serveur web / jSON</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- libreria jSON utilizzata da Spring -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- componente utilizzato da Spring RestTemplate -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
<scope>test</scope>
</dependency>
<!-- libreria di log -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- plugin -->
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
<name>intro-client-webjson-01</name>
</project>
- righe 14-18: il progetto Maven padre [spring-boot-starter-parent] che ci permette di definire una serie di dipendenze senza specificarne le versioni, poiché queste sono definite nel progetto padre;
- righe 22-25: sebbene non stiamo sviluppando un'applicazione web, abbiamo bisogno della dipendenza [spring-web] che include la classe [RestTemplate], la quale consente di interfacciarsi facilmente con un'applicazione web / jSON;
- righe 27-34: una libreria jSON;
- righe 36-39: una dipendenza che ci consentirà di associare un timeout alle richieste HTTP del client. Un timeout è il tempo massimo di attesa per la risposta del server. Superato tale tempo, il client segnala un errore timeout generando un'eccezione;
- righe 41-46: la libreria Google Guava utilizzata nel test JUnit. Per questo motivo, ne abbiamo impostato l'ambito a [test] (riga 45). Ciò significa che questa dipendenza viene inclusa solo durante l'esecuzione del codice del ramo [src/test/java];
- righe 48-51: la libreria di log;
- righe 52-63: la dipendenza per i test JUnit. Essa include in particolare la libreria JUnit 4 necessaria per i test. Queste dipendenze hanno l’attributo [<scope>test</scope>] che indica che sono necessarie solo per la fase di test. Non sono incluse nell’archivio finale del progetto;
13.6.3. Implementazione del livello [DAO]
![]() |
![]() |
- il pacchetto [spring.client.config] contiene la configurazione Spring del livello [DAO];
- il pacchetto [spring.client.dao] contiene l'implementazione del livello [DAO];
- il pacchetto [spring.client.entities] contiene gli oggetti scambiati con il servizio web / jSON;
13.6.3.1. Configuration
![]() |
La classe [DaoConfig] gestisce la configurazione Spring del livello [DAO]. Il suo codice è il seguente:
package spring.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@ComponentScan({ "spring.client.dao" })
public class DaoConfig {
// costanti
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8080";
@Bean
public RestTemplate restTemplate(int timeout) {
// creazione del componente RestTemplate
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// timeout delle comunicazioni
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// risultato
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
// filtri jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// mappatore jSON
ObjectMapper mapper = new ObjectMapper();
// filtri
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// risultato
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// mappatore jSON
ObjectMapper mapper = new ObjectMapper();
// filtri
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// risultato
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// mappatore jSON
ObjectMapper mapper = new ObjectMapper();
// filtri
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// risultato
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// mappatore jSON
ObjectMapper mapper = new ObjectMapper();
// filtri
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// risultato
return mapper;
}
}
- riga 13: la classe è una classe di configurazione Spring; i componenti Spring si trovano nel pacchetto [spring.client.dao];
- riga 17: si imposta un timeout di un secondo (1000 ms);
- righe 32-35: il bean che restituisce questo valore;
- riga 18: l'URL del servizio web / jSON;
- righe 37-40: il bean che restituisce questo valore;
- righe 20-30: la configurazione della classe [RestTemplate] che gestisce le comunicazioni con il servizio web / jSON. Quando non è necessario configurarla, è possibile utilizzarla nel codice semplicemente indicando [new RestTemplate()]. In questo caso, vogliamo impostare il timeout per gli scambi con il servizio web / jSON. Il bean [timeout] della riga 36 viene passato come parametro al metodo [restTemplate] della riga 24;
- riga 23: il componente [HttpComponentsClientHttpRequestFactory] è quello che ci permette di impostare il timeout degli scambi (righe 29-30);
- riga 24: la classe [RestTemplate] è costruita con questo componente. Poiché si basa su di esso per comunicare con il servizio web / jSON, gli scambi saranno effettivamente soggetti al timeout;
- il client e il server si scambieranno righe di testo. Un convertitore si occupa di serializzare un oggetto in testo e, viceversa, di deserializzare un testo in oggetto. Possono esserci diversi convertitori associati alla classe [RestTemplate] e quello scelto in un dato momento dipende dalle intestazioni HTTP inviate dal server. In questo caso, non avremo alcun convertitore. Pertanto, il componente [RestTemplate] non tenterà in alcun modo di convertire i due elementi seguenti:
- il testo inviato;
- il testo ricevuto in risposta;
Questi testi saranno stringhe jSON che saranno quindi lasciate così come sono dal componente [RestTemplate]. Saremo noi sviluppatori a occuparci delle serializzazioni/deserializzazioni jSON necessarie. Questo perché i filtri da applicare al valore inviato e alla risposta ricevuta possono essere diversi e l’esperienza dimostra che è più facile gestirli autonomamente piuttosto che cercare di configurare il componente [RestTemplate] affinché utilizzi il convertitore corretto jSON;
- righe 42-92: definiscono i filtri jSON. Sono gli stessi di quelli del server presentati e spiegati nel paragrafo 13.5.3.1;
- righe 43-46: un mappatore jSON senza filtri;
- righe 64-68: un mappatore jSON per ottenere una categoria senza i relativi prodotti;
- righe 48-58: un mappatore jSON per ottenere una categoria con i relativi prodotti;
- righe 83-92: un mappatore jSON per ottenere un prodotto senza la relativa categoria;
- righe 60-70: un mappatore jSON per ottenere un prodotto con la relativa categoria;
Tutti questi bean saranno disponibili nei codici del livello [DAO] e nel test Junit.
13.6.3.2. Le entità
![]() |
Le entità gestite dal livello [DAO] sono quelle che esso scambia con il servizio web / jSON. Si tratta degli articoli e dei prodotti. Sul lato server, queste entità presentavano annotazioni di persistenza JPA. In questo caso, tali annotazioni sono state rimosse. Riportiamo il codice delle entità a titolo di promemoria:
[AbstractEntity]
package spring.client.entities;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public abstract class AbstractEntity {
// proprietà
protected Long id;
protected Long version;
// costruttori
public AbstractEntity() {
}
public AbstractEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
// ridefinizione di [equals] e [hashcode]
@Override
public int hashCode() {
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return id != null && this.id == other.id.longValue();
}
// firma jSON
public String toString() {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(this);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
// getter e setter
...
}
[Categorie]
package spring.client.entities;
import java.util.HashSet;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractEntity {
// proprietà
private String nom;
// prodotti correlati
public Set<Produit> produits = new HashSet<Produit>();
// costruttori
public Categorie() {
}
public Categorie(String nom) {
this.nom = nom;
}
// metodi
public void addProduit(Produit produit) {
// si aggiunge il prodotto
produits.add(produit);
// si imposta la categoria
produit.setCategorie(this);
}
// getter e setter
...
}
[Produit]
package spring.webjson.client.entities;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {
// il nome
private String nom;
// il numero della categoria
private Long idCategorie;
// il prezzo
private double prix;
// la descrizione
private String description;
// la categoria
private Categorie categorie;
// produttori
public Produit() {
}
public Produit(String nom, double prix, String description) {
this.nom = nom;
this.prix = prix;
this.description = description;
}
// getter e setter
...
}
13.6.3.3. La classe [DaoException]
![]() |
Quando il livello [DAO] rileva un errore, genera un'eccezione di tipo [DaoException]. Questa classe è quella utilizzata sul lato server e descritta al paragrafo 11.3.7.
13.6.3.4. L'interfaccia del livello [DAO]
![]() |
Il livello [DAO] presenta l’interfaccia [IDao] descritta al paragrafo 11.3.7.
package spring.client.dao;
import java.util.List;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
public interface IDao {
// inserimento di un elenco di prodotti
public List<Produit> addProduits(List<Produit> produits);
// eliminazione di tutti i prodotti
public void deleteAllProduits();
// aggiornamento di un elenco di prodotti
public List<Produit> updateProduits(List<Produit> produits);
// recupero di tutti i prodotti
public List<Produit> getAllProduits();
// inserimento di un elenco di categorie
public List<Categorie> addCategories(List<Categorie> categories);
// eliminazione di tutte le categorie
public void deleteAllCategories();
// aggiornamento di un elenco di categorie
public List<Categorie> updateCategories(List<Categorie> categories);
// recupero di tutte le categorie
public List<Categorie> getAllCategories();
// un prodotto specifico
public Produit getProduitByIdWithCategorie(Long idProduit);
public Produit getProduitByIdWithoutCategorie(Long idProduit);
public Produit getProduitByNameWithCategorie(String nom);
public Produit getProduitByNameWithoutCategorie(String nom);
// una categoria specifica
public Categorie getCategorieByIdWithProduits(Long idCategorie);
public Categorie getCategorieByIdWithoutProduits(Long idCategorie);
public Categorie getCategorieByNameWithProduits(String nom);
public Categorie getCategorieByNameWithoutProduits(String nom);
}
13.6.3.5. La risposta del servizio web / jSON
![]() |
Abbiamo visto che tutte le URL del servizio web / jSON restituivano un tipo [Response] definito al paragrafo 13.5.5.3. Riportiamo qui di seguito questa classe:
package spring.client.dao;
import java.util.List;
public class Response<T> {
// ----------------- proprietà
// stato dell'operazione
private int status;
// eventuali messaggi di errore
private List<String> messages;
// il corpo della risposta
private T body;
// costruttori
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getter e setter
...
}
13.6.3.6. Implementazione degli scambi con il servizio web / jSON
![]() |
La classe [AbstractDao] implementa gli scambi con il servizio web / jSON:
package spring.client.dao;
import java.net.URI;
import java.net.URISyntaxException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.web.client.RestTemplate;
public abstract class AbstractDao {
// dati
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// richiesta generica
protected String getResponse(String url, String jsonPost) {
// URL: URL da contattare
// jsonPost: il valore jSON da inviare
try {
// esecuzione della richiesta
RequestEntity<?> request;
if (jsonPost != null) {
// richiesta POST
request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
} else {
// richiesta GET
request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
.accept(MediaType.APPLICATION_JSON).build();
}
// si esegue la query
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e1) {
throw new DaoException(20, e1);
} catch (RuntimeException e2) {
throw new DaoException(21, e2);
}
}
}
- righe 15-16: inserimento del componente [RestTemplate] che garantisce la comunicazione con il server;
- righe 17-18: iniezione del componente URL del servizio web / jSON;
L'implementazione dei metodi di comunicazione con il server è fattorizzata nel metodo [getResponse]:
- riga 21: il metodo riceve 2 parametri:
- [url]: il URL richiesto;
- [jsonPost]: la stringa jSON da inviare, altrimenti null. Se [jsonPost==null], la richiesta di URL viene effettuata con un GET, altrimenti con un POST;
- riga 38: l’istruzione che invia la richiesta al server e ne riceve la risposta. Il componente [RestTemplate] offre un numero significativo di metodi di scambio con il server. Qui abbiamo scelto il metodo [exchange], ma ne esistono altri;
- righe 27-36: dobbiamo costruire la richiesta di tipo [RequestEntity]. Essa varia a seconda che si utilizzi un GET o un POST per effettuare la richiesta;
- righe 30-31: la richiesta per un GET. La classe [RequestEntity] offre metodi statici per creare le richieste GET, POST, HEAD,... Il metodo [RequestEntity.get] consente di creare una richiesta GET concatenando i diversi metodi che la compongono:
- il metodo [RequestEntity.get] accetta come parametro l'URL di destinazione sotto forma di un'istanza URI,
- il metodo [accept] consente di definire gli elementi dell’intestazione HTTP [Accept]. Qui specifichiamo che accettiamo il tipo [application/json] che il server invierà;
- il metodo [build] utilizza queste diverse informazioni per costruire il tipo [RequestEntity] della richiesta;
- righe 34-35: la richiesta per un POST. Il metodo [RequestEntity.post] consente di creare una richiesta POST concatenando i diversi metodi che la compongono:
- il metodo [RequestEntity.post] accetta come parametro il URL di destinazione sotto forma di un'istanza URI,
- il metodo [header] definisce un'intestazione HTTP. In questo caso si invia al server l'intestazione [Content-Type: application/json] per indicargli che il valore inviato gli perverrà sotto forma di una stringa jSON;
- il metodo [accept] consente di indicare che accettiamo il tipo [application/json] che il server invierà;
- il metodo [body] imposta il valore inviato via POST. Si tratta del quarto parametro del metodo generico [getResponse] (riga 1);
- riga 38: il metodo [RestTemplate].exchange restituisce un tipo [ResponseEntity<String>] che incapsula l'intera risposta del server: intestazioni HTTP e corpo del documento. Il metodo [ResponseEntity].getBody() consente di ottenere questo corpo che rappresenta la risposta del server, in questo caso una stringa di caratteri;
13.6.3.7. Implementazione dell’interfaccia [IDao]
![]() |
La classe [Dao] implementa l’interfaccia [IDao]:
package spring.client.dao;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
@Component
public class Dao extends AbstractDao implements IDao {
@Autowired
private ApplicationContext context;
// filtri jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
@Override
public List<Produit> addProduits(List<Produit> produits) {
// ----------- aggiungere prodotti (senza la relativa categoria)
...
}
- riga 17: la classe [Dao] è un componente Spring in cui è quindi possibile iniettare altri componenti Spring;
- riga 18: la classe [Dao] estende la classe [AbstractDao] che abbiamo appena visto e implementa l'interfaccia [IDao];
- righe 20-21: si inietta il contesto Spring per poter accedere ai suoi bean;
- righe 24-38: inserimento dei mappatori jSON definiti nella classe [AppConfig] descritta al paragrafo 13.6.2;
Le implementazioni dei diversi metodi dell’interfaccia [IDao] seguono tutte lo stesso schema. Presenteremo due metodi, uno basato su un’operazione [POST], l’altro su un’operazione [GET].
Un esempio di [GET]: [getCategorieByNameWithProduits]
@Override
public Categorie getCategorieByNameWithProduits(String nom) {
// ----------- recuperare una categoria specificata per nome, con i relativi prodotti
try {
// richiesta
Response<Categorie> response = jsonMapperCategorieWithProduits.readValue(
getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null),
new TypeReference<Response<Categorie>>() {
});
// errore?
if (response.getStatus() != 0) {
// viene generata 1 eccezione
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// viene restituito il corpo della risposta del server
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(113, e2);
}
}
- riga 7: viene chiamato il metodo [getResponse] della classe padre. È questo metodo che gestisce le comunicazioni con il servizio web / jSON. I suoi parametri sono i seguenti:
getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null)
- (continua)
- l'ID URL del servizio interrogato [/getCategorieByNameWithProduits/nom];
- il valore inviato. In questo caso non ce n’è;
Il metodo [getResponse] restituisce un tipo String che corrisponde alla risposta jSON inviata dal server. Si deserializza questa risposta jSON nel modo seguente:
jsonMapperCategorieWithProduits.readValue(
jsonResponse,
new TypeReference<Response<Categorie>>() {
});
poiché la stringa jSON è la serializzazione di un tipo [Response<Categorie>];
- righe 11-17: si verifica lo stato della risposta. Se lo stato è diverso da 0, significa che si è verificato un errore lato server. Si genera quindi un’eccezione (riga 13), riprendendo le informazioni contenute nella risposta (stato ed elenco dei messaggi di errore);
- riga 16: se non si è verificato alcun errore lato server, si restituisce il corpo del tipo [Response<Categorie>], ovvero la categoria richiesta;
- righe 18-19: si gestisce l'eccezione generata alla riga 16;
- righe 20-22: gestiscono tutte le altre eccezioni;
Un esempio di [POST]: [addCategories]
@Override
public List<Categorie> addCategories(List<Categorie> categories) {
// ----------- aggiungi categorie (con i relativi prodotti)
try {
// richiesta
Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories)),
new TypeReference<Response<List<Categorie>>>() {
});
// errore?
if (response.getStatus() != 0) {
// viene generata un'eccezione
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// viene restituito il corpo della risposta del server
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(104, e2);
}
}
- riga 2: il metodo [addCategories] serve a salvare nel database le categorie passate come parametri. Queste stesse categorie vengono arricchite con le loro chiavi primarie. Se le categorie vengono passate insieme a dei prodotti, anche questi ultimi vengono salvati;
- riga 7: si chiama il metodo [getResponse] del genitore per effettuare gli scambi con il servizio web / jSON;
- il primo parametro è URL [/addCategories];
- il secondo parametro è il valore inviato, in questo caso l'elenco delle categorie da salvare;
getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories))
La stringa jSON ottenuta viene quindi deserializzata per ottenere il tipo [Response<List<Categorie>] previsto:
Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
jsonResponse,
new TypeReference<Response<List<Categorie>>>() {
});
- righe 11-17: gestione della risposta del server (errore o meno);
- righe 20-22: gestione delle eccezioni;
Tutti gli altri metodi seguono lo schema dei due metodi presentati.
13.6.4. Il test JUnit
Torniamo all’architettura client/server in fase di sviluppo:
![]() |
Abbiamo realizzato un livello [DAO] [2] con la stessa interfaccia del livello [DAO] [4]. Per testare il livello [DAO] [2], è quindi possibile utilizzare il test JUnit che è stato utilizzato per testare il livello [DAO] [4]. Ricordiamo che il test è il seguente:
![]() |
package spring.client.junit;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import spring.client.config.DaoConfig;
import spring.client.dao.DaoException;
import spring.client.dao.IDao;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {
// livello [DAO]
@Autowired
private IDao dao;
// filtri jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
@Before
public void cleanAndFill() {
// si pulisce il database prima di ogni test
log("Vidage de la base de données", 1);
// si svuota la tabella [CATEGORIES] - di conseguenza, la tabella [PRODUITS] verrà svuotata
dao.deleteAllCategories();
// --------------------------------------------------------------------------------------
log("Remplissage de la base", 1);
// si compilano le tabelle
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < 2; i++) {
Categorie categorie = new Categorie(String.format("categorie%d", i));
for (int j = 0; j < 5; j++) {
categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
String.format("desc%d%d", i, j)));
}
categories.add(categorie);
}
// Aggiunta della categoria - per effetto a cascata verranno inseriti anche i prodotti
categories = dao.addCategories(categories);
}
@Test
public void showDataBase() throws BeansException, JsonProcessingException {
// elenco delle categorie
log("Liste des catégories", 2);
List<Categorie> categories = dao.getAllCategories();
affiche(categories, jsonMapperCategorieWithoutProduits);
// elenco dei prodotti
log("Liste des produits", 2);
List<Produit> produits = dao.getAllProduits();
affiche(produits, jsonMapperProduitWithoutCategorie);
// alcune verifiche
Assert.assertEquals(2, categories.size());
Assert.assertEquals(10, produits.size());
Categorie categorie = findCategorieByName("categorie0", categories);
Assert.assertNotNull(categorie);
Produit produit = findProduitByName("produit03", produits);
Assert.assertNotNull(produit);
Long idCategorie = produit.getIdCategorie();
Assert.assertEquals(categorie.getId(), idCategorie);
}
@Test
public void getCategorieByNameWithProduits() {
log("getCategorieByNameWithProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Assert.assertNotNull(categorie1);
Assert.assertEquals(5, categorie1.getProduits().size());
}
@Test
public void getCategorieByNameWithoutProduits() {
log("getCategorieByNameWithoutProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithoutProduits("categorie1");
Assert.assertNotNull(categorie1);
Assert.assertEquals("categorie1", categorie1.getNom());
}
@Test
public void getCategorieByIdWithProduits() {
log("getCategorieByIdWithProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie2 = dao.getCategorieByIdWithProduits(categorie1.getId());
Assert.assertNotNull(categorie2);
Assert.assertEquals(categorie1.getId(), categorie2.getId());
Assert.assertEquals(categorie1.getNom(), categorie2.getNom());
}
@Test
public void getCategorieByIdWithoutProduits() {
log("getCategorieByIdWithoutProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie2 = dao.getCategorieByIdWithoutProduits(categorie1.getId());
Assert.assertNotNull(categorie2);
Assert.assertEquals(categorie1.getNom(), categorie2.getNom());
}
@Test
public void getProduitByNameWithCategorie() {
log("getProduitByNameWithCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Assert.assertNotNull(produit);
Assert.assertNotNull(produit.getCategorie());
}
@Test
public void getProduitByNameWithoutCategorie() {
log("getProduitByNameWithoutCategorie", 1);
Produit produit = dao.getProduitByNameWithoutCategorie("produit03");
Assert.assertNotNull(produit);
Assert.assertEquals("produit03", produit.getNom());
}
@Test
public void getProduitByIdWithCategorie() {
log("getProduitByNameWithCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Produit produit2 = dao.getProduitByIdWithCategorie(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
Assert.assertEquals(produit.getCategorie().getId(), produit2.getCategorie().getId());
}
@Test
public void getProduitByIdWithoutCategorie() {
log("getProduitByIdWithoutCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Produit produit2 = dao.getProduitByIdWithoutCategorie(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
}
@Test
public void doInsertsInTransaction() {
log("Ajout d'une catégorie [cat1] avec deux produits de même nom", 1);
// si procede all'inserimento
Categorie categorie = new Categorie("cat1");
categorie.addProduit(new Produit("x", 1.0, ""));
categorie.addProduit(new Produit("x", 1.0, ""));
// aggiunta della categoria - di conseguenza verranno inseriti anche i prodotti
try {
categorie = dao.addCategories(Lists.newArrayList(categorie)).get(0);
} catch (DaoException e) {
show("Les erreurs suivantes se sont produites :", e.getErreurs());
}
// controlli
List<Categorie> categories = dao.getAllCategories();
Assert.assertEquals(2, categories.size());
List<Produit> produits = dao.getAllProduits();
Assert.assertEquals(10, produits.size());
}
@Test
public void updateDataBase() {
log("Mise à jour du prix des produits de [categorie1]", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie1Saved = dao.getCategorieByNameWithProduits("categorie1");
Set<Produit> produits = categorie1.getProduits();
for (Produit produit : produits) {
produit.setPrix(1.1 * produit.getPrix());
}
List<Produit> produits2 = Lists.newArrayList(produits);
produits2 = dao.updateProduits(produits2);
// controlli
List<Produit> produitsSaved = Lists.newArrayList(categorie1Saved.getProduits());
for (Produit produit2 : produits2) {
Produit produit = findProduitByName(produit2.getNom(), produitsSaved);
Assert.assertEquals(produit2.getPrix(), produit.getPrix() * 1.1, 1e-6);
}
}
@Test
public void addProduits() throws BeansException, JsonProcessingException {
log("Ajout de deux produits de catégorie [categorie0]", 1);
Categorie categorie0 = dao.getCategorieByNameWithoutProduits("categorie0");
Long idCategorie = categorie0.getId();
Produit p1 = new Produit("x", 1, "");
p1.setIdCategorie(idCategorie);
p1.setCategorie(categorie0);
Produit p2 = new Produit("y", 1, "");
p2.setIdCategorie(idCategorie);
p2.setCategorie(categorie0);
List<Produit> produits = new ArrayList<Produit>();
produits.add(p1);
produits.add(p2);
produits = dao.addProduits(produits);
// verifica
affiche(produits, jsonMapperProduitWithoutCategorie);
}
// -------------- metodi privati
private Produit findProduitByName(String nom, List<Produit> produits) {
for (Produit produit : produits) {
if (produit.getNom().equals(nom)) {
return produit;
}
}
return null;
}
private Categorie findCategorieByName(String nom, List<Categorie> categories) {
for (Categorie categorie : categories) {
if (categorie.getNom().equals(nom)) {
return categorie;
}
}
return null;
}
// visualizzazione di un elemento di tipo T
static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(element));
}
// visualizzazione di un elenco di elementi di tipo T
static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
for (T element : elements) {
affiche(element, jsonMapper);
}
}
private static void log(String message, int mode) {
// visualizza messaggio
String toPrint = null;
switch (mode) {
case 1:
toPrint = String.format("%s --------------------------------", message);
break;
case 2:
toPrint = String.format("-- %s", message);
break;
}
System.out.println(toPrint);
}
private static void show(String title, List<String> messages) {
// titolo
System.out.println(String.format("%s : ", title));
// messaggi
for (String message : messages) {
System.out.println(String.format("- %s", message));
}
}
}
La sua esecuzione va a buon fine e restituisce i seguenti risultati sulla console:
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Ajout de deux produits de catégorie [categorie0] --------------------------------
{"id":6285,"version":0,"nom":"x","idCategorie":1319,"prix":1.0,"description":""}
{"id":6286,"version":0,"nom":"y","idCategorie":1319,"prix":1.0,"description":""}
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Mise à jour du prix des produits de [categorie1] --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByIdWithoutProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithoutCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByNameWithProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByNameWithoutProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByIdWithoutCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
-- Liste des catégories
{"id":1337,"version":0,"nom":"categorie0"}
{"id":1338,"version":0,"nom":"categorie1"}
-- Liste des produits
{"id":6367,"version":0,"nom":"produit00","idCategorie":1337,"prix":100.0,"description":"desc00"}
{"id":6368,"version":0,"nom":"produit01","idCategorie":1337,"prix":101.0,"description":"desc01"}
{"id":6369,"version":0,"nom":"produit02","idCategorie":1337,"prix":102.0,"description":"desc02"}
{"id":6370,"version":0,"nom":"produit03","idCategorie":1337,"prix":103.0,"description":"desc03"}
{"id":6371,"version":0,"nom":"produit04","idCategorie":1337,"prix":104.0,"description":"desc04"}
{"id":6372,"version":0,"nom":"produit10","idCategorie":1338,"prix":110.0,"description":"desc10"}
{"id":6373,"version":0,"nom":"produit11","idCategorie":1338,"prix":111.0,"description":"desc11"}
{"id":6374,"version":0,"nom":"produit12","idCategorie":1338,"prix":112.0,"description":"desc12"}
{"id":6375,"version":0,"nom":"produit13","idCategorie":1338,"prix":113.0,"description":"desc13"}
{"id":6376,"version":0,"nom":"produit14","idCategorie":1338,"prix":114.0,"description":"desc14"}
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByIdWithProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Ajout d'une catégorie [cat1] avec deux produits de même nom --------------------------------
Les erreurs suivantes se sont produites :
- org.hibernate.exception.ConstraintViolationException: could not execute statement
- could not execute statement
- Duplicate entry 'x' for key 'NOM'
11:24:37.650 [Thread-1] INFO o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@f8c1ddd: startup date [Fri Nov 20 11:24:34 CET 2015]; root of context hierarchy



























































