13. [Corso]: Pubblicazione di un database sul Web con Spring MVC
Parole chiave: architettura multilivello, Spring, iniezione di dipendenze, servizio web / JSON, client / server
13.1. Assistenza
![]() | ![]() |
I progetti relativi a questo capitolo si trovano nella cartella [support / chap-13]. Lo script SQL [dbintrospringdata.sql] crea il database MySQL necessario per i test.
13.2. Il ruolo di Spring MVC in un'applicazione web
Inquadriamo Spring MVC nello sviluppo di un'applicazione web. Molto spesso, essa sarà costruita su un'architettura a più livelli come la seguente:
![]() |
- il livello [Web] è quello a contatto con l'utente dell'applicazione web. L'utente interagisce con l'applicazione web tramite pagine web visualizzate in un browser. Spring MVC si trova in questo livello e solo in questo livello;
- il livello [business] implementa la logica di business 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 dal DBMS 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 nel DBMS. Il livello [ORM] funge da ponte tra gli oggetti gestiti dal livello [DAO] e le righe e le colonne delle tabelle in un database relazionale. La specifica JPA (Java Persistence API) consente l'astrazione dall'ORM utilizzato, a condizione che implementi tali specifiche. Questo sarà il caso in esame e d'ora in poi ci riferiremo al livello ORM come al livello JPA;
- L'integrazione dei livelli è gestita dal framework Spring;
13.3. Il modello di sviluppo Spring MVC
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 [Front Controller] utilizza un file di configurazione o annotazioni Java per "instradare" la richiesta al controller corretto e all'azione corretta all'interno di quel controller. A tal fine, utilizza il campo [Action] dell'URL. Il resto dell'URL [/param1/param2/...] è costituito da parametri opzionali che verranno passati all'azione. La C in MVC qui si riferisce alla catena [Front Controller, Controller, Action]. Se nessun controller è in grado di gestire l'azione richiesta, il server web risponderà che l'URL richiesto non è stato trovato.
- Elaborazione
- L'azione selezionata può utilizzare i parametri che il [Front Controller] le ha passato. Questi possono provenire da diverse fonti:
- il percorso [/param1/param2/...] dell'URL,
- i parametri [p1=v1&p2=v2] dell'URL,
- 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;
Ora, chiariamo la relazione tra l'architettura web MVC e l'architettura a livelli. A seconda di come è definito il modello, questi due concetti possono essere correlati o meno. Consideriamo un'applicazione web Spring MVC a livello singolo:
![]() |
Se implementiamo il livello [Web] con Spring MVC, avremo effettivamente un'architettura web MVC ma non un'architettura a più livelli. In questo caso, il livello [Web] gestirà tutto: presentazione, logica di business e accesso ai dati. Saranno le azioni a svolgere questo lavoro.
Ora, consideriamo un'architettura web multistrato:
![]() |
Il livello [Web] può essere implementato senza un framework e senza seguire il modello MVC. In questo caso, abbiamo comunque un'architettura a più livelli, ma il livello Web non implementa il modello MVC.
Ad esempio, nell'ambiente .NET, il livello [Web] sopra descritto può essere implementato utilizzando ASP.NET MVC, ottenendo un'architettura a livelli con un livello [Web] in stile MVC. Detto questo, questo livello ASP.NET MVC può essere sostituito con un livello ASP.NET classico (WebForms) mantenendo invariato il resto (logica di business, DAO, ORM). Si ottiene così un'architettura a livelli con un livello [Web] che non è più basato su MVC.
In MVC, abbiamo detto che il modello M era quello della vista V, ovvero l'insieme di dati visualizzati dalla vista V. Viene fornita un'altra definizione del modello M in MVC:
![]() |
Molti autori ritengono che ciò che si trova a destra del livello [Web] costituisca il modello M dell'MVC. Per evitare ambiguità, possiamo fare riferimento al:
- il modello di dominio quando ci si riferisce a tutto ciò che si trova a destra del livello [Web]
- il modello di vista quando ci riferiamo ai dati visualizzati da una vista V
D'ora in poi, il termine "modello M" si riferirà esclusivamente al modello di una vista V.
13.4. Un progetto Web/JSON con Spring MVC
Il sito [http://spring.io/guides] offre tutorial introduttivi per esplorare l'ecosistema Spring. Ne seguiremo uno per scoprire la configurazione Maven necessaria per un progetto Spring MVC.
13.4.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 dati JSON sono spesso chiamati servizi REST (REpresentational State Transfer). Un servizio è detto RESTful se segue 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 specifichi il tipo di file prodotto dalla build di Maven. In sua assenza, viene utilizzato il tipo [jar]. L'applicazione è quindi un'applicazione eseguibile basata su console, 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 definisce la maggior parte delle dipendenze del progetto. Esse potrebbero essere sufficienti, nel qual caso non vengono aggiunte dipendenze aggiuntive, oppure potrebbero non esserlo, nel qual caso vengono aggiunte le dipendenze mancanti;
- Righe 17–20: L'artefatto [spring-boot-starter-web] include le librerie necessarie per un progetto di servizio web Spring MVC in cui non vengono generate viste. Questo artefatto include un numero molto elevato di librerie, comprese quelle per un server Tomcat incorporato. L'applicazione verrà eseguita su questo server;
Le librerie incluse in questa configurazione sono molto numerose:
![]() | ![]() |
Sopra, vediamo 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 come segue:
![]() |
- 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;
13.4.4. Il controller C
![]() |
L'applicazione importata dispone del 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, il che significa che i suoi metodi sono registrati per gestire gli URL. Abbiamo già visto l'annotazione simile [@Controller]. Il tipo di ritorno dei metodi di quel controller era [String], ovvero il nome della vista da visualizzare. Qui è diverso. I metodi di un [@RestController] restituiscono oggetti che vengono serializzati per essere inviati al browser. Il tipo di serializzazione eseguita dipende dalla configurazione di Spring MVC. Qui, saranno serializzati in JSON. È la presenza di una libreria JSON nelle dipendenze del progetto che fa sì che Spring Boot configuri automaticamente il progetto in questo modo;
- riga 14: l'annotazione [@RequestMapping] specifica l'URL gestito dal metodo, 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 l'accesso concorrente. Più thread potrebbero voler incrementare la variabile [counter] contemporaneamente. Ciò verrà gestito correttamente. Un thread può leggere il valore del contatore solo una volta che il thread che lo sta modificando ha terminato la modifica.
13.4.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
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à incontrato e spiegato questo codice nell'esempio precedente.
13.4.7. Esecuzione del progetto
![]() |
Otteniamo i seguenti log della console:
- riga 13: il server Tomcat si avvia sulla porta 8080 (riga 12);
- riga 17: il servlet [DispatcherServlet] è presente;
- riga 20: 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 l'estensione di Chrome chiamata [Advanced Rest Client] (Chrome / Ctrl-T / menu [Applicazioni] / [Advanced Rest Client] - vedi Appendici, 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 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" e il suo valore;
- in [8], il browser comunica al server che sta inviando informazioni [urlencoded];
- in [9], la risposta JSON del server;
13.4.8. Creazione di un archivio eseguibile
Ora creeremo un archivio eseguibile:
![]() |
![]() |
- in [1]: eseguiamo un target 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]: generiamo il target;
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:
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 come segue:
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 è in esecuzione, è possibile accedervi utilizzando un browser:
![]() |
13.4.9. Distribuzione dell'applicazione su un server Tomcat
Come abbiamo fatto per il progetto precedente, modifichiamo 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>
...
</project>
- Riga 9: È necessario specificare che si intende generare un file WAR (Web Archive);
È 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-2], eseguire il progetto su uno dei server registrati nell'IDE Eclipse;
Una volta 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 MVC in cui l'applicazione web invia un flusso JSON al browser. Ora svilupperemo un'applicazione web/JSON per esporre sul web il database [dbintrospringdata] studiato nel tutorial [Introduzione a Spring Data].
13.5. Rendere accessibile il 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 scritta nel tutorial [Introduzione a Spring Data].
13.5.2. Installazione del database
![]() |
Lo script SQL [dbintrospringdata.sql] crea il database MySQL necessario per il test.
13.5.3. Il progetto Eclipse per il servizio web / JSON
Il progetto Eclipse per il 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 creare un servizio web/JSON. Include 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 di Spring Boot;
Le dipendenze introdotte da questa configurazione sono le seguenti:
![]() |
- In [1], possiamo vedere che Eclipse ha rilevato la dipendenza dall'archivio del progetto [intro-spring-data-01];
Le dipendenze sopra indicate appartengono sia al livello [DAO] che al livello [web].
13.5.3.1. Configurazione del livello [web]
Il livello [web] è configurato da 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 {
// -------------------------------- layer configuration [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);
}
// filters jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// result
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// result
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// result
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// result
return mapper;
}
}
- riga 18: l'annotazione [@EnableWebMvc] attiva le configurazioni automatiche per il framework Spring MVC;
- riga 19: la classe [WebConfig] estende la classe Spring [WebMvcConfigurerAdapter] per ridefinire 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 al controller e al metodo corretti. [DispatcherServlet] è una classe Spring;
- righe 31–34: specifichiamo che questo servlet gestisce tutti gli URL;
- righe 36–39: la presenza di questo bean attiverà il server Tomcat incluso negli archivi del progetto. Ascolterà 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 recuperare una categoria insieme ai suoi prodotti. Si noti che quando si richiede una categoria con i suoi prodotti, è necessario configurare sia il filtro JSON per la classe [Category] sia quello per la classe [Product]. Questo vale sempre. Quando si serializza/deserializza una classe in JSON, è necessario configurare il filtro JSON per la classe e quelli per tutte le dipendenze da includere in essa;
- righe 59–69: il mappatore JSON che consente di visualizzare un prodotto con la sua categoria;
- righe 71–80: il mappatore JSON che consente di avere una categoria senza i relativi prodotti;
- righe 82–91: il mappatore JSON che consente di recuperare un prodotto senza la sua 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: importa i bean dal livello [DAO] e quelli dal livello [web];
- riga 8: specifica i pacchetti in cui si trovano altri bean Spring;
Si noti che non abbiamo utilizzato l'annotazione [@EnableAutoConfiguration] in nessun punto. 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 {
// the [DAO] layer
@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/i controller comunicano con il singleton [ApplicationModel];
Questa strategia offre flessibilità nella gestione di una potenziale cache. La classe [ApplicationModel] può essere utilizzata per memorizzare informazioni ottenute dal livello [DAO] o dati di configurazione. Ciò può essere utile quando non si ha il controllo sul livello [DAO]. Questa strategia di caching può evolversi nel tempo. Le modifiche non avranno alcun impatto sul codice dei controller.
13.5.5. Il controller
![]() |
![]() |
Qui abbiamo un solo controller, la classe [MyController].
13.5.5.1. URL esposti
Gli URL esposti da questo controller sono i seguenti:
| Aggiunge i prodotti al database. Questi vengono inviati tramite POST. La risposta è una stringa JSON contenente l'elenco dei prodotti aggiunti con le loro chiavi primarie. |
| Elimina tutti i prodotti dal database. |
| Aggiorna i prodotti nel database. Questi vengono inviati tramite POST. La risposta è una stringa JSON contenente l'elenco dei prodotti aggiornati. |
| Recupera la stringa JSON per tutti i prodotti. |
| Aggiunge le categorie al database. Queste vengono inviate tramite POST. La risposta è una stringa JSON contenente l'elenco delle categorie aggiunte insieme alle loro chiavi primarie. 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 è vuoto. |
| Aggiorna le categorie nel database. Queste vengono inviate tramite POST. 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; |
| Recupera la stringa JSON per 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 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 suo nome, senza la categoria. |
| Recupera la stringa JSON relativa a una categoria specificata dal suo ID, insieme ai relativi prodotti. |
| Recupera la stringa JSON relativa a una categoria specificata dal suo nome, insieme ai relativi prodotti. |
| Recupera la stringa JSON relativa a una categoria specificata dal suo nome, senza i relativi prodotti. |
| Recupera la stringa JSON relativa a una categoria identificata dal suo ID, esclusi i relativi prodotti. |
Gli URL esposti corrispondono ai metodi dell'interfaccia [IDao] nel 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
Lo scheletro del controller è il 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 {
// spring dependencies
@Autowired
private ApplicationModel application;
// filters 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;
// class [MyController] is a singleton and is instantiated only once the 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: iniezione dei riferimenti ai mappatori JSON;
- riga 58: l'URL esposto è [/addProducts]. Il client deve utilizzare un metodo [POST] per effettuare la richiesta (method = RequestMethod.POST). Deve inviare il valore inviato come stringa JSON (content-type = "application/json; charset=UTF-8"). Il metodo stesso restituisce la risposta al client (riga 59). Si tratterà di una stringa (riga 60). L'intestazione HTTP [Content-type: application/json; charset=UTF-8] verrà inviata al client per indicare che riceverà una stringa JSON (riga 58);
- riga 60: il metodo [addProduits] restituisce la stringa JSON contenente l'elenco dei prodotti aggiunti al database;
13.5.5.3. Risposte dei metodi del controller
Tutti i metodi del controller restituiscono il seguente tipo [Response]:
![]() |
package spring.webjson.service;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
- riga 5: la risposta incapsula un tipo T;
- riga 13: la risposta di tipo T;
- righe 9–11: un metodo può incontrare un'eccezione. In questo caso, restituirà una risposta con:
- riga 9: status!=0;
- riga 11: l'elenco degli errori riscontrati;
13.5.5.4. L'URL [/addProducts]
L'URL [/addProducts] è gestito 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 {
// answer
Response<List<Produit>> response;
try {
// retrieve the posted value
String body = CharStreams.toString(request.getReader());
List<Produit> produits = jsonMapperProduitWithoutCategorie.readValue(body, new TypeReference<List<Produit>>() {
});
// we re-establish the link between products and categories
for (Produit produit : produits) {
produit.setCategorie(application.getCategorieByIdWithoutProduits(produit.getIdCategorie()));
}
// we persist products
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);
}
// answer jSON
return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
- riga 3: il metodo accetta [HttpServletRequest request] come parametro, che incapsula tutte le informazioni relative alla richiesta del client;
- riga 5: la risposta che verrà inviata al client: un elenco di prodotti;
- riga 8: recuperiamo il valore inviato. La classe [CharStreams] appartiene alla libreria [Google Guava], il cui riferimento abbiamo aggiunto al file [pom.xml]. Otteniamo la stringa JSON inviata dal client. Dobbiamo deserializzarla per poterla utilizzare;
- righe 8–10: viene eseguita la deserializzazione. Otteniamo un elenco di prodotti in cui ogni prodotto ha un campo [category=null];
- righe 12–14: reimpostiamo il campo [category] per tutti i prodotti nell'elenco. Per farlo, utilizziamo il campo [categoryId] del prodotto, che viene 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 incontra un'eccezione dal livello [DAO]. Inizializziamo la risposta con [status=1000] (codice di errore) [messages=e1.getMessages()], ovvero inviamo al client l'elenco degli errori riscontrati sul lato server;
- righe 20–21: caso in cui il metodo incontra un altro tipo di eccezione. Inizializziamo la risposta con [status=1000] (codice di errore) [messages=getErrorsForException(e)] dove [getErrorsForException] è un metodo privato della classe che restituisce l'elenco degli errori associati alle eccezioni nello stack di eccezioni di e, e [body=null];
- riga 24: viene restituita la stringa JSON della risposta;
13.5.5.5. L'URL [/getAllProducts]
L'URL [/getAllProducts] è gestito dal seguente metodo:
@RequestMapping(value = "/getAllProduits", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllProduits() throws JsonProcessingException {
// answer
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);
}
// answer jSON
return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
- Riga 1: L'URL [/getAllProduits] viene richiesto utilizzando un'operazione [GET]. Restituisce JSON;
- riga 2: il metodo stesso invia la risposta JSON al client;
- riga 5: il metodo restituisce una stringa JSON di tipo [Response<List<Product>>];
- riga 7: i prodotti vengono richiesti senza la loro categoria;
- righe 8–12: in caso di errore, la risposta viene inizializzata con un codice di errore e messaggi di errore;
- riga 14: la risposta JSON viene inviata al client;
13.5.5.6. Conclusione
Non tratteremo gli altri metodi del controller. Sono simili a uno o all'altro dei due metodi che abbiamo appena presentato.
13.5.6. Il servizio Web / Classe di esecuzione 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 questa classe viene eseguita, vengono generati i seguenti log:
- righe 17-19: avvio del server Tomcat per eseguire il servizio web/JSON;
- righe 25-33: costruzione del livello [DAO];
- righe 32-51: vengono individuati gli URL esposti;
13.5.7. Test del servizio web / JSON
Per eseguire i test, generiamo il database MySQL [dbintrospringdata] dallo script SQL [dbintrospringdata.sql]:
![]() |
Una volta fatto ciò, utilizziamo [Advanced Rest Client] (vedere la sezione 22.5) per interrogare gli URL esposti dal servizio web / JSON (il servizio web / JSON deve essere in esecuzione).
![]() |
- In [1-3], richiediamo l'URL [/getAllCategories] tramite una richiesta HTTP GET;
Riceviamo 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 con successo la richiesta;
- in [4], la risposta JSON del server;
La risposta JSON completa è 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 ci sono stati errori lato server;
- messages: 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 il prodotto [product15] alla categoria [category1]. Per farlo, useremo l'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);
// we persist categories
try {
// retrieve the posted value
String body = CharStreams.toString(request.getReader());
mapper.setFilters(jsonFilterCategorieWithProduits);
List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
});
// we re-establish the link between products and categories
for (Categorie categorie : categories) {
Set<Produit> produits = categorie.getProduits();
if (produits != null) {
for (Produit produit : categorie.getProduits()) {
produit.setCategorie(categorie);
}
}
}
// we persist categories
application.addCategories(categories);
response = new Response<List<Categorie>>(0, null, categories);
} catch (Exception e) {
response = new Response<List<Categorie>>(1004, getErreursForException(e), null);
}
// answer jSON
return mapper.writeValueAsString(response);
}
- riga 1: il client deve inviare una richiesta POST e il valore inviato deve essere una stringa JSON;
- righe 9–12: il valore inviato deve essere un elenco di categorie con i prodotti associati;
Creeremo una categoria [category2] con un prodotto [product21]. La stringa JSON da inviare sarà 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 come segue:
![]() |
- in [1], l'URL richiesto;
- in [2], la richiesta viene effettuata tramite un'operazione POST;
- in [3], la stringa JSON inviata;
- in [4], al server viene comunicato che verranno inviati dati JSON;
La risposta del server è la seguente:
![]() |
- in [1], vediamo che sia la categoria che il suo prodotto hanno ora una chiave primaria, il che indica che sono stati probabilmente inseriti nel database. Lo verificheremo utilizzando l'URL [/getCategorieByNameWithProduits/categorie2]:
![]() |
Otteniamo il seguente risultato:
![]() |
Abbiamo effettivamente recuperato la categoria [categorie2] con il suo unico prodotto [produit21]. Possiamo anche richiedere solo il prodotto. Per farlo, utilizziamo l'URL [/getProduitByIdWithoutCategorie/1859]:
![]() |
Otteniamo il seguente risultato:
![]() |
Tutte le operazioni [GET] possono essere eseguite in un browser web standard:
![]() |
I lettori sono invitati a provare gli altri URL del servizio web /json.
13.6. Un client programmato per il servizio web /json
Ora che il database [dbintrospringdata] è disponibile sul web, scriveremo un'applicazione che lo utilizzi. 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 del progetto Maven
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>
<!-- jSON library used by 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>
<!-- component used by 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>
<!-- log library -->
<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>
<!-- plugins -->
<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 scrivendo un'applicazione web, abbiamo bisogno della dipendenza [spring-web], che include la classe [RestTemplate] che consente un facile interfacciamento con un'applicazione web/JSON;
- righe 27–34: una libreria JSON;
- righe 36–39: una dipendenza che ci permetterà di impostare un timeout per le richieste HTTP del client. Un timeout è il tempo massimo di attesa per la risposta del server. Trascorso questo tempo, il client segnala un errore di timeout generando un'eccezione;
- righe 41–46: la libreria Google Guava utilizzata nel test JUnit. Per questo motivo, ne abbiamo impostato l'ambito su [test] (riga 45). Ciò significa che questa dipendenza è inclusa solo quando si esegue il codice dal ramo [src/test/java];
- righe 48–51: la libreria di logging;
- righe 52–63: le dipendenze per i test JUnit. In particolare, includono la libreria JUnit 4 necessaria per l'esecuzione dei test. Queste dipendenze presentano l'attributo [<scope>test</scope>], a indicare che sono richieste solo per la fase di test. Non vengono incluse nell'archivio finale del progetto;
13.6.3. Implementazione del livello [DAO]
![]() |
![]() |
- Il pacchetto [spring.client.config] contiene la configurazione Spring per il 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. Configurazione
![]() |
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 {
// constants
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8080";
@Bean
public RestTemplate restTemplate(int timeout) {
// creation of the RestTemplate component
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// exchange timeout
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// result
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
// filters jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// result
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// result
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// result
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// result
return mapper;
}
}
- riga 13: la classe è una classe di configurazione Spring; i componenti Spring si trovano nel pacchetto [spring.client.dao];
- riga 17: viene impostato 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 la comunicazione con il servizio web / JSON. Quando non è richiesta alcuna configurazione, può essere istanziata nel codice con un semplice [new RestTemplate()]. Qui, vogliamo impostare il timeout per la comunicazione con il servizio web / JSON. Il bean [timeout] alla riga 36 viene passato come parametro al metodo [RestTemplate] alla riga 24;
- riga 23: il componente [HttpComponentsClientHttpRequestFactory] è quello che ci permette di impostare il timeout per le comunicazioni (righe 29–30);
- riga 24: la classe [RestTemplate] viene costruita utilizzando questo componente. Poiché si affida a questo componente per comunicare con il servizio web / JSON, gli scambi saranno effettivamente soggetti al timeout;
- Il client e il server scambieranno righe di testo. Un convertitore gestisce la serializzazione di un oggetto in testo e, viceversa, la deserializzazione del testo in un 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, gli sviluppatori, a eseguire la necessaria serializzazione e deserializzazione JSON. Questo perché i filtri da applicare al valore inviato e alla risposta ricevuta potrebbero differire, e l'esperienza dimostra che è più facile gestirli autonomamente piuttosto che cercare di configurare il componente [RestTemplate] per utilizzare il convertitore JSON corretto;
- righe 42–92: definiscono i filtri JSON. Questi sono gli stessi filtri lato server presentati e spiegati nella Sezione 13.5.3.1;
- righe 43–46: un mappatore JSON senza filtri;
- righe 64–68: un mappatore JSON per recuperare una categoria senza i relativi prodotti;
- righe 48–58: un mappatore JSON per recuperare una categoria con i relativi prodotti;
- righe 83–92: un mappatore JSON per recuperare un prodotto senza la sua categoria;
- righe 60-70: un mappatore JSON per recuperare un prodotto con la sua categoria;
Tutti questi bean saranno disponibili sia per il codice del livello [DAO] che per il test JUnit.
13.6.3.2. Le entità
![]() |
Le entità gestite dal livello [DAO] sono quelle che scambia con il servizio web / JSON. Si tratta degli articoli e dei prodotti. Sul lato server, queste entità presentavano annotazioni di persistenza JPA. Qui, tali annotazioni sono state rimosse. Riportiamo nuovamente il codice delle entità a titolo di riferimento:
[AbstractEntity]
package spring.client.entities;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public abstract class AbstractEntity {
// properties
protected Long id;
protected Long version;
// manufacturers
public AbstractEntity() {
}
public AbstractEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
// redefine [equals] and [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();
}
// signature jSON
public String toString() {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(this);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
// getters and setters
...
}
[Categoria]
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 {
// properties
private String nom;
// related products
public Set<Produit> produits = new HashSet<Produit>();
// manufacturers
public Categorie() {
}
public Categorie(String nom) {
this.nom = nom;
}
// methods
public void addProduit(Produit produit) {
// we add the product
produits.add(produit);
// set your category
produit.setCategorie(this);
}
// getters and setters
...
}
[Prodotto]
package spring.webjson.client.entities;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {
// the name
private String nom;
// category number
private Long idCategorie;
// the price
private double prix;
// the description
private String description;
// the category
private Categorie categorie;
// manufacturers
public Produit() {
}
public Produit(String nom, double prix, String description) {
this.nom = nom;
this.prix = prix;
this.description = description;
}
// getters and setters
...
}
13.6.3.3. La classe [DaoException]
![]() |
Quando il livello [DAO] rileva un errore, genera un'eccezione [DaoException]. Questa classe viene utilizzata sul lato server ed è descritta nella sezione 11.3.7.
13.6.3.4. L'interfaccia del livello [DAO]
![]() |
Il livello [DAO] implementa l'interfaccia [IDao] descritta nella Sezione 11.3.7.
package spring.client.dao;
import java.util.List;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
public interface IDao {
// insert product list
public List<Produit> addProduits(List<Produit> produits);
// removal of all products
public void deleteAllProduits();
// product list update
public List<Produit> updateProduits(List<Produit> produits);
// all products obtained
public List<Produit> getAllProduits();
// inserting a list of categories
public List<Categorie> addCategories(List<Categorie> categories);
// delete all categories
public void deleteAllCategories();
// updating a list of categories
public List<Categorie> updateCategories(List<Categorie> categories);
// obtaining all categories
public List<Categorie> getAllCategories();
// a special product
public Produit getProduitByIdWithCategorie(Long idProduit);
public Produit getProduitByIdWithoutCategorie(Long idProduit);
public Produit getProduitByNameWithCategorie(String nom);
public Produit getProduitByNameWithoutCategorie(String nom);
// a special category
public Categorie getCategorieByIdWithProduits(Long idCategorie);
public Categorie getCategorieByIdWithoutProduits(Long idCategorie);
public Categorie getCategorieByNameWithProduits(String nom);
public Categorie getCategorieByNameWithoutProduits(String nom);
}
13.6.3.5. Il servizio web / Risposta JSON
![]() |
Abbiamo visto che tutti gli URL del servizio web / JSON restituiscono un tipo [Response] definito nella sezione 13.5.5.3. Riportiamo qui questa classe:
package spring.client.dao;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
13.6.3.6. Implementazione della comunicazione con il servizio web / JSON
![]() |
La classe [ AbstractDao] implementa la comunicazione 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 {
// data
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// generic request
protected String getResponse(String url, String jsonPost) {
// url : URL to contact
// jsonPost: the jSON value to be posted
try {
// request execution
RequestEntity<?> request;
if (jsonPost != null) {
// query POST
request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
} else {
// query GET
request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
.accept(MediaType.APPLICATION_JSON).build();
}
// execute the 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 gestisce la comunicazione con il server;
- righe 17-18: iniezione dell'URL del servizio web / JSON;
L'implementazione dei metodi per la comunicazione con il server è raggruppata nel metodo [getResponse]:
- riga 21: il metodo riceve 2 parametri:
- [url]: l'URL richiesto;
- [jsonPost]: la stringa JSON da inviare, oppure null in caso contrario. Se [jsonPost == null], la richiesta URL viene effettuata utilizzando un GET; altrimenti, utilizzando un POST;
- riga 38: l'istruzione che invia la richiesta al server e riceve la sua risposta. Il componente [RestTemplate] offre un'ampia gamma di metodi per interagire con il server. Qui abbiamo scelto il metodo [exchange], ma ne sono disponibili altri;
- righe 27–36: dobbiamo costruire la richiesta [RequestEntity]. Essa varia a seconda che si utilizzi una richiesta GET o POST;
- righe 30–31: la richiesta per un GET. La classe [RequestEntity] fornisce metodi statici per creare richieste GET, POST, HEAD e altre. Il metodo [RequestEntity.get] consente di creare una richiesta GET concatenando i vari metodi che la compongono:
- il metodo [RequestEntity.get] accetta l'URL di destinazione come parametro 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 informazioni per costruire il tipo [RequestEntity] della richiesta;
- righe 34–35: la richiesta POST. Il metodo [RequestEntity.post] crea una richiesta POST concatenando i vari metodi che la compongono:
- il metodo [RequestEntity.post] accetta l'URL di destinazione come parametro sotto forma di un'istanza URI,
- il metodo [header] definisce un'intestazione HTTP. In questo caso, inviamo al server l'intestazione [Content-Type: application/json] per indicare che i dati inviati arriveranno sotto forma di stringa JSON;
- il metodo [accept] ci permette di indicare che accettiamo il tipo [application/json] che il server invierà;
- il metodo [body] imposta il valore inviato. Questo è il 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() recupera questo corpo, che rappresenta la risposta del server—in questo caso, una stringa;
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;
// filters 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) {
// ----------- add products (without category)
...
}
- riga 17: la classe [Dao] è un componente Spring in cui possono essere iniettati altri componenti Spring;
- riga 18: la classe [Dao] estende la classe [AbstractDao] che abbiamo appena visto e implementa l'interfaccia [IDao];
- righe 20–21: iniettiamo il contesto Spring per accedere ai suoi bean;
- righe 24–38: iniezione dei mappatori JSON definiti nella classe [AppConfig] presentata nella sezione 13.6.2;
Le implementazioni dei vari 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) {
// ----------- obtain a category designated by its name, with its products
try {
// request
Response<Categorie> response = jsonMapperCategorieWithProduits.readValue(
getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null),
new TypeReference<Response<Categorie>>() {
});
// mistake?
if (response.getStatus() != 0) {
// 1 exception is thrown
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// render the core of the server response
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 gestisce la comunicazione con il servizio web/JSON. I suoi parametri sono i seguenti:
getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null)
- (continua)
- l'URL del servizio interrogato [/getCategoryByNameWithProducts/name];
- il valore inviato. In questo caso, non ce n'è nessuno;
Il metodo [getResponse] restituisce una stringa che rappresenta la risposta JSON inviata dal server. Deserializziamo questa risposta JSON come segue:
jsonMapperCategorieWithProduits.readValue(
jsonResponse,
new TypeReference<Response<Categorie>>() {
});
poiché la stringa JSON è la serializzazione di un tipo [Response<Category>];
- righe 11–17: controlliamo lo stato della risposta. Se lo stato non è 0, significa che si è verificato un errore lato server. A quel punto generiamo un'eccezione (riga 13), utilizzando le informazioni contenute nella risposta (stato ed elenco dei messaggi di errore);
- riga 16: se non si è verificato alcun errore lato server, restituiamo il corpo di tipo [Response<Category>], ovvero la categoria richiesta;
- righe 18–19: gestiamo l'eccezione generata alla riga 16;
- righe 20–22: gestiamo tutte le altre eccezioni;
Un esempio di [POST]: [addCategories]
@Override
public List<Categorie> addCategories(List<Categorie> categories) {
// ----------- add categories (with their products)
try {
// request
Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories)),
new TypeReference<Response<List<Categorie>>>() {
});
// mistake?
if (response.getStatus() != 0) {
// 1 exception is thrown
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// render the core of the server response
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(104, e2);
}
}
- riga 2: il metodo [addCategories] viene utilizzato per salvare nel database le categorie passate come parametri. Restituisce queste stesse categorie arricchite con le loro chiavi primarie. Se le categorie vengono passate insieme ai prodotti, anche questi vengono salvati;
- riga 7: viene chiamato il metodo [getResponse] del genitore per gestire la comunicazione con il servizio web / JSON;
- il primo parametro è l'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 risultante viene quindi deserializzata per ottenere il tipo [Response<List<Category>] 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 attualmente in fase di sviluppo:
![]() |
Abbiamo realizzato un livello [DAO] [2] con la stessa interfaccia del livello [DAO] [4]. Per testare il livello [DAO] [2], possiamo quindi utilizzare il test JUnit che è stato utilizzato per testare il livello [DAO] [4]. Come promemoria, è 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 {
// layer [DAO]
@Autowired
private IDao dao;
// filters 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() {
// the base is cleaned before each test
log("Vidage de la base de données", 1);
// table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
dao.deleteAllCategories();
// --------------------------------------------------------------------------------------
log("Remplissage de la base", 1);
// fill the tables
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);
}
// add the category - the products will be cascaded in as well
categories = dao.addCategories(categories);
}
@Test
public void showDataBase() throws BeansException, JsonProcessingException {
// list of categories
log("Liste des catégories", 2);
List<Categorie> categories = dao.getAllCategories();
affiche(categories, jsonMapperCategorieWithoutProduits);
// product list
log("Liste des produits", 2);
List<Produit> produits = dao.getAllProduits();
affiche(produits, jsonMapperProduitWithoutCategorie);
// a few checks
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);
// we insert
Categorie categorie = new Categorie("cat1");
categorie.addProduit(new Produit("x", 1.0, ""));
categorie.addProduit(new Produit("x", 1.0, ""));
// add the category - the products will be cascaded in as well
try {
categorie = dao.addCategories(Lists.newArrayList(categorie)).get(0);
} catch (DaoException e) {
show("Les erreurs suivantes se sont produites :", e.getErreurs());
}
// checks
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);
// checks
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);
// check
affiche(produits, jsonMapperProduitWithoutCategorie);
}
// -------------- private methods
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;
}
// display of a T-type element
static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(element));
}
// display a list of elements of type 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) {
// poster message
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) {
// title
System.out.println(String.format("%s : ", title));
// messages
for (String message : messages) {
System.out.println(String.format("- %s", message));
}
}
}
L'esecuzione va a buon fine e produce 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



























































