Skip to content

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:

  1. 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.
  2. 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];
  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:

{"id":2,"content":"Hello, World!"}

oppure

{"id":2,"content":"Hello, John!"}

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:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.9.RELEASE)

2014-11-28 15:22:55.005  INFO 3152 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 3152 (started by ST in D:\data\istia-1415\spring mvc\dvp-final\gs-rest-service)
2014-11-28 15:22:55.046  INFO 3152 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@62e136d3: startup date [Fri Nov 28 15:22:55 CET 2014]; root of context hierarchy
2014-11-28 15:22:55.762  INFO 3152 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2014-11-28 15:22:56.567  INFO 3152 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-11-28 15:22:56.738  INFO 3152 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-11-28 15:22:56.740  INFO 3152 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.56
2014-11-28 15:22:56.869  INFO 3152 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-11-28 15:22:56.870  INFO 3152 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1827 ms
2014-11-28 15:22:57.478  INFO 3152 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-11-28 15:22:57.481  INFO 3152 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-11-28 15:22:57.685  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:57.879  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public hello.Greeting hello.GreetingController.greeting(java.lang.String)
2014-11-28 15:22:57.884  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2014-11-28 15:22:57.885  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2014-11-28 15:22:57.906  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] su gestore di tipo [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:57.907  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] gestore di tipo [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:58.231  INFO 3152 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-11-28 15:22:58.318  INFO 3152 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-11-28 15:22:58.319  INFO 3152 --- [           main] hello.Application                        : Started Application in 3.788 seconds (JVM running for 4.424)
  • 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.

[INFO] --- spring-boot-maven-plugin:1.1.0.RELEASE:repackage (default) @ gs-rest-service ---

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:


    @RequestMapping(value = "/addProduits",
method = RequestMethod.POST,
consumes = "application/json; charset=UTF-8")
    public String addProduits(HttpServletRequest request) {
...
    }
Aggiunge i prodotti al database. Questi vengono salvati. La risposta è la stringa jSON, ovvero l'elenco dei prodotti aggiunti con la loro chiave primaria.

    @RequestMapping(value = "/deleteAllProduits",
method = RequestMethod.GET)
    public String deleteAllProduits() {
..
    }
Elimina tutti i prodotti dal database.

    @RequestMapping(value = "/updateProduits",
method = RequestMethod.POST,
consumes = "application/json; charset=UTF-8")
    public String updateProduits(HttpServletRequest request) {
..
    }

Aggiorna i prodotti nel database. Questi vengono pubblicati. La risposta è la stringa jSON dell'elenco dei prodotti aggiornati.

    @RequestMapping(value = "/getAllProduits",
method = RequestMethod.GET)
    public String getAllProduits() {
..
    }

Ottiene la stringa jSON da tutti i prodotti.

    @RequestMapping(value = "/addCategories",
method = RequestMethod.POST,
consumes = "application/json; charset=UTF-8")
    public String addCategories(HttpServletRequest request) {
..
    }

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.

    @RequestMapping(value = "/deleteAllCategories",
method = RequestMethod.GET)
    public String deleteAllCategories() {
...
    }

Elimina tutte le categorie dal database, insieme a tutti i prodotti in esse contenuti. Al termine, il database risulterà vuoto.

    @RequestMapping(value = "/updateCategories",
method = RequestMethod.POST,
consumes = "application/json; charset=UTF-8")
    public String updateCategories
(HttpServletRequest request) {
...
    }

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;

    @RequestMapping(value = "/getAllCategories",
method = RequestMethod.GET)
    public String getAllCategories() {
...
    }

Ottiene la stringa jSON da tutte le categorie.

    @RequestMapping(value = "/getProduitByIdWithCategorie/{idProduit}",
method = RequestMethod.GET)
    public String getProduitByIdWithCategorie
(@PathVariable("idProduit") Long idProduit) {
...
    }

Recupera la stringa jSON relativa a un prodotto identificato dal suo ID, insieme alla sua categoria.

    @RequestMapping(value = "/getProduitByIdWithoutCategorie/{idProduit}",
method = RequestMethod.GET)
    public String getProduitByIdWithoutCategorie
@PathVariable("idProduit") Long idProduit) {
...
    }

Recupera la stringa jSON relativa a un prodotto identificato dal suo ID, senza indicarne la categoria.

    @RequestMapping(value = "/getProduitByNameWithCategorie/{nom}",
method = RequestMethod.GET)
    public String getProduitByNameWithCategorie(
@PathVariable("nom") String nom) {
...
    }

Recupera la stringa jSON relativa a un prodotto identificato dal suo nome, insieme alla sua categoria.

    @RequestMapping(value = "/getProduitByNameWithoutCategorie/{nom}",
method = RequestMethod.GET)
    public String getProduitByNameWithoutCategorie
(@PathVariable("nom") String nom) {
...
    }

Recupera la stringa jSON relativa a un prodotto identificato dal nome, senza indicarne la categoria.

    @RequestMapping(value = "/getCategorieByIdWithProduits/{idCategorie}",
 method = RequestMethod.GET)
    public String getCategorieByIdWithProduits
@PathVariable("idCategorie") Long idCategorie) {
...
    }

Recupera la stringa jSON relativa a una categoria indicata dal suo ID, insieme ai relativi prodotti.

    @RequestMapping(value = "/getCategorieByNameWithProduits/{nom}",
method = RequestMethod.GET)
    public String getCategorieByNameWithProduits
(@PathVariable("nom") String nom) {
...
    }

Recupera la stringa jSON relativa a una categoria specificata per nome, insieme ai relativi prodotti.

    @RequestMapping(value = "/getCategorieByNameWithoutProduits/{nom}",
method = RequestMethod.GET)
    public String getCategorieByNameWithoutProduits(
@PathVariable("nom") String nom) {
...
    }

Recupera la stringa jSON relativa a una categoria specificata per nome, esclusi i relativi prodotti.

    @RequestMapping(value = "/getCategorieByIdWithoutProduits/{idCategorie}",
method = RequestMethod.GET)
    public String getCategorieByIdWithoutProduits(
@PathVariable("idCategorie") Long idCategorie) {
...
    }

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:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.2.RELEASE)

2015-03-24 16:22:46.608  INFO 9492 --- [           main] spring.webjson.server.boot.Boot          : Starting Boot on Gportpers3 with PID 9492 (D:\data\istia-1415\eclipse\intro-web-json\intro-webjson-server-02\target\classes started by ST in D:\data\istia-1415\eclipse\intro-web-json\intro-webjson-server-02)
2015-03-24 16:22:46.654  INFO 9492 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1d7acb34: startup date [Tue Mar 24 16:22:46 CET 2015]; root of context hierarchy
2015-03-24 16:22:47.521  INFO 9492 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2015-03-24 16:22:47.569  INFO 9492 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'entityManagerFactory': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=spring.data.config.DaoConfig; factoryMethodName=entityManagerFactory; initMethodName=null; destroyMethodName=(inferred); defined in class spring.data.config.DaoConfig] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=true; factoryBeanName=org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; factoryMethodName=entityManagerFactory; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]]
2015-03-24 16:22:48.137  INFO 9492 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$405db6ba] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.162  INFO 9492 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionAttributeSource' of type [class org.springframework.transaction.annotation.AnnotationTransactionAttributeSource] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.172  INFO 9492 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionInterceptor' of type [class org.springframework.transaction.interceptor.TransactionInterceptor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.178  INFO 9492 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.config.internalTransactionAdvisor' of type [class org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.586  INFO 9492 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2015-03-24 16:22:48.850  INFO 9492 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2015-03-24 16:22:48.852  INFO 9492 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.0.20
2015-03-24 16:22:48.992  INFO 9492 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2015-03-24 16:22:48.992  INFO 9492 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2342 ms
2015-03-24 16:22:49.645  INFO 9492 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2015-03-24 16:22:49.650  INFO 9492 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'characterEncodingFilter' to: [/*]
2015-03-24 16:22:49.651  INFO 9492 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2015-03-24 16:22:50.380  INFO 9492 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2015-03-24 16:22:50.392  INFO 9492 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2015-03-24 16:22:50.478  INFO 9492 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.8.Final}
2015-03-24 16:22:50.480  INFO 9492 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2015-03-24 16:22:50.483  INFO 9492 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2015-03-24 16:22:50.697  INFO 9492 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
2015-03-24 16:22:50.806  INFO 9492 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
2015-03-24 16:22:51.058  INFO 9492 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory
2015-03-24 16:22:52.581  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1d7acb34: startup date [Tue Mar 24 16:22:46 CET 2015]; root of context hierarchy
2015-03-24 16:22:52.654  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/addProduits],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Produit>> spring.webjson.server.service.Controller.addProduits(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/updateProduits],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Produit>> spring.webjson.server.service.Controller.updateProduits(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllProduits],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Produit>> spring.webjson.server.service.Controller.getAllProduits()
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllCategories],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.getAllCategories()
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/addCategories],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.addCategories(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/updateCategories],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.updateCategories(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByNameWithoutProduits/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByNameWithoutProduits(java.lang.String)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByNameWithoutCategorie/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByNameWithoutCategorie(java.lang.String)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByNameWithCategorie/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByNameWithCategorie(java.lang.String)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByIdWithCategorie/{idProduit}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByIdWithCategorie(java.lang.Long)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByNameWithProduits/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByNameWithProduits(java.lang.String)
2015-03-24 16:22:52.657  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByIdWithProduits/{idCategorie}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByIdWithProduits(java.lang.Long)
2015-03-24 16:22:52.657  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/deleteAllCategories],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.lang.Void> spring.webjson.server.service.Controller.deleteAllCategories()
2015-03-24 16:22:52.657  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByIdWithoutProduits/{idCategorie}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByIdWithoutProduits(java.lang.Long)
2015-03-24 16:22:52.657  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/deleteAllProduits],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.lang.Void> spring.webjson.server.service.Controller.deleteAllProduits()
2015-03-24 16:22:52.658  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByIdWithoutCategorie/{idProduit}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByIdWithoutCategorie(java.lang.Long)
2015-03-24 16:22:52.659  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.659  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.691  INFO 9492 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] su gestore di tipo [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:52.692  INFO 9492 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] su handler di tipo [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:52.742  INFO 9492 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:53.001  INFO 9492 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2015-03-24 16:22:53.106  INFO 9492 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2015-03-24 16:22:53.108  INFO 9492 --- [           main] spring.webjson.server.boot.Boot          : Started Boot in 6.752 seconds (JVM running for 7.433)
  • 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