Skip to content

13. [Corso]: Pubblicazione di un database sul Web con Spring MVC

Parole chiave: architettura multilivello, Spring, iniezione di dipendenze, servizio web / JSON, client / server

13.1. Assistenza

 

I progetti relativi a questo capitolo si trovano nella cartella [support / chap-13]. Lo script SQL [dbintrospringdata.sql] crea il database MySQL necessario per i test.

13.2. Il ruolo di Spring MVC in un'applicazione web

Inquadriamo Spring MVC nello sviluppo di un'applicazione web. Molto spesso, essa sarà costruita su un'architettura a più livelli come la seguente:

  • il livello [Web] è quello a contatto con l'utente dell'applicazione web. L'utente interagisce con l'applicazione web tramite pagine web visualizzate in un browser. Spring MVC si trova in questo livello e solo in questo livello;
  • il livello [business] implementa la logica di business dell’applicazione, come il calcolo di uno stipendio o di una fattura. Questo livello utilizza i dati provenienti dall’utente tramite il livello [Web] e dal DBMS tramite il livello [DAO];
  • il livello [DAO] (Data Access Objects), il livello [ORM] (Object Relational Mapper) e il driver JDBC gestiscono l'accesso ai dati nel DBMS. Il livello [ORM] funge da ponte tra gli oggetti gestiti dal livello [DAO] e le righe e le colonne delle tabelle in un database relazionale. La specifica JPA (Java Persistence API) consente l'astrazione dall'ORM utilizzato, a condizione che implementi tali specifiche. Questo sarà il caso in esame e d'ora in poi ci riferiremo al livello ORM come al livello JPA;
  • L'integrazione dei livelli è gestita dal framework Spring;

13.3. Il modello di sviluppo Spring MVC

Spring MVC implementa il modello architettonico MVC (Model–View–Controller) come segue:

L'elaborazione di una richiesta del client procede come segue:

  1. richiesta - gli URL richiesti hanno il formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... Il [Front Controller] utilizza un file di configurazione o annotazioni Java per "instradare" la richiesta al controller corretto e all'azione corretta all'interno di quel controller. A tal fine, utilizza il campo [Action] dell'URL. Il resto dell'URL [/param1/param2/...] è costituito da parametri opzionali che verranno passati all'azione. La C in MVC qui si riferisce alla catena [Front Controller, Controller, Action]. Se nessun controller è in grado di gestire l'azione richiesta, il server web risponderà che l'URL richiesto non è stato trovato.
  2. Elaborazione
    • L'azione selezionata può utilizzare i parametri che il [Front Controller] le ha passato. Questi possono provenire da diverse fonti:
    • il percorso [/param1/param2/...] dell'URL,
    • i parametri [p1=v1&p2=v2] dell'URL,
    • dai parametri inviati dal browser con la sua richiesta;
    • durante l'elaborazione della richiesta dell'utente, l'azione potrebbe aver bisogno del livello [business] [2b]. Una volta elaborata la richiesta del client, questa può innescare varie risposte. Un esempio classico è:
    • una pagina di errore se la richiesta non è stata elaborata correttamente
    • una pagina di conferma in caso contrario
    • l'azione indica di visualizzare una vista specifica [3]. Questa vista mostrerà i dati noti come modello di vista. Questa è la M in MVC. L'azione creerà questo modello M [2c] e indicherà di visualizzare una vista V [3];
  3. risposta - la vista V selezionata utilizza il modello M costruito dall'azione per inizializzare le parti dinamiche della risposta HTML che deve inviare al client, quindi invia questa risposta.

Per un servizio web / JSON, l'architettura precedente viene leggermente modificata:

  • in [4a], il modello, che è una classe Java, viene convertito in una stringa JSON da una libreria JSON;
  • in [4b], questa stringa JSON viene inviata al browser;

Ora, chiariamo la relazione tra l'architettura web MVC e l'architettura a livelli. A seconda di come è definito il modello, questi due concetti possono essere correlati o meno. Consideriamo un'applicazione web Spring MVC a livello singolo:

Se implementiamo il livello [Web] con Spring MVC, avremo effettivamente un'architettura web MVC ma non un'architettura a più livelli. In questo caso, il livello [Web] gestirà tutto: presentazione, logica di business e accesso ai dati. Saranno le azioni a svolgere questo lavoro.

Ora, consideriamo un'architettura web multistrato:

Il livello [Web] può essere implementato senza un framework e senza seguire il modello MVC. In questo caso, abbiamo comunque un'architettura a più livelli, ma il livello Web non implementa il modello MVC.

Ad esempio, nell'ambiente .NET, il livello [Web] sopra descritto può essere implementato utilizzando ASP.NET MVC, ottenendo un'architettura a livelli con un livello [Web] in stile MVC. Detto questo, questo livello ASP.NET MVC può essere sostituito con un livello ASP.NET classico (WebForms) mantenendo invariato il resto (logica di business, DAO, ORM). Si ottiene così un'architettura a livelli con un livello [Web] che non è più basato su MVC.

In MVC, abbiamo detto che il modello M era quello della vista V, ovvero l'insieme di dati visualizzati dalla vista V. Viene fornita un'altra definizione del modello M in MVC:

Molti autori ritengono che ciò che si trova a destra del livello [Web] costituisca il modello M dell'MVC. Per evitare ambiguità, possiamo fare riferimento al:

  • il modello di dominio quando ci si riferisce a tutto ciò che si trova a destra del livello [Web]
  • il modello di vista quando ci riferiamo ai dati visualizzati da una vista V

D'ora in poi, il termine "modello M" si riferirà esclusivamente al modello di una vista V.

13.4. Un progetto Web/JSON con Spring MVC

Il sito [http://spring.io/guides] offre tutorial introduttivi per esplorare l'ecosistema Spring. Ne seguiremo uno per scoprire la configurazione Maven necessaria per un progetto Spring MVC.

13.4.1. Il progetto demo

  • In [1], importiamo una delle guide di Spring;
  • in [2], selezioniamo l'esempio [Rest Service];
  • in [3], selezioniamo il progetto Maven;
  • in [4], selezioniamo la versione finale della guida;
  • in [5], confermiamo;
  • in [6], il progetto importato;

I servizi web accessibili tramite URL standard che restituiscono dati JSON sono spesso chiamati servizi REST (REpresentational State Transfer). Un servizio è detto RESTful se segue determinate regole.

Esaminiamo ora il progetto importato, iniziando dalla sua configurazione Maven.

13.4.2. Configurazione Maven

Il file [pom.xml] è il seguente:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>org.springframework</groupId>
    <artifactId>gs-rest-service</artifactId>
    <version>0.1.0</version>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.2.RELEASE</version>
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
 
    <properties>
        <start-class>hello.Application</start-class>
    </properties>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
    <repositories>
        <repository>
            <id>spring-releases</id>
            <url>https://repo.spring.io/libs-release</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <url>https://repo.spring.io/libs-release</url>
        </pluginRepository>
    </pluginRepositories>
</project>
  • righe 6–8: le proprietà del progetto Maven. Manca un tag [<packaging>] che specifichi il tipo di file prodotto dalla build di Maven. In sua assenza, viene utilizzato il tipo [jar]. L'applicazione è quindi un'applicazione eseguibile basata su console, non un'applicazione web, nel qual caso il packaging sarebbe [war];
  • righe 10–14: il progetto Maven ha un progetto padre [spring-boot-starter-parent]. Questo definisce la maggior parte delle dipendenze del progetto. Esse potrebbero essere sufficienti, nel qual caso non vengono aggiunte dipendenze aggiuntive, oppure potrebbero non esserlo, nel qual caso vengono aggiunte le dipendenze mancanti;
  • Righe 17–20: L'artefatto [spring-boot-starter-web] include le librerie necessarie per un progetto di servizio web Spring MVC in cui non vengono generate viste. Questo artefatto include un numero molto elevato di librerie, comprese quelle per un server Tomcat incorporato. L'applicazione verrà eseguita su questo server;

Le librerie incluse in questa configurazione sono molto numerose:

Sopra, vediamo i tre archivi del server Tomcat.

13.4.3. L'architettura di un servizio Spring [web / JSON]

Per un servizio web/JSON, Spring MVC implementa il modello MVC come segue:

  • In [4a], il modello — che è una classe Java — viene convertito in una stringa JSON da una libreria JSON;
  • in [4b], questa stringa JSON viene inviata al browser;

13.4.4. Il controller C

  

L'applicazione importata dispone del seguente controller:


package hello;
 
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class GreetingController {
 
    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();
 
    @RequestMapping("/greeting")
    public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
        return new Greeting(counter.incrementAndGet(), String.format(template, name));
    }
}
  • Riga 9: L'annotazione [@RestController] rende la classe [GreetingController] un controller Spring, il che significa che i suoi metodi sono registrati per gestire gli URL. Abbiamo già visto l'annotazione simile [@Controller]. Il tipo di ritorno dei metodi di quel controller era [String], ovvero il nome della vista da visualizzare. Qui è diverso. I metodi di un [@RestController] restituiscono oggetti che vengono serializzati per essere inviati al browser. Il tipo di serializzazione eseguita dipende dalla configurazione di Spring MVC. Qui, saranno serializzati in JSON. È la presenza di una libreria JSON nelle dipendenze del progetto che fa sì che Spring Boot configuri automaticamente il progetto in questo modo;
  • riga 14: l'annotazione [@RequestMapping] specifica l'URL gestito dal metodo, in questo caso l'URL [/greeting];
  • riga 15: abbiamo già spiegato l'annotazione [@RequestParam]. Il risultato restituito dal metodo è un oggetto di tipo [Greeting].
  • riga 12: un intero lungo di tipo atomico. Ciò significa che supporta l'accesso concorrente. Più thread potrebbero voler incrementare la variabile [counter] contemporaneamente. Ciò verrà gestito correttamente. Un thread può leggere il valore del contatore solo una volta che il thread che lo sta modificando ha terminato la modifica.

13.4.5. Il modello M

Il modello M prodotto dal metodo precedente è il seguente oggetto [Greeting]:

  

package hello;
 
public class Greeting {
 
    private final long id;
    private final String content;
 
    public Greeting(long id, String content) {
        this.id = id;
        this.content = content;
    }
 
    public long getId() {
        return id;
    }
 
    public String getContent() {
        return content;
    }
}

La trasformazione JSON di questo oggetto creerà la stringa {"id":n,"content":"text"}. Alla fine, la stringa JSON prodotta dal metodo del controller avrà la forma:

{"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à incontrato e spiegato questo codice nell'esempio precedente.

13.4.7. Esecuzione del progetto

Eseguiamo il progetto:

 

Otteniamo 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/**] onto handler of type [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 [/**] onto handler of type [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: il servlet [DispatcherServlet] è presente;
  • riga 20: il metodo [GreetingController.greeting] è stato individuato;

Per testare l'applicazione web, richiediamo l'URL [http://localhost:8080/greeting]:

 

Riceviamo la stringa JSON prevista. Potrebbe essere interessante visualizzare le intestazioni HTTP inviate dal server. Per farlo, useremo l'estensione di Chrome chiamata [Advanced Rest Client] (Chrome / Ctrl-T / menu [Applicazioni] / [Advanced Rest Client] - vedi Appendici, paragrafo 22.5):

  • in [1], l'URL richiesto;
  • in [2], viene utilizzato il metodo GET;
  • in [3], la risposta JSON;
  • in [4], il server ha indicato che avrebbe inviato una risposta in formato JSON;
  • in [5], richiediamo lo stesso URL ma questa volta utilizzando una richiesta POST;
  • in [7], le informazioni vengono inviate al server in formato [urlencoded];
  • in [6], il parametro "name" e il suo valore;
  • in [8], il browser comunica al server che sta inviando informazioni [urlencoded];
  • in [9], la risposta JSON del server;

13.4.8. Creazione di un archivio eseguibile

Ora creeremo un archivio eseguibile:

  • in [1]: eseguiamo un target Maven;
  • in [2]: ci sono due goal: [clean] per eliminare la cartella [target] dal progetto Maven, [package] per rigenerarla;
  • in [3]: la cartella [target] generata si troverà in questa cartella;
  • in [4]: generiamo il target;

Nei log che compaiono nella console, è importante vedere il plugin [spring-boot-maven-plugin]. Questo è il plugin che genera l'archivio eseguibile.

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

Utilizzando una console, accedere alla cartella generata:


D:\Temp\wksSTS\gs-rest-service\target>dir
 ...
11/06/2014  15:30    <DIR>          classes
11/06/2014  15:30    <DIR>          generated-sources
11/06/2014  15:30        11 073 572 gs-rest-service-0.1.0.jar
11/06/2014  15:30             3 690 gs-rest-service-0.1.0.jar.original
11/06/2014  15:30    <DIR>          maven-archiver
11/06/2014  15:30    <DIR>          maven-status
...
  • riga 5: l'archivio generato;

Questo archivio viene eseguito come segue:


D:\Temp\wksSTS\gs-rest-service-complete\target>java -jar gs-rest-service-0.1.0.jar
 
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.0.RELEASE)
 
2014-06-11 15:32:47.088  INFO 4972 --- [           main] hello.Application
                  : Starting Application on Gportpers3 with PID 4972 (D:\Temp\wk
sSTS\gs-rest-service-complete\target\gs-rest-service-0.1.0.jar started by ST in
D:\Temp\wksSTS\gs-rest-service-complete\target)
...

Ora che l'applicazione web è in esecuzione, è possibile accedervi utilizzando un browser:

 

13.4.9. Distribuzione dell'applicazione su un server Tomcat

Come abbiamo fatto per il progetto precedente, modifichiamo il file [pom.xml] come segue:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>org.springframework</groupId>
    <artifactId>gs-rest-service</artifactId>
    <version>0.1.0</version>
    <packaging>war</packaging>
 
    ...
</project>
  • Riga 9: È necessario specificare che si intende generare un file WAR (Web Archive);

È inoltre necessario configurare l'applicazione web. In assenza di un file [web.xml], ciò avviene utilizzando una classe che estende [SpringBootServletInitializer]:

  

La classe [ApplicationInitializer] è la seguente:


package hello;
 
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
 
public class ApplicationInitializer extends SpringBootServletInitializer {
 
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }
 
}
  • riga 6: la classe [ApplicationInitializer] estende la classe [SpringBootServletInitializer];
  • riga 9: il metodo [configure] viene sovrascritto (riga 8);
  • riga 10: viene fornita la classe che configura il progetto;

Per eseguire il progetto, procedere come segue:

  • In [1-2], eseguire il progetto su uno dei server registrati nell'IDE Eclipse;

Una volta fatto ciò, è possibile richiamare l'URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] in un browser:

 

13.4.10. Conclusione

Abbiamo presentato un tipo di progetto Spring MVC in cui l'applicazione web invia un flusso JSON al browser. Ora svilupperemo un'applicazione web/JSON per esporre sul web il database [dbintrospringdata] studiato nel tutorial [Introduzione a Spring Data].

13.5. Rendere accessibile il database [dbintrospringdata] sul web

13.5.1. Architettura del servizio Web/JSON

Implementeremo la seguente architettura:

I livelli [DAO] e [JPA] sono implementati dall'applicazione scritta nel tutorial [Introduzione a Spring Data].

13.5.2. Installazione del database

  

Lo script SQL [dbintrospringdata.sql] crea il database MySQL necessario per il test.

13.5.3. Il progetto Eclipse per il servizio web / JSON

Il progetto Eclipse per il servizio web / JSON è il seguente:

  

Si tratta di un progetto Maven il cui file [pom.xml] è il seguente:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.webjson</groupId>
    <artifactId>intro-server-webjson01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
 
    <name>intro-server-webjson01</name>
    <description>démo spring mvc</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>istia.st.springdata</groupId>
            <artifactId>intro-spring-data-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  • righe 11–15: il progetto Maven padre già utilizzato per il livello [DAO];
  • righe 18–22: la dipendenza dal livello [DAO];
  • righe 23–26: la dipendenza dall'artefatto [spring-boot-starter-web]. Questo artefatto include tutte le dipendenze necessarie per creare un servizio web/JSON. Include anche librerie non necessarie. Sarebbe quindi necessaria una configurazione più precisa. Tuttavia, questa configurazione è utile per iniziare;
  • righe 28–30: la dipendenza dall'artefatto [spring-boot-starter] consente di gestire le annotazioni di Spring Boot;

Le dipendenze introdotte da questa configurazione sono le seguenti:

  • In [1], possiamo vedere che Eclipse ha rilevato la dipendenza dall'archivio del progetto [intro-spring-data-01];

Le dipendenze sopra indicate appartengono sia al livello [DAO] che al livello [web].

13.5.3.1. Configurazione del livello [web]

Il livello [web] è configurato da un file [AppConfig]:

  

La classe [WebConfig] configura il livello [web]:


package spring.webjson.config;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
 
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
 
    // -------------------------------- layer configuration [web]
    @Autowired
    private ApplicationContext context;
 
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
        return servlet;
    }
 
    @Bean
    public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new ServletRegistrationBean(dispatcherServlet, "/*");
    }
 
    @Bean
    public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory("", 8080);
    }
 
    // filters jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }
 
    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // result
        return mapper;
    }
}
  • riga 18: l'annotazione [@EnableWebMvc] attiva le configurazioni automatiche per il framework Spring MVC;
  • riga 19: la classe [WebConfig] estende la classe Spring [WebMvcConfigurerAdapter] per ridefinire alcuni bean (righe 26–40);
  • righe 22–23: iniezione del contesto Spring;
  • righe 25–29: definizione del servlet del framework Spring MVC, che instrada le richieste HTTP al controller e al metodo corretti. [DispatcherServlet] è una classe Spring;
  • righe 31–34: specifichiamo che questo servlet gestisce tutti gli URL;
  • righe 36–39: la presenza di questo bean attiverà il server Tomcat incluso negli archivi del progetto. Ascolterà le richieste sulla porta 8080;
  • righe 42–91: bean che verranno utilizzati per gestire i filtri JSON;
  • righe 42–45: un mappatore JSON senza filtri;
  • righe 47–57: il mappatore JSON che consente di recuperare una categoria insieme ai suoi prodotti. Si noti che quando si richiede una categoria con i suoi prodotti, è necessario configurare sia il filtro JSON per la classe [Category] sia quello per la classe [Product]. Questo vale sempre. Quando si serializza/deserializza una classe in JSON, è necessario configurare il filtro JSON per la classe e quelli per tutte le dipendenze da includere in essa;
  • righe 59–69: il mappatore JSON che consente di visualizzare un prodotto con la sua categoria;
  • righe 71–80: il mappatore JSON che consente di avere una categoria senza i relativi prodotti;
  • righe 82–91: il mappatore JSON che consente di recuperare un prodotto senza la sua categoria;

La classe [AppConfig] configura l'intera applicazione, ovvero i livelli [web] e [DAO]:


package spring.webjson.config;
 
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
 
import spring.data.config.DaoConfig;
 
@ComponentScan(basePackages = { "spring.webjson" })
@Import({ DaoConfig.class, WebConfig.class})
public class AppConfig {
 
}
  • Riga 9: importa i bean dal livello [DAO] e quelli dal livello [web];
  • riga 8: specifica i pacchetti in cui si trovano altri bean Spring;

Si noti che non abbiamo utilizzato l'annotazione [@EnableAutoConfiguration] in nessun punto. Abbiamo preferito gestire la configurazione autonomamente.

13.5.4. Il modello dell'applicazione

  

La classe [ApplicationModel] è la seguente:


package spring.webjson.models;
 
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
 
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
 
@Component
public class ApplicationModel implements IDao {
 
    // the [DAO] layer
    @Autowired
    private IDao dao;
 
    @Override
    public void addProduits(List<Produit> produits) {
        dao.addProduits(produits);
    }
 
    @Override
    public void deleteAllProduits() {
        dao.deleteAllProduits();
    }
 
    @Override
    public void updateProduits(List<Produit> produits) {
        dao.updateProduits(produits);
    }
 
    @Override
    public List<Produit> getAllProduits() {
        return dao.getAllProduits();
    }
 
    @Override
    public void addCategories(List<Categorie> categories) {
        dao.addCategories(categories);
    }
 
    @Override
    public void deleteAllCategories() {
        dao.deleteAllCategories();
    }
 
    @Override
    public void updateCategories(List<Categorie> categories) {
        dao.updateCategories(categories);
    }
 
    @Override
    public List<Categorie> getAllCategories() {
        return dao.getAllCategories();
    }
 
    @Override
    public Produit getProduitByIdWithCategorie(Long idProduit) {
        return dao.getProduitByIdWithCategorie(idProduit);
    }
 
    @Override
    public Produit getProduitByNameWithCategorie(String nom) {
        return dao.getProduitByNameWithCategorie(nom);
    }
 
    @Override
    public Categorie getCategorieByIdWithProduits(Long idCategorie) {
        return dao.getCategorieByIdWithProduits(idCategorie);
    }
 
    @Override
    public Categorie getCategorieByNameWithProduits(String nom) {
        return dao.getCategorieByNameWithProduits(nom);
    }
 
    @Override
    public Produit getProduitByIdWithoutCategorie(Long idProduit) {
        return dao.getProduitByIdWithoutCategorie(idProduit);
    }
 
    @Override
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie) {
        return dao.getCategorieByIdWithoutProduits(idCategorie);
    }
 
    @Override
    public Produit getProduitByNameWithoutCategorie(String nom) {        
        return dao.getProduitByNameWithoutCategorie(nom);
    }
 
    @Override
    public Categorie getCategorieByNameWithoutProduits(String nom) {
        return dao.getCategorieByNameWithoutProduits(nom);
    }
 
}
  • riga 12: la classe è un singleton Spring;
  • riga 13: che implementa l'interfaccia [IDao] del livello [DAO];
  • righe 16–17: iniezione di un riferimento nel livello [DAO];
  • righe 19–99: implementazione dell'interfaccia [IDao];

L'architettura del livello web si evolve come segue:

  • in [2b], i metodi del/i controller comunicano con il singleton [ApplicationModel];

Questa strategia offre flessibilità nella gestione di una potenziale cache. La classe [ApplicationModel] può essere utilizzata per memorizzare informazioni ottenute dal livello [DAO] o dati di configurazione. Ciò può essere utile quando non si ha il controllo sul livello [DAO]. Questa strategia di caching può evolversi nel tempo. Le modifiche non avranno alcun impatto sul codice dei controller.

13.5.5. Il controller

  

Qui abbiamo un solo controller, la classe [MyController].

13.5.5.1. URL esposti

Gli URL esposti da questo controller sono i seguenti:


    @RequestMapping(value = "/addProducts",
method = RequestMethod.POST,
content-type = "application/json; charset=UTF-8")
    public String addProducts(HttpServletRequest request) {
...
    }
Aggiunge i prodotti al database. Questi vengono inviati tramite POST. La risposta è una stringa JSON contenente l'elenco dei prodotti aggiunti con le loro chiavi primarie.

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

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

Aggiorna i prodotti nel database. Questi vengono inviati tramite POST. La risposta è una stringa JSON contenente l'elenco dei prodotti aggiornati.

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

Recupera la stringa JSON per tutti i prodotti.

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

Aggiunge le categorie al database. Queste vengono inviate tramite POST. La risposta è una stringa JSON contenente l'elenco delle categorie aggiunte insieme alle loro chiavi primarie. Se le categorie contengono prodotti, anche questi vengono aggiunti al database.

    @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 è vuoto.

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

Aggiorna le categorie nel database. Queste vengono inviate tramite POST. La risposta è l'elenco delle categorie aggiornate. Se le categorie contengono prodotti, anche questi vengono aggiornati nel database. Restituisce la stringa JSON delle categorie modificate;

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

Recupera la stringa JSON per tutte le categorie.

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

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

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

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

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

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

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

Recupera la stringa JSON relativa a un prodotto identificato dal suo nome, senza la categoria.

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

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

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

Recupera la stringa JSON relativa a una categoria specificata dal suo nome, insieme ai relativi prodotti.

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

Recupera la stringa JSON relativa a una categoria specificata dal suo nome, senza i relativi prodotti.

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

Recupera la stringa JSON relativa a una categoria identificata dal suo ID, esclusi i relativi prodotti.

Gli URL esposti corrispondono ai metodi dell'interfaccia [IDao] nel livello [DAO]. I metodi del servizio web / JSON sono tutti basati sullo stesso modello. Ne esamineremo alcuni.

13.5.5.2. Lo scheletro del controller

Lo scheletro del controller è il seguente:


package spring.webjson.service;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
 
import javax.servlet.http.HttpServletRequest;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.CharStreams;
 
import spring.data.dao.DaoException;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
import spring.webjson.models.ApplicationModel;
import spring.webjson.models.Response;
 
@Controller
public class MyController {
 
    // spring dependencies
    @Autowired
    private ApplicationModel application;
 
    // filters jSON
    @Autowired
    @Qualifier("jsonMapper")
    private ObjectMapper jsonMapper;
    @Autowired
    @Qualifier("jsonMapperCategorieWithProduits")
    private ObjectMapper jsonMapperCategorieWithProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithCategorie")
    private ObjectMapper jsonMapperProduitWithCategorie;
    @Autowired
    @Qualifier("jsonMapperCategorieWithoutProduits")
    private ObjectMapper jsonMapperCategorieWithoutProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithoutCategorie")
    private ObjectMapper jsonMapperProduitWithoutCategorie;
 
    // class [MyController] is a singleton and is instantiated only once the bean
 
    public MyController() {
        // System.out.println("MyController");
    }
 
    @RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String addProduits(HttpServletRequest request) throws JsonProcessingException {
        ...
    }
 
  • riga 28: l'annotazione [@Controller] rende la classe un componente Spring;
  • righe 32–33: iniezione di un riferimento alla classe [ApplicationModel];
  • righe 36-50: iniezione dei riferimenti ai mappatori JSON;
  • riga 58: l'URL esposto è [/addProducts]. Il client deve utilizzare un metodo [POST] per effettuare la richiesta (method = RequestMethod.POST). Deve inviare il valore inviato come stringa JSON (content-type = "application/json; charset=UTF-8"). Il metodo stesso restituisce la risposta al client (riga 59). Si tratterà di una stringa (riga 60). L'intestazione HTTP [Content-type: application/json; charset=UTF-8] verrà inviata al client per indicare che riceverà una stringa JSON (riga 58);
  • riga 60: il metodo [addProduits] restituisce la stringa JSON contenente l'elenco dei prodotti aggiunti al database;

13.5.5.3. Risposte dei metodi del controller

Tutti i metodi del controller restituiscono il seguente tipo [Response]:

  

package spring.webjson.service;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
    ...
}
  • riga 5: la risposta incapsula un tipo T;
  • riga 13: la risposta di tipo T;
  • righe 9–11: un metodo può incontrare un'eccezione. In questo caso, restituirà una risposta con:
    • riga 9: status!=0;
    • riga 11: l'elenco degli errori riscontrati;

13.5.5.4. L'URL [/addProducts]

L'URL [/addProducts] è gestito dal seguente metodo:


@RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String addProduits(HttpServletRequest request) throws JsonProcessingException {
        // answer
        Response<List<Produit>> response;
        try {
            // retrieve the posted value
            String body = CharStreams.toString(request.getReader());
            List<Produit> produits = jsonMapperProduitWithoutCategorie.readValue(body, new TypeReference<List<Produit>>() {
            });
            // we re-establish the link between products and categories
            for (Produit produit : produits) {
                produit.setCategorie(application.getCategorieByIdWithoutProduits(produit.getIdCategorie()));
            }
            // we persist products
            application.addProduits(produits);
            response = new Respon    se<List<Produit>>(0, null, produits);
        } catch (DaoException e1) {
            response = new Response<List<Produit>>(1000, e1.getErreurs(), null);
        } catch (Exception e2) {
            response = new Response<List<Produit>>(1000, getErreursForException(e2), null);
        }
        // answer jSON
        return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
    }
  • riga 3: il metodo accetta [HttpServletRequest request] come parametro, che incapsula tutte le informazioni relative alla richiesta del client;
  • riga 5: la risposta che verrà inviata al client: un elenco di prodotti;
  • riga 8: recuperiamo il valore inviato. La classe [CharStreams] appartiene alla libreria [Google Guava], il cui riferimento abbiamo aggiunto al file [pom.xml]. Otteniamo la stringa JSON inviata dal client. Dobbiamo deserializzarla per poterla utilizzare;
  • righe 8–10: viene eseguita la deserializzazione. Otteniamo un elenco di prodotti in cui ogni prodotto ha un campo [category=null];
  • righe 12–14: reimpostiamo il campo [category] per tutti i prodotti nell'elenco. Per farlo, utilizziamo il campo [categoryId] del prodotto, che viene inizializzato;
  • riga 16: i prodotti vengono inseriti nel database;
  • riga 17: l'oggetto [response] viene inizializzato con l'elenco dei prodotti;
  • righe 18-19: caso in cui il metodo incontra un'eccezione dal livello [DAO]. Inizializziamo la risposta con [status=1000] (codice di errore) [messages=e1.getMessages()], ovvero inviamo al client l'elenco degli errori riscontrati sul lato server;
  • righe 20–21: caso in cui il metodo incontra un altro tipo di eccezione. Inizializziamo la risposta con [status=1000] (codice di errore) [messages=getErrorsForException(e)] dove [getErrorsForException] è un metodo privato della classe che restituisce l'elenco degli errori associati alle eccezioni nello stack di eccezioni di e, e [body=null];
  • riga 24: viene restituita la stringa JSON della risposta;

13.5.5.5. L'URL [/getAllProducts]

L'URL [/getAllProducts] è gestito dal seguente metodo:


    @RequestMapping(value = "/getAllProduits", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllProduits() throws JsonProcessingException {
        // answer
        Response<List<Produit>> response;
        try {
            response = new Response<List<Produit>>(0, null, application.getAllProduits());
        } catch (DaoException e1) {
            response = new Response<List<Produit>>(1003, e1.getErreurs(), null);
        } catch (Exception e2) {
            response = new Response<List<Produit>>(1003, getErreursForException(e2), null);
        }
        // answer jSON
        return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
  • Riga 1: L'URL [/getAllProduits] viene richiesto utilizzando un'operazione [GET]. Restituisce JSON;
  • riga 2: il metodo stesso invia la risposta JSON al client;
  • riga 5: il metodo restituisce una stringa JSON di tipo [Response<List<Product>>];
  • riga 7: i prodotti vengono richiesti senza la loro categoria;
  • righe 8–12: in caso di errore, la risposta viene inizializzata con un codice di errore e messaggi di errore;
  • riga 14: la risposta JSON viene inviata al client;

13.5.5.6. Conclusione

Non tratteremo gli altri metodi del controller. Sono simili a uno o all'altro dei due metodi che abbiamo appena presentato.

13.5.6. Il servizio Web / Classe di esecuzione JSON

  

La classe [Boot] è la classe eseguibile del progetto:


package spring.webjson.boot;
 
import org.springframework.boot.SpringApplication;
 
import spring.webjson.server.config.AppConfig;
 
public class Boot {
 
    public static void main(String[] args) {
        SpringApplication.run(AppConfig.class, args);
    }
}
  • Riga 10: Viene eseguito il metodo statico [SpringApplication.run]. La classe [SpringApplication] è una classe del progetto [Spring Boot] (riga 3). Le vengono passati due parametri:
    • [AppConfig.class]: la classe che configura l'intera applicazione;
    • [args]: eventuali argomenti passati al metodo [main] alla riga 9. Questo parametro non viene utilizzato in questo caso;

Quando questa classe viene eseguita, vengono generati i seguenti log:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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/**] onto handler of type [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 [/**] onto handler of type [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 per eseguire il servizio web/JSON;
  • righe 25-33: costruzione del livello [DAO];
  • righe 32-51: vengono individuati gli URL esposti;

13.5.7. Test del servizio web / JSON

Per eseguire i test, generiamo il database MySQL [dbintrospringdata] dallo script SQL [dbintrospringdata.sql]:

  

Una volta fatto ciò, utilizziamo [Advanced Rest Client] (vedere la sezione 22.5) per interrogare gli URL esposti dal servizio web / JSON (il servizio web / JSON deve essere in esecuzione).

  • In [1-3], richiediamo l'URL [/getAllCategories] tramite una richiesta HTTP GET;

Riceviamo la seguente risposta:

  • In [1], la richiesta HTTP del client;
  • in [2], la risposta HTTP del server;
  • in [3], lo stato [200 OK] indica che il server ha elaborato con successo la richiesta;
  • in [4], la risposta JSON del server;

La risposta JSON completa è la seguente:


{"status":0,"messages":null,"body":[{"id":415,"version":0,"nom":"categorie0","produits":[{"id":1849,"version":0,"nom":"produit00","idCategorie":415,"prix":100.0,"description":"desc00"},{"id":1850,"version":0,"nom":"produit01","idCategorie":415,"prix":101.0,"description":"desc01"},{"id":1851,"version":0,"nom":"produit02","idCategorie":415,"prix":102.0,"description":"desc02"},{"id":1852,"version":0,"nom":"produit03","idCategorie":415,"prix":103.0,"description":"desc03"},{"id":1853,"version":0,"nom":"produit04","idCategorie":415,"prix":104.0,"description":"desc04"}]},{"id":416,"version":0,"nom":"categorie1","produits":[{"id":1856,"version":0,"nom":"produit12","idCategorie":416,"prix":112.0,"description":"desc12"},{"id":1857,"version":0,"nom":"produit13","idCategorie":416,"prix":113.0,"description":"desc13"},{"id":1858,"version":0,"nom":"produit14","idCategorie":416,"prix":114.0,"description":"desc14"},{"id":1854,"version":0,"nom":"produit10","idCategorie":416,"prix":110.0,"description":"desc10"},{"id":1855,"version":0,"nom":"produit11","idCategorie":416,"prix":111.0,"description":"desc11"}]}]}
  • status:0 significa che non ci sono stati errori lato server;
  • messages: null significa che non ci sono messaggi di errore;
  • body: è il corpo della risposta, in questo caso l'elenco delle categorie con i relativi prodotti. Ci sono due categorie, ciascuna con 5 prodotti;

Aggiungeremo il prodotto [product15] alla categoria [category1]. Per farlo, useremo l'URL [/addCategories], che ha il seguente codice:


@RequestMapping(value = "/addCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String addCategories(HttpServletRequest request) throws JsonProcessingException {
        Response<List<Categorie>> response;
        ObjectMapper mapper = context.getBean(ObjectMapper.class);
        // we persist categories
        try {
            // retrieve the posted value
            String body = CharStreams.toString(request.getReader());
            mapper.setFilters(jsonFilterCategorieWithProduits);
            List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
            });
            // we re-establish the link between products and categories
            for (Categorie categorie : categories) {
                Set<Produit> produits = categorie.getProduits();
                if (produits != null) {
                    for (Produit produit : categorie.getProduits()) {
                        produit.setCategorie(categorie);
                    }
                }
            }
            // we persist categories
            application.addCategories(categories);
            response = new Response<List<Categorie>>(0, null, categories);
        } catch (Exception e) {
            response = new Response<List<Categorie>>(1004, getErreursForException(e), null);
        }
        // answer jSON
        return mapper.writeValueAsString(response);
    }
  • riga 1: il client deve inviare una richiesta POST e il valore inviato deve essere una stringa JSON;
  • righe 9–12: il valore inviato deve essere un elenco di categorie con i prodotti associati;

Creeremo una categoria [category2] con un prodotto [product21]. La stringa JSON da inviare sarà quindi la seguente:

[{"id":null,"version":0,"nom":"categorie2","produits":[{"id":null,"version":0,"nom":"produit21","idCategorie":null,"prix":111.0,"description":"desc21"}]}]

La richiesta al servizio web / JSON viene effettuata come segue:

  • in [1], l'URL richiesto;
  • in [2], la richiesta viene effettuata tramite un'operazione POST;
  • in [3], la stringa JSON inviata;
  • in [4], al server viene comunicato che verranno inviati dati JSON;

La risposta del server è la seguente:

  • in [1], vediamo che sia la categoria che il suo prodotto hanno ora una chiave primaria, il che indica che sono stati probabilmente inseriti nel database. Lo verificheremo utilizzando l'URL [/getCategorieByNameWithProduits/categorie2]:

Otteniamo il seguente risultato:

Abbiamo effettivamente recuperato la categoria [categorie2] con il suo unico prodotto [produit21]. Possiamo anche richiedere solo il prodotto. Per farlo, utilizziamo l'URL [/getProduitByIdWithoutCategorie/1859]:

Otteniamo il seguente risultato:

Tutte le operazioni [GET] possono essere eseguite in un browser web standard:

 

I lettori sono invitati a provare gli altri URL del servizio web /json.

13.6. Un client programmato per il servizio web /json

Ora che il database [dbintrospringdata] è disponibile sul web, scriveremo un'applicazione che lo utilizzi. Avremo quindi la seguente architettura client/server:

L'applicazione client avrà due livelli:

  • un livello [DAO] [2] per comunicare con l'applicazione web /json che espone il database;
  • un livello di test JUnit [1] per verificare che il client e il server funzionino correttamente;

13.6.1. Il progetto Eclipse

Il progetto Eclipse del client è il seguente:

  
  • la cartella [src/main/java] implementa il livello [DAO];
  • la cartella [src/test/java] implementa i test JUnit;

13.6.2. Configurazione del progetto Maven

Il progetto è un progetto Maven configurato dal seguente file [pom.xml]:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.webjson</groupId>
    <artifactId>intro-client-webjson-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <description>Client console du serveur web / jSON</description>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>
 
    <dependencies>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>
        <!-- jSON library used by Spring -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- component used by Spring RestTemplate -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
            <scope>test</scope>
        </dependency>
        <!-- log library -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
    <name>intro-client-webjson-01</name>
</project>
  • righe 14–18: il progetto Maven padre [spring-boot-starter-parent], che ci permette di definire una serie di dipendenze senza specificarne le versioni, poiché queste sono definite nel progetto padre;
  • righe 22–25: sebbene non stiamo scrivendo un'applicazione web, abbiamo bisogno della dipendenza [spring-web], che include la classe [RestTemplate] che consente un facile interfacciamento con un'applicazione web/JSON;
  • righe 27–34: una libreria JSON;
  • righe 36–39: una dipendenza che ci permetterà di impostare un timeout per le richieste HTTP del client. Un timeout è il tempo massimo di attesa per la risposta del server. Trascorso questo tempo, il client segnala un errore di timeout generando un'eccezione;
  • righe 41–46: la libreria Google Guava utilizzata nel test JUnit. Per questo motivo, ne abbiamo impostato l'ambito su [test] (riga 45). Ciò significa che questa dipendenza è inclusa solo quando si esegue il codice dal ramo [src/test/java];
  • righe 48–51: la libreria di logging;
  • righe 52–63: le dipendenze per i test JUnit. In particolare, includono la libreria JUnit 4 necessaria per l'esecuzione dei test. Queste dipendenze presentano l'attributo [<scope>test</scope>], a indicare che sono richieste solo per la fase di test. Non vengono incluse nell'archivio finale del progetto;

13.6.3. Implementazione del livello [DAO]

  
  • Il pacchetto [spring.client.config] contiene la configurazione Spring per il livello [DAO];
  • Il pacchetto [spring.client.dao] contiene l'implementazione del livello [DAO];
  • Il pacchetto [spring.client.entities] contiene gli oggetti scambiati con il servizio web / JSON;

13.6.3.1. Configurazione

  

La classe [DaoConfig] gestisce la configurazione Spring del livello [DAO]. Il suo codice è il seguente:


package spring.client.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
 
@ComponentScan({ "spring.client.dao" })
public class DaoConfig {
 
    // constants
    static private final int TIMEOUT = 1000;
    static private final String URL_WEBJSON = "http://localhost:8080";
 
    @Bean
    public RestTemplate restTemplate(int timeout) {
        // creation of the RestTemplate component
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // exchange timeout
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // result
        return restTemplate;
    }
 
    @Bean
    public int timeout() {
        return TIMEOUT;
    }
 
    @Bean
    public String urlWebJson() {
        return URL_WEBJSON;
    }
 
    // filters jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }
 
    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // result
        return mapper;
    }
}
  • riga 13: la classe è una classe di configurazione Spring; i componenti Spring si trovano nel pacchetto [spring.client.dao];
  • riga 17: viene impostato un timeout di un secondo (1000 ms);
  • righe 32–35: il bean che restituisce questo valore;
  • riga 18: l'URL del servizio web / JSON;
  • righe 37–40: il bean che restituisce questo valore;
  • righe 20–30: la configurazione della classe [RestTemplate] che gestisce la comunicazione con il servizio web / JSON. Quando non è richiesta alcuna configurazione, può essere istanziata nel codice con un semplice [new RestTemplate()]. Qui, vogliamo impostare il timeout per la comunicazione con il servizio web / JSON. Il bean [timeout] alla riga 36 viene passato come parametro al metodo [RestTemplate] alla riga 24;
  • riga 23: il componente [HttpComponentsClientHttpRequestFactory] è quello che ci permette di impostare il timeout per le comunicazioni (righe 29–30);
  • riga 24: la classe [RestTemplate] viene costruita utilizzando questo componente. Poiché si affida a questo componente per comunicare con il servizio web / JSON, gli scambi saranno effettivamente soggetti al timeout;
  • Il client e il server scambieranno righe di testo. Un convertitore gestisce la serializzazione di un oggetto in testo e, viceversa, la deserializzazione del testo in un oggetto. Possono esserci diversi convertitori associati alla classe [RestTemplate] e quello scelto in un dato momento dipende dalle intestazioni HTTP inviate dal server. In questo caso, non avremo alcun convertitore. Pertanto, il componente [RestTemplate] non tenterà in alcun modo di convertire i due elementi seguenti:
    • il testo inviato;
    • il testo ricevuto in risposta;

Questi testi saranno stringhe JSON, che saranno quindi lasciate così come sono dal componente [RestTemplate]. Saremo noi, gli sviluppatori, a eseguire la necessaria serializzazione e deserializzazione JSON. Questo perché i filtri da applicare al valore inviato e alla risposta ricevuta potrebbero differire, e l'esperienza dimostra che è più facile gestirli autonomamente piuttosto che cercare di configurare il componente [RestTemplate] per utilizzare il convertitore JSON corretto;

  • righe 42–92: definiscono i filtri JSON. Questi sono gli stessi filtri lato server presentati e spiegati nella Sezione 13.5.3.1;
  • righe 43–46: un mappatore JSON senza filtri;
  • righe 64–68: un mappatore JSON per recuperare una categoria senza i relativi prodotti;
  • righe 48–58: un mappatore JSON per recuperare una categoria con i relativi prodotti;
  • righe 83–92: un mappatore JSON per recuperare un prodotto senza la sua categoria;
  • righe 60-70: un mappatore JSON per recuperare un prodotto con la sua categoria;

Tutti questi bean saranno disponibili sia per il codice del livello [DAO] che per il test JUnit.

13.6.3.2. Le entità

  

Le entità gestite dal livello [DAO] sono quelle che scambia con il servizio web / JSON. Si tratta degli articoli e dei prodotti. Sul lato server, queste entità presentavano annotazioni di persistenza JPA. Qui, tali annotazioni sono state rimosse. Riportiamo nuovamente il codice delle entità a titolo di riferimento:

[AbstractEntity]


package spring.client.entities;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public abstract class AbstractEntity {
    // properties
    protected Long id;
    protected Long version;
 
    // manufacturers
    public AbstractEntity() {
 
    }
 
    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }
 
    // redefine [equals] and [hashcode]
    @Override
    public int hashCode() {
        return (id != null ? id.hashCode() : 0);
    }
 
    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return id != null && this.id == other.id.longValue();
    }
 
    // signature jSON
    public String toString() {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
 
    // getters and setters
...
}

[Categoria]


package spring.client.entities;
 
import java.util.HashSet;
import java.util.Set;
 
import com.fasterxml.jackson.annotation.JsonFilter;
 
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractEntity {
 
    // properties
    private String nom;
 
    // related products
    public Set<Produit> produits = new HashSet<Produit>();
 
    // manufacturers
    public Categorie() {
 
    }
 
    public Categorie(String nom) {
        this.nom = nom;
    }
 
    // methods
    public void addProduit(Produit produit) {
        // we add the product
        produits.add(produit);
        // set your category
        produit.setCategorie(this);
    }
 
    // getters and setters
    ...
}

[Prodotto]


package spring.webjson.client.entities;
 
import com.fasterxml.jackson.annotation.JsonFilter;
 
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {
 
    // the name
    private String nom;
   // category number
    private Long idCategorie;
   // the price
    private double prix;
   // the description
    private String description;
 
    // the category
    private Categorie categorie;
 
    // manufacturers
    public Produit() {
 
    }
 
    public Produit(String nom, double prix, String description) {
        this.nom = nom;
        this.prix = prix;
        this.description = description;
    }
 
    // getters and setters
...
}

13.6.3.3. La classe [DaoException]

 

Quando il livello [DAO] rileva un errore, genera un'eccezione [DaoException]. Questa classe viene utilizzata sul lato server ed è descritta nella sezione 11.3.7.

13.6.3.4. L'interfaccia del livello [DAO]

 

Il livello [DAO] implementa l'interfaccia [IDao] descritta nella Sezione 11.3.7.


package spring.client.dao;
 
import java.util.List;
 
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
 
public interface IDao {
 
    // insert product list
    public List<Produit> addProduits(List<Produit> produits);
 
    // removal of all products
    public void deleteAllProduits();
 
    // product list update
    public List<Produit> updateProduits(List<Produit> produits);
 
    // all products obtained
    public List<Produit> getAllProduits();
 
    // inserting a list of categories
    public List<Categorie> addCategories(List<Categorie> categories);
 
    // delete all categories
    public void deleteAllCategories();
 
    // updating a list of categories
    public List<Categorie> updateCategories(List<Categorie> categories);
 
    // obtaining all categories
    public List<Categorie> getAllCategories();
 
    // a special product
    public Produit getProduitByIdWithCategorie(Long idProduit);
 
    public Produit getProduitByIdWithoutCategorie(Long idProduit);
 
    public Produit getProduitByNameWithCategorie(String nom);
 
    public Produit getProduitByNameWithoutCategorie(String nom);
 
    // a special category
    public Categorie getCategorieByIdWithProduits(Long idCategorie);
 
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie);
 
    public Categorie getCategorieByNameWithProduits(String nom);
 
    public Categorie getCategorieByNameWithoutProduits(String nom);
 
}

13.6.3.5. Il servizio web / Risposta JSON

  

Abbiamo visto che tutti gli URL del servizio web / JSON restituiscono un tipo [Response] definito nella sezione 13.5.5.3. Riportiamo qui questa classe:


package spring.client.dao;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
    ...
}

13.6.3.6. Implementazione della comunicazione con il servizio web / JSON

  

La classe [ AbstractDao] implementa la comunicazione con il servizio web / JSON:


package spring.client.dao;
 
import java.net.URI;
import java.net.URISyntaxException;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.web.client.RestTemplate;
 
public abstract class AbstractDao {
 
    // data
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;
 
    // generic request
    protected String getResponse(String url, String jsonPost) {
 
        // url : URL to contact
        // jsonPost: the jSON value to be posted
        try {
            // request execution
            RequestEntity<?> request;
            if (jsonPost != null) {
                // query POST
                request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
            } else {
                // query GET
                request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON).build();
            }
            // execute the query
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e1) {
            throw new DaoException(20, e1);
        } catch (RuntimeException e2) {
            throw new DaoException(21, e2);
        }
    }
 
}
 
  • righe 15-16: inserimento del componente [RestTemplate], che gestisce la comunicazione con il server;
  • righe 17-18: iniezione dell'URL del servizio web / JSON;

L'implementazione dei metodi per la comunicazione con il server è raggruppata nel metodo [getResponse]:

  • riga 21: il metodo riceve 2 parametri:
    • [url]: l'URL richiesto;
    • [jsonPost]: la stringa JSON da inviare, oppure null in caso contrario. Se [jsonPost == null], la richiesta URL viene effettuata utilizzando un GET; altrimenti, utilizzando un POST;
  • riga 38: l'istruzione che invia la richiesta al server e riceve la sua risposta. Il componente [RestTemplate] offre un'ampia gamma di metodi per interagire con il server. Qui abbiamo scelto il metodo [exchange], ma ne sono disponibili altri;
  • righe 27–36: dobbiamo costruire la richiesta [RequestEntity]. Essa varia a seconda che si utilizzi una richiesta GET o POST;
  • righe 30–31: la richiesta per un GET. La classe [RequestEntity] fornisce metodi statici per creare richieste GET, POST, HEAD e altre. Il metodo [RequestEntity.get] consente di creare una richiesta GET concatenando i vari metodi che la compongono:
    • il metodo [RequestEntity.get] accetta l'URL di destinazione come parametro sotto forma di un'istanza URI,
    • il metodo [accept] consente di definire gli elementi dell'intestazione HTTP [Accept]. Qui, specifichiamo che accettiamo il tipo [application/json] che il server invierà;
    • il metodo [build] utilizza queste informazioni per costruire il tipo [RequestEntity] della richiesta;
  • righe 34–35: la richiesta POST. Il metodo [RequestEntity.post] crea una richiesta POST concatenando i vari metodi che la compongono:
    • il metodo [RequestEntity.post] accetta l'URL di destinazione come parametro sotto forma di un'istanza URI,
    • il metodo [header] definisce un'intestazione HTTP. In questo caso, inviamo al server l'intestazione [Content-Type: application/json] per indicare che i dati inviati arriveranno sotto forma di stringa JSON;
    • il metodo [accept] ci permette di indicare che accettiamo il tipo [application/json] che il server invierà;
    • il metodo [body] imposta il valore inviato. Questo è il quarto parametro del metodo generico [getResponse] (riga 1);
  • riga 38: il metodo [RestTemplate].exchange restituisce un tipo [ResponseEntity<String>] che incapsula l'intera risposta del server: intestazioni HTTP e corpo del documento. Il metodo [ResponseEntity].getBody() recupera questo corpo, che rappresenta la risposta del server—in questo caso, una stringa;

13.6.3.7. Implementazione dell'interfaccia [IDao]

  

La classe [Dao] implementa l'interfaccia [IDao]:


package spring.client.dao;
 
import java.io.IOException;
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
 
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
 
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
 
@Component
public class Dao extends AbstractDao implements IDao {
 
    @Autowired
    private ApplicationContext context;
 
    // filters jSON
    @Autowired
    @Qualifier("jsonMapper")
    private ObjectMapper jsonMapper;
    @Autowired
    @Qualifier("jsonMapperCategorieWithProduits")
    private ObjectMapper jsonMapperCategorieWithProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithCategorie")
    private ObjectMapper jsonMapperProduitWithCategorie;
    @Autowired
    @Qualifier("jsonMapperCategorieWithoutProduits")
    private ObjectMapper jsonMapperCategorieWithoutProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithoutCategorie")
    private ObjectMapper jsonMapperProduitWithoutCategorie;
 
    @Override
    public List<Produit> addProduits(List<Produit> produits) {
        // ----------- add products (without category)
        ...
}
  • riga 17: la classe [Dao] è un componente Spring in cui possono essere iniettati altri componenti Spring;
  • riga 18: la classe [Dao] estende la classe [AbstractDao] che abbiamo appena visto e implementa l'interfaccia [IDao];
  • righe 20–21: iniettiamo il contesto Spring per accedere ai suoi bean;
  • righe 24–38: iniezione dei mappatori JSON definiti nella classe [AppConfig] presentata nella sezione 13.6.2;

Le implementazioni dei vari metodi dell'interfaccia [IDao] seguono tutte lo stesso schema. Presenteremo due metodi, uno basato su un'operazione [POST], l'altro su un'operazione [GET].

Un esempio di [GET]: [getCategorieByNameWithProduits]


@Override
    public Categorie getCategorieByNameWithProduits(String nom) {
        // ----------- obtain a category designated by its name, with its products
        try {
            // request
            Response<Categorie> response = jsonMapperCategorieWithProduits.readValue(
                    getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null),
                    new TypeReference<Response<Categorie>>() {
                    });
            // mistake?
            if (response.getStatus() != 0) {
                // 1 exception is thrown
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // render the core of the server response
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (RuntimeException | IOException e2) {
            throw new DaoException(113, e2);
        }
    }
  • Riga 7: Viene chiamato il metodo [getResponse] della classe padre. Questo metodo gestisce la comunicazione con il servizio web/JSON. I suoi parametri sono i seguenti:

getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null)
  • (continua)
    • l'URL del servizio interrogato [/getCategoryByNameWithProducts/name];
    • il valore inviato. In questo caso, non ce n'è nessuno;

Il metodo [getResponse] restituisce una stringa che rappresenta la risposta JSON inviata dal server. Deserializziamo questa risposta JSON come segue:


jsonMapperCategorieWithProduits.readValue(
                    jsonResponse,
                    new TypeReference<Response<Categorie>>() {
});

poiché la stringa JSON è la serializzazione di un tipo [Response<Category>];

  • righe 11–17: controlliamo lo stato della risposta. Se lo stato non è 0, significa che si è verificato un errore lato server. A quel punto generiamo un'eccezione (riga 13), utilizzando le informazioni contenute nella risposta (stato ed elenco dei messaggi di errore);
  • riga 16: se non si è verificato alcun errore lato server, restituiamo il corpo di tipo [Response<Category>], ovvero la categoria richiesta;
  • righe 18–19: gestiamo l'eccezione generata alla riga 16;
  • righe 20–22: gestiamo tutte le altre eccezioni;

Un esempio di [POST]: [addCategories]


@Override
    public List<Categorie> addCategories(List<Categorie> categories) {
        // ----------- add categories (with their products)
        try {
            // request
            Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
                    getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories)),
                    new TypeReference<Response<List<Categorie>>>() {
                    });
            // mistake?
            if (response.getStatus() != 0) {
                // 1 exception is thrown
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // render the core of the server response
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (RuntimeException | IOException e2) {
            throw new DaoException(104, e2);
        }
    }
  • riga 2: il metodo [addCategories] viene utilizzato per salvare nel database le categorie passate come parametri. Restituisce queste stesse categorie arricchite con le loro chiavi primarie. Se le categorie vengono passate insieme ai prodotti, anche questi vengono salvati;
  • riga 7: viene chiamato il metodo [getResponse] del genitore per gestire la comunicazione con il servizio web / JSON;
    • il primo parametro è l'URL [/addCategories];
    • il secondo parametro è il valore inviato, in questo caso l'elenco delle categorie da salvare;

getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories))

La stringa JSON risultante viene quindi deserializzata per ottenere il tipo [Response<List<Category>] previsto:


Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
                    jsonResponse,
                    new TypeReference<Response<List<Categorie>>>() {
                    });
  • righe 11–17: gestione della risposta del server (errore o meno);
  • righe 20–22: gestione delle eccezioni;

Tutti gli altri metodi seguono lo schema dei due metodi presentati.

13.6.4. Il test JUnit

Torniamo all'architettura client/server attualmente in fase di sviluppo:

Abbiamo realizzato un livello [DAO] [2] con la stessa interfaccia del livello [DAO] [4]. Per testare il livello [DAO] [2], possiamo quindi utilizzare il test JUnit che è stato utilizzato per testare il livello [DAO] [4]. Come promemoria, è il seguente:

  

package spring.client.junit;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
 
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
 
import spring.client.config.DaoConfig;
import spring.client.dao.DaoException;
import spring.client.dao.IDao;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
 
@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {

    // layer [DAO]
    @Autowired
    private IDao dao;
 
    // filters jSON
    @Autowired
    @Qualifier("jsonMapper")
    private ObjectMapper jsonMapper;
    @Autowired
    @Qualifier("jsonMapperCategorieWithProduits")
    private ObjectMapper jsonMapperCategorieWithProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithCategorie")
    private ObjectMapper jsonMapperProduitWithCategorie;
    @Autowired
    @Qualifier("jsonMapperCategorieWithoutProduits")
    private ObjectMapper jsonMapperCategorieWithoutProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithoutCategorie")
    private ObjectMapper jsonMapperProduitWithoutCategorie;
 
    @Before
    public void cleanAndFill() {
        // the base is cleaned before each test
        log("Vidage de la base de données", 1);
        // table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
        dao.deleteAllCategories();
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // fill the tables
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < 2; i++) {
            Categorie categorie = new Categorie(String.format("categorie%d", i));
            for (int j = 0; j < 5; j++) {
                categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                        String.format("desc%d%d", i, j)));
            }
            categories.add(categorie);
        }
        // add the category - the products will be cascaded in as well
        categories = dao.addCategories(categories);
    }
 
    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // list of categories
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories();
        affiche(categories, jsonMapperCategorieWithoutProduits);
        // product list
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits();
        affiche(produits, jsonMapperProduitWithoutCategorie);
        // a few checks
        Assert.assertEquals(2, categories.size());
        Assert.assertEquals(10, produits.size());
        Categorie categorie = findCategorieByName("categorie0", categories);
        Assert.assertNotNull(categorie);
        Produit produit = findProduitByName("produit03", produits);
        Assert.assertNotNull(produit);
        Long idCategorie = produit.getIdCategorie();
        Assert.assertEquals(categorie.getId(), idCategorie);
    }
 
    @Test
    public void getCategorieByNameWithProduits() {
        log("getCategorieByNameWithProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
        Assert.assertNotNull(categorie1);
        Assert.assertEquals(5, categorie1.getProduits().size());
    }
 
    @Test
    public void getCategorieByNameWithoutProduits() {
        log("getCategorieByNameWithoutProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithoutProduits("categorie1");
        Assert.assertNotNull(categorie1);
        Assert.assertEquals("categorie1", categorie1.getNom());
    }
 
    @Test
    public void getCategorieByIdWithProduits() {
        log("getCategorieByIdWithProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
        Categorie categorie2 = dao.getCategorieByIdWithProduits(categorie1.getId());
        Assert.assertNotNull(categorie2);
        Assert.assertEquals(categorie1.getId(), categorie2.getId());
        Assert.assertEquals(categorie1.getNom(), categorie2.getNom());
    }
 
    @Test
    public void getCategorieByIdWithoutProduits() {
        log("getCategorieByIdWithoutProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
        Categorie categorie2 = dao.getCategorieByIdWithoutProduits(categorie1.getId());
        Assert.assertNotNull(categorie2);
        Assert.assertEquals(categorie1.getNom(), categorie2.getNom());
    }
 
    @Test
    public void getProduitByNameWithCategorie() {
        log("getProduitByNameWithCategorie", 1);
        Produit produit = dao.getProduitByNameWithCategorie("produit03");
        Assert.assertNotNull(produit);
        Assert.assertNotNull(produit.getCategorie());
    }
 
    @Test
    public void getProduitByNameWithoutCategorie() {
        log("getProduitByNameWithoutCategorie", 1);
        Produit produit = dao.getProduitByNameWithoutCategorie("produit03");
        Assert.assertNotNull(produit);
        Assert.assertEquals("produit03", produit.getNom());
    }
 
    @Test
    public void getProduitByIdWithCategorie() {
        log("getProduitByNameWithCategorie", 1);
        Produit produit = dao.getProduitByNameWithCategorie("produit03");
        Produit produit2 = dao.getProduitByIdWithCategorie(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
        Assert.assertEquals(produit.getCategorie().getId(), produit2.getCategorie().getId());
    }
 
    @Test
    public void getProduitByIdWithoutCategorie() {
        log("getProduitByIdWithoutCategorie", 1);
        Produit produit = dao.getProduitByNameWithCategorie("produit03");
        Produit produit2 = dao.getProduitByIdWithoutCategorie(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
    }
 
    @Test
    public void doInsertsInTransaction() {
        log("Ajout d'une catégorie [cat1] avec deux produits de même nom", 1);
        // we insert
        Categorie categorie = new Categorie("cat1");
        categorie.addProduit(new Produit("x", 1.0, ""));
        categorie.addProduit(new Produit("x", 1.0, ""));
        // add the category - the products will be cascaded in as well
        try {
            categorie = dao.addCategories(Lists.newArrayList(categorie)).get(0);
        } catch (DaoException e) {
            show("Les erreurs suivantes se sont produites :", e.getErreurs());
        }
        // checks
        List<Categorie> categories = dao.getAllCategories();
        Assert.assertEquals(2, categories.size());
        List<Produit> produits = dao.getAllProduits();
        Assert.assertEquals(10, produits.size());
    }
 
    @Test
    public void updateDataBase() {
        log("Mise à jour du prix des produits de [categorie1]", 1);
        Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
        Categorie categorie1Saved = dao.getCategorieByNameWithProduits("categorie1");
        Set<Produit> produits = categorie1.getProduits();
        for (Produit produit : produits) {
            produit.setPrix(1.1 * produit.getPrix());
        }
        List<Produit> produits2 = Lists.newArrayList(produits);
        produits2 = dao.updateProduits(produits2);
        // checks
        List<Produit> produitsSaved = Lists.newArrayList(categorie1Saved.getProduits());
        for (Produit produit2 : produits2) {
            Produit produit = findProduitByName(produit2.getNom(), produitsSaved);
            Assert.assertEquals(produit2.getPrix(), produit.getPrix() * 1.1, 1e-6);
        }
    }
 
    @Test
    public void addProduits() throws BeansException, JsonProcessingException {
        log("Ajout de deux produits de catégorie [categorie0]", 1);
        Categorie categorie0 = dao.getCategorieByNameWithoutProduits("categorie0");
        Long idCategorie = categorie0.getId();
        Produit p1 = new Produit("x", 1, "");
        p1.setIdCategorie(idCategorie);
        p1.setCategorie(categorie0);
        Produit p2 = new Produit("y", 1, "");
        p2.setIdCategorie(idCategorie);
        p2.setCategorie(categorie0);
        List<Produit> produits = new ArrayList<Produit>();
        produits.add(p1);
        produits.add(p2);
        produits = dao.addProduits(produits);
        // check
        affiche(produits, jsonMapperProduitWithoutCategorie);
    }
 
    // -------------- private methods
    private Produit findProduitByName(String nom, List<Produit> produits) {
        for (Produit produit : produits) {
            if (produit.getNom().equals(nom)) {
                return produit;
            }
        }
        return null;
    }
 
    private Categorie findCategorieByName(String nom, List<Categorie> categories) {
        for (Categorie categorie : categories) {
            if (categorie.getNom().equals(nom)) {
                return categorie;
            }
        }
        return null;
    }
 
    // display of a T-type element
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }
 
    // display a list of elements of type T
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }
 
    private static void log(String message, int mode) {
        // poster message
        String toPrint = null;
        switch (mode) {
        case 1:
            toPrint = String.format("%s --------------------------------", message);
            break;
        case 2:
            toPrint = String.format("-- %s", message);
            break;
        }
        System.out.println(toPrint);
    }
 
    private static void show(String title, List<String> messages) {
        // title
        System.out.println(String.format("%s : ", title));
        // messages
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }
 
}

L'esecuzione va a buon fine e produce i seguenti risultati sulla console:


Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Ajout de deux produits de catégorie [categorie0] --------------------------------
{"id":6285,"version":0,"nom":"x","idCategorie":1319,"prix":1.0,"description":""}
{"id":6286,"version":0,"nom":"y","idCategorie":1319,"prix":1.0,"description":""}
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Mise à jour du prix des produits de [categorie1] --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByIdWithoutProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithoutCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByNameWithProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByNameWithoutProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByIdWithoutCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
-- Liste des catégories
{"id":1337,"version":0,"nom":"categorie0"}
{"id":1338,"version":0,"nom":"categorie1"}
-- Liste des produits
{"id":6367,"version":0,"nom":"produit00","idCategorie":1337,"prix":100.0,"description":"desc00"}
{"id":6368,"version":0,"nom":"produit01","idCategorie":1337,"prix":101.0,"description":"desc01"}
{"id":6369,"version":0,"nom":"produit02","idCategorie":1337,"prix":102.0,"description":"desc02"}
{"id":6370,"version":0,"nom":"produit03","idCategorie":1337,"prix":103.0,"description":"desc03"}
{"id":6371,"version":0,"nom":"produit04","idCategorie":1337,"prix":104.0,"description":"desc04"}
{"id":6372,"version":0,"nom":"produit10","idCategorie":1338,"prix":110.0,"description":"desc10"}
{"id":6373,"version":0,"nom":"produit11","idCategorie":1338,"prix":111.0,"description":"desc11"}
{"id":6374,"version":0,"nom":"produit12","idCategorie":1338,"prix":112.0,"description":"desc12"}
{"id":6375,"version":0,"nom":"produit13","idCategorie":1338,"prix":113.0,"description":"desc13"}
{"id":6376,"version":0,"nom":"produit14","idCategorie":1338,"prix":114.0,"description":"desc14"}
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByIdWithProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Ajout d'une catégorie [cat1] avec deux produits de même nom --------------------------------
Les erreurs suivantes se sont produites : 
- org.hibernate.exception.ConstraintViolationException: could not execute statement
- could not execute statement
- Duplicate entry 'x' for key 'NOM'
11:24:37.650 [Thread-1] INFO  o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@f8c1ddd: startup date [Fri Nov 20 11:24:34 CET 2015]; root of context hierarchy