Skip to content

18. Un client programmato per il servizio web /JSON

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

L'applicazione client avrà tre livelli:

  • un livello [Client HTTP] [3] per comunicare con l'applicazione web /jSON che espone il database;
  • un livello [DAO] [2] che presenterà la stessa interfaccia del livello [DAO] [4];
  • un livello di test JUnit [1] per verificare che il client e il server funzionino correttamente;

18.1. Il progetto Eclipse

Il progetto Eclipse del client è il seguente:

 
  • Il pacchetto [spring.webjson.client.config] contiene la configurazione Spring per il livello [DAO];
  • il pacchetto [spring.webjson.client.dao] contiene l'implementazione del livello [DAO];
  • il pacchetto [spring.webjson.client.entities] contiene gli oggetti scambiati con il servizio web / JSON. Li conosciamo tutti;
  • il pacchetto [spring.webjson.client.infrastructure] contiene le classi di eccezione utilizzate dal progetto. Le conosciamo tutte;

18.2. Configurazione Maven del progetto

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


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-webjson-client-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>
 
    <description>Client console du serveur web / jSON</description>
    <name>spring-webjson-client-generic</name>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.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>
        </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>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  • righe 16–20: 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 24–27: 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 o JSON;
  • righe 29–36: una libreria JSON;
  • righe 38–41: una dipendenza che ci permetterà di impostare un timeout per le richieste HTTP del client. Un timeout è il tempo massimo di attesa per una risposta del server. Trascorso questo tempo, il client segnala un errore di timeout generando un'eccezione;
  • righe 43–48: la libreria Google Guava;
  • righe 50–53: la libreria di logging;
  • righe 54–64: la dipendenza per i test JUnit. Include la libreria JUnit 4 necessaria per i test. Queste dipendenze hanno l'attributo [<scope>test</scope>], che indica che sono necessarie solo per la fase di test. Non sono incluse nell'archivio finale del progetto;

18.3. Configurazione Spring

  

La classe [AppConfig] gestisce la configurazione Spring per il client HTTP. Il suo codice è il seguente:


package spring.webjson.client.config;
 
import java.util.ArrayList;
import java.util.List;
 
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
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;
 
@Configuration
@ComponentScan({ "spring.webjson.client.dao" })
public class AppConfig {
 
    // constants
    static private final int TIMEOUT = 1000;
    static private final String URL_WEBJSON = "http://localhost:8081";
 
    // filters jSON
    @Bean
    public ObjectMapper jsonMapper(RestTemplate restTemplate) {
        return ((MappingJackson2HttpMessageConverter) (restTemplate.getMessageConverters().get(0))).getObjectMapper();
    }
 
    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperShortCategorie(RestTemplate restTemplate) {
        ObjectMapper jsonMapper = jsonMapper(restTemplate);
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        return jsonMapper;
    }
 
    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperLongCategorie(RestTemplate restTemplate) {
        ObjectMapper jsonMapper = jsonMapper(restTemplate);
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        return jsonMapper;
    }
 
    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperShortProduit(RestTemplate restTemplate) {
        ObjectMapper jsonMapper = jsonMapper(restTemplate);
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        return jsonMapper;
    }
 
    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperLongProduit(RestTemplate restTemplate) {
        ObjectMapper jsonMapper = jsonMapper(restTemplate);
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        return jsonMapper;
    }
 
    @Bean
    public RestTemplate restTemplate(int timeout) {
        // creation of the RestTemplate component
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // converter jSON
        List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
        messageConverters.add(new MappingJackson2HttpMessageConverter());
        restTemplate.setMessageConverters(messageConverters);
        // exchange timeout
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // result
        return restTemplate;
    }
 
    @Bean
    public int timeout() {
        return TIMEOUT;
    }
 
    @Bean
    public String urlWebJson() {
        return URL_WEBJSON;
    }
}
  • riga 20: la classe è una classe di configurazione Spring;
  • riga 21: altri componenti Spring sono disponibili nel pacchetto [spring.webjson.client.dao];
  • riga 25: è impostato un timeout di un secondo (1000 ms);
  • righe 88–91: il bean che restituisce questo valore;
  • riga 26: l'URL del servizio web / JSON;
  • righe 93–96: il bean che restituisce questo valore;
  • righe 72–86: 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 89 viene passato come parametro al metodo [restTemplate] alla riga 73;
  • riga 75: il componente [HttpComponentsClientHttpRequestFactory] è quello che ci permette di impostare il timeout per le comunicazioni (righe 82–83);
  • riga 76: 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;
  • righe 78–80: associamo un convertitore JSON alla classe [RestTemplate]. Ne abbiamo già parlato quando abbiamo studiato il servizio web. Il client e il server si scambiano righe di testo. Un convertitore serializza un oggetto in testo e deserializza il testo in un oggetto. Possono esserci più convertitori associati alla classe [RestTemplate] e quello scelto in un dato momento dipende dalle intestazioni HTTP inviate dal server. Qui abbiamo solo un convertitore JSON poiché le righe di testo scambiate sono JSON;
  • righe 82–83: vengono impostati i timeout di scambio;
  • righe 28–70: definizione dei filtri JSON. Si tratta degli stessi filtri presenti sul server descritti nella sezione 17.3.2.1;
  • righe 29–32: il bean [jsonMapper] è il mappatore JSON per il [MappingJackson2HttpMessageConverter] che abbiamo associato alla classe [RestTemplate]. Ne abbiamo bisogno nella definizione dei filtri JSON;
  • righe 34–41: un bean che definisce il filtro JSON [categoria senza i suoi prodotti]. Il metodo [jsonMapperShortCategory] accetta come parametro il bean [RestTemplate] definito alla riga 73;
  • riga 37: chiamiamo il metodo [jsonMapper] dalla riga 30 per recuperare il mappatore JSON;
  • righe 38–39: impostiamo il filtro in modo che restituisca una categoria senza i relativi prodotti;
  • riga 40: il mappatore JSON viene restituito come configurato;
  • righe 42–51: il filtro JSON per recuperare una categoria insieme ai suoi prodotti;
  • righe 53–60: il filtro JSON per recuperare un prodotto senza la sua categoria;
  • righe 62–70: il filtro JSON per recuperare un prodotto con la sua categoria;

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

18.4. Implementazione del client HTTP

Quello che vedete sopra è il livello [Client HTTP] che comunica con il servizio web che abbiamo appena creato. Ora lo esamineremo.

  

La classe [Client] gestisce la comunicazione con il servizio web / JSON. Essa implementa la seguente interfaccia [IClient]:


package spring.webjson.client.dao;
 
import org.springframework.http.HttpMethod;
 
public interface IClient {
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body);
}

L'interfaccia ha un solo metodo [getResponse]:

  • riga 6: il metodo [getResponse] è un metodo generico parametrizzato da due tipi:
    • [T1]: è il tipo di risposta previsto dal server in [Response<T1>], ad esempio [List<Category>],
    • [T2]: è il tipo del parametro JSON inviato tramite operazioni POST, ad esempio [List<Product>];
  • riga 6: il metodo [getResponse] restituisce un risultato di tipo T1, ad esempio [List<Category>];
  • riga 6: i parametri di [getResponse] sono i seguenti:
    • [String url]: l'URL da interrogare;
    • [HttpMethod method]: metodo HTTP della richiesta, GET o POST a seconda dei casi,
    • [int errStatus]: codice di errore da utilizzare nella classe [DaoException], se si verifica un errore durante la comunicazione con il server,
    • [T2 body]: il valore da inviare se viene effettuata una richiesta POST;

La classe [Client] implementa l'interfaccia [IClient] come segue:


package spring.webjson.client.dao;
 
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
 
import spring.webjson.client.infrastructure.DaoException;
 
@Component
public class Client implements IClient {
 
    // injections
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;
 
    // local
    private String simpleClassName = getClass().getSimpleName();
 
    // generic request
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
    ...
    }
 
    // list of exception error messages
    protected List<String> getMessagesForException(Exception exception) {
    ...
    }
}
  • riga 18: la classe [Client] è un componente Spring e può quindi essere iniettata in altri componenti Spring;
  • righe 22–23: iniezione del bean [RestTemplate] definito in [AppConfig] (vedere la sezione 18.3), che gestisce la comunicazione con il server;
  • righe 24–25: iniezione dell'URL del servizio web / JSON definito in [AppConfig] (vedi sezione 18.3);
  • Righe 37–39: il metodo privato [getMessagesForException] è un metodo di utilità utilizzato per recuperare l'elenco dei messaggi di errore contenuti in un'eccezione. Lo abbiamo incontrato diverse volte;

Continuiamo:


    // generic request
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
        // the server response
        ResponseEntity<Response<T1>> response;
        try {
            // prepare the query
            RequestEntity<?> request = null;
            if (method == HttpMethod.GET) {
                request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON).build();
            }
            if (method == HttpMethod.POST) {
                request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(body);
            }
            // execute the query
            response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
            });
        } catch (Exception e) {
            // encapsulate the exception
            throw new DaoException(errStatus, e, simpleClassName);
        }
        ...
}
  • riga 18: l'istruzione che invia la richiesta al server e ne riceve la risposta. Il componente [RestTemplate] offre un gran numero di metodi per interagire con il server, ma solo il metodo [exchange] accetta parametri generici. Ecco perché è stato scelto. Il secondo parametro specifica il tipo di risposta prevista. Il primo parametro è la richiesta [RequestEntity] (riga 8). Il risultato del metodo [exchange] è di tipo [ResponseEntity<Response<T1>>] (riga 5). Il tipo [ResponseEntity] incapsula la risposta completa del server, incluse le intestazioni HTTP e il documento inviato dal server. Analogamente, il tipo [RequestEntity] incapsula l'intera richiesta del client, incluse le intestazioni HTTP e qualsiasi dato inviato;
  • righe 8–16: dobbiamo costruire la richiesta [RequestEntity]. Essa varia a seconda che si utilizzi una richiesta GET o POST;
  • riga 10: la richiesta GET. La classe [RequestEntity] fornisce metodi statici per creare richieste GET, POST, HEAD e di altro tipo. 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;
  • Riga 14: la richiesta POST. Il metodo [RequestEntity.post] consente di creare 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. Qui, inviamo l'intestazione [Content-Type: application/json] al server 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);
  • Righe 20–23: Se si verifica un errore di comunicazione con il server, viene generata un'eccezione [DaoException] con il codice di errore impostato sul parametro [errStatus] passato come terzo parametro al metodo generico [getResponse] (riga 3);

Il metodo [getResponse] prosegue come segue:


// generic request
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
    ...
        // retrieve the body of the reply
        Response<T1> entity = response.getBody();
        int status = entity.getStatus();
        // server-side errors?
        if (status != 0) {
            // create an exception
            throw new DaoException(status, new RuntimeException(entity.getException()), simpleClassName);
        } else {
            // it's good
            return entity.getBody();
        }
    }
  • riga 4: abbiamo ricevuto la risposta dal server. È di tipo [ResponseEntity<Response<T1>>] (riga 5 dell'esempio di codice precedente) dove la classe [Response] è la classe già utilizzata sul lato server:

package spring.webjson.client.dao;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // the possible exception
    private String exception;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, String exception, T body) {
        this.status = status;
        this.exception = exception;
        this.body = body;
    }
 
    // getters and setters
...
}

Torniamo al metodo [getResponse]:

  • riga 6: recuperiamo l'oggetto [Response<T1>] incapsulato nella risposta. Questo tipo ha i campi [int status, String exception, T1 body];
  • riga 7: recuperiamo lo [status] della risposta, che è un codice di errore;
  • righe 9–12: se c'è un errore, generiamo un'eccezione contenente le due informazioni [status, exception] dalla risposta del server;
  • riga 14: altrimenti, restituiamo il tipo [T1] contenuto nella risposta [Response<T1>];

La classe [Client] è generica. Può essere utilizzata per qualsiasi client web/JSON.

18.5. Implementazione del livello [Dao]

  

18.5.1. La classe [AbstractDao]

Il livello [DAO] lato client ha la stessa interfaccia del livello [DAO] lato server (vedere la sezione 4.7):


package spring.webjson.client.dao;
 
import java.util.List;
 
import spring.webjson.client.entities.AbstractCoreEntity;
 
public interface IDao<T extends AbstractCoreEntity> {
 
    // list of all T entities
    public List<T> getAllShortEntities();
 
    public List<T> getAllLongEntities();
 
    // special entities - short version
    public List<T> getShortEntitiesById(Iterable<Long> ids);
 
    public List<T> getShortEntitiesById(Long... ids);
 
    public List<T> getShortEntitiesByName(Iterable<String> names);
 
    public List<T> getShortEntitiesByName(String... names);
 
    // special entities - long version
    public List<T> getLongEntitiesById(Iterable<Long> ids);
 
    public List<T> getLongEntitiesById(Long... ids);
 
    public List<T> getLongEntitiesByName(Iterable<String> names);
 
    public List<T> getLongEntitiesByName(String... names);
 
    // update of several entities
    public List<T> saveEntities(Iterable<T> entities);
 
    public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
 
    // delete all entities
    public void deleteAllEntities();
 
    // deletion of multiple entities
    public void deleteEntitiesById(Iterable<Long> ids);
 
    public void deleteEntitiesById(Long... ids);
 
    public void deleteEntitiesByName(Iterable<String> names);
 
    public void deleteEntitiesByName(String... names);
 
    public void deleteEntitiesByEntity(Iterable<T> entities);
 
    public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}

La classe [AbstractDao] implementa l'interfaccia [IDao]. È analoga alla classe omonima sul lato server (vedi Sezione 4.8). Funge da classe padre per le classi [DaoCategorie] e [DaoProduit]. Non è identica per due motivi:

  • sul lato server, la classe [AbstractDao] gestisce un'unica informazione:

    // injections
    @Autowired
    @Qualifier("maxPreparedStatementParameters")
    protected int maxPreparedStatementParameters;

di cui qui non abbiamo bisogno.

  • Sul lato server, la classe [AbstractDao] utilizza le annotazioni [@Transactional] per incapsulare ogni metodo all'interno di una transazione. Sul lato client, non c'è alcun database da gestire. Questa annotazione scompare quindi;

La classe [AbstractDao] verifica semplicemente la validità dei parametri di chiamata per i metodi dell'interfaccia [IDao] prima di delegare la chiamata alle classi figlie:


package spring.webjson.client.dao;
 
import java.util.ArrayList;
import java.util.List;
 
import spring.webjson.client.entities.AbstractCoreEntity;
import spring.webjson.client.infrastructure.MyIllegalArgumentException;
 
import com.google.common.collect.Lists;
 
public abstract class AbstractDao<T1 extends AbstractCoreEntity> implements IDao<T1> {
 
    // local
    protected String simpleClassName = getClass().getSimpleName();
 
    @Override
    public List<T1> getShortEntitiesById(Iterable<Long> ids) {
        // argument validity
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // result
        return getShortEntitiesById(Lists.newArrayList(ids));
    }
 
    @Override
    public List<T1> getShortEntitiesById(Long... ids) {
        // argument validity
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // result
        return getShortEntitiesById(Lists.newArrayList(ids));
    }
...
    @Override
    public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T1... entities) {
        ...
    }
 
    // méthodes privées ----------------------------------------------
    private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T3> elements) {
        // elements null ?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),
                    simpleClassName);
        }
        // empty elements?
        if (!elements.iterator().hasNext()) {
            if (checkEmpty) {
                throw new MyIllegalArgumentException(223, new RuntimeException("l'argument ne peut être une liste vide"),simpleClassName);
            } else {
                return new ArrayList<T1>();
            }
        }
        // default result
        return null;
    }
 
    @SuppressWarnings("unchecked")
    private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, T3... elements) {
        // elements null ?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),simpleClassName);
        }
        // empty elements?
        if (elements.length == 0) {
            if (checkEmpty) {
                throw new MyIllegalArgumentException(223, new RuntimeException("L'argument ne peut être une liste vide"),
                        simpleClassName);
            } else {
                return new ArrayList<T1>();
            }
        }
        // default result
        return null;
    }
 
    // méthodes protégées ----------------------------------------------
    abstract protected List<T1> getShortEntitiesById(List<Long> ids);
 
    abstract protected List<T1> getShortEntitiesByName(List<String> names);
 
    abstract protected List<T1> getLongEntitiesById(List<Long> ids);
 
    abstract protected List<T1> getLongEntitiesByName(List<String> names);
 
    abstract protected List<T1> saveEntities(List<T1> entities);
 
    abstract protected void deleteEntitiesById(List<Long> ids);
 
    abstract protected void deleteEntitiesByName(List<String> names);
}

18.5.2. La classe [DaoCategorie]

  

La classe [DaoCategorie] è la seguente:


package spring.webjson.client.dao;
 
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
 
import spring.webjson.client.entities.Categorie;
import spring.webjson.client.entities.CoreCategorie;
import spring.webjson.client.entities.CoreProduit;
import spring.webjson.client.entities.Produit;
import spring.webjson.client.infrastructure.DaoException;
 
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
 
@Component
public class DaoCategorie extends AbstractDao<Categorie> {

    @Autowired
    private ApplicationContext context;
    @Autowired
    private IClient client;
 
...
}
  • riga 19: la classe [DaoClient] è un componente Spring in cui possono essere iniettati altri componenti Spring;
  • riga 20: la classe [DaoClient] estende la classe [AbstractDao<Category>] che abbiamo appena visto e quindi implementa l'interfaccia [IDao<Category>];
  • righe 22–23: iniettiamo il contesto Spring per accedere ai suoi bean;
  • righe 24–25: iniettiamo il client HTTP che abbiamo appena creato;

Le implementazioni dei vari metodi dell'interfaccia [DaoCategorie] seguono tutte lo stesso schema. Presenteremo tre metodi, uno basato su un'operazione [GET], gli altri due su un'operazione [POST].

18.5.2.1. Il metodo [getAllLongEntities]

Il metodo [getAllLongEntities] restituisce la versione estesa di tutte le categorie presenti nel database:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            // filters jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // get all categories
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // the List<Categorie> category list
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });
            // redo the product --> category link
            return linkCategorieWithProduits(categories);
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(233, e2, simpleClassName);
        }
}
  • riga 2: il metodo restituisce l'elenco delle categorie nelle loro versioni lunghe;
  • riga 5: il mappatore JSON che serializzerà il valore inviato (non ce n'è uno) e deserializzerà la risposta restituita dalla classe [Client] (categorie nelle loro versioni lunghe);
  • riga 7: chiamiamo il metodo [getResponse] della classe [Client]. Questo metodo gestisce la comunicazione con il servizio web / JSON. I suoi parametri sono i seguenti:
    • l'URL del servizio da interrogare [/getAllLongCategories];
    • il metodo [GET] da utilizzare;
    • il codice di errore da utilizzare in caso di errore (232);
    • il valore inviato. In questo caso, non ce n'è nessuno;
  • Riga 7: Nell'espressione [client.<List<Category>, Void>], specifichiamo i parametri effettivi dei tipi generici [T1, T2] per il metodo [getResponse]. Ricordiamo che [T1] è il tipo della risposta prevista e [T2] è il tipo del valore inviato. Qui, ci aspettiamo un risultato di tipo [List<Category>] e non c'è alcun valore inviato [Void];
  • Riga 7: Il risultato restituito dal metodo [getResponse] viene memorizzato in un oggetto di tipo [Object]. Questo è un po' strano poiché ci aspettiamo un tipo [List<Category>]. Ciò è dovuto al fatto che il metodo [getResponse], che opera con i tipi generici [T1, T2], restituisce sempre un tipo [java.util.LinkedHashMap], che deve poi essere elaborato per restituire il tipo corretto;
  • Riga 9: Restituiamo l'elenco delle categorie. Per farlo, serializziamo l'oggetto [map] [mapper.writeValueAsString(map)] in una stringa JSON, che poi deserializziamo nuovamente in un tipo [List<Category>];
  • riga 13: abbiamo ricevuto un elenco di categorie, alcune delle quali potrebbero avere dei prodotti. Riceviamo la versione breve di questi prodotti. Pertanto, quando vengono deserializzati, gli oggetti [Product] creati hanno il campo [category] impostato su null. Il metodo [linkCategoryWithProducts] ristabilisce il collegamento tra un [Product] e la sua [Category];
  • righe 14–15: intercettiamo la [DaoException] che il metodo [getResponse] potrebbe aver generato e la rigeneriamo immediatamente. Questo comportamento insolito è dovuto al fatto che, se non lo facessimo, la [DaoException] verrebbe intercettata dalle righe 16–18, e non vogliamo che ciò accada;
  • righe 16–18: intercettiamo tutte le altre eccezioni per incapsularle in un tipo [DaoException]. Ricordiamo che il livello [DAO] deve lanciare solo questo tipo di eccezione;

Il metodo [linkCategorieWithProduits], che ristabilisce i collegamenti tra le entità [Product] e le entità [Category], è il seguente:


    private List<Categorie> linkCategorieWithProduits(List<Categorie> categories) {
        for (Categorie categorie : categories) {
            List<Produit> produits = categorie.getProduits();
            if (produits != null) {
                for (Produit produit : produits) {
                    produit.setCategorie(categorie);
                }
            }
        }
        return categories;
}

18.5.2.2. Gestione dei filtri JSON

Rivediamo la gestione dei filtri JSON nel precedente metodo [getAllLongEntities]:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            // filters jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // get all categories
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // the List<Categorie> category list
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });
 
  • Riga 5: recuperiamo un mappatore JSON dal contesto Spring in grado di gestire le versioni lunghe delle categorie. Rivediamo la definizione di questo mappatore nella configurazione Spring [AppConfig]:

// filters jSON
    @Bean
    public ObjectMapper jsonMapper(RestTemplate restTemplate) {
        return ((MappingJackson2HttpMessageConverter) (restTemplate.getMessageConverters().get(0))).getObjectMapper();
    }
 
    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperLongCategorie(RestTemplate restTemplate) {
        ObjectMapper jsonMapper = jsonMapper(restTemplate);
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        return jsonMapper;
}
    @Bean
    public RestTemplate restTemplate(int timeout) {
    ...
    }
 
  • Il bean [jsonMapperLongCategorie] richiesto dal metodo [getAlllongEntities] è il bean nelle righe 7–15;
  • riga 10: il mapper è fornito dal metodo [jsonMapper] nelle righe 2–5. Possiamo vedere che questo mapper JSON appartiene all'oggetto [RestTemplate], che gestisce gli scambi HTTP tra il client e il server. Questo mapper viene utilizzato per impostazione predefinita per:
    • serializzare il valore inviato al server;
    • deserializzare la risposta restituita dal server;

Torniamo al codice di [getAllLongEntities]:


            // filters jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // get all categories
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // the List<Categorie> category list
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });
            // redo the product --> category link
return linkCategorieWithProduits(categories);
  • Riga 2: recuperiamo il mapper [jsonMapperLongCategorie] dal contesto Spring;
  • riga 4: viene eseguito il metodo [getResponse]. Ciò comporta:
    • la serializzazione automatica del valore inviato (qui non ce n'è uno);
    • deserializzazione automatica della risposta ricevuta, in questo caso di tipo List<Category>. Questo perché l'entità [Category] ha un filtro JSON [jsonFilterCategory], che doveva essere gestito. Questo è il motivo della riga 2;
  • riga 6: il risultato subisce una seconda serializzazione/deserializzazione con questo stesso mappatore per recuperare il tipo List<Category>. Riga 4: il tipo restituito da [getResponse] è un tipo [Object];

Nei metodi seguenti, si noti che il mappatore JSON richiesto dal contesto Spring viene utilizzato sia per il valore inviato (serializzazione) che per quello ricevuto (deserializzazione). Se uno o entrambi i valori presentano un filtro JSON, questi devono essere configurati. Il mappatore può quindi avere fino a due filtri configurati. Nel seguito, ciò non si verifica mai. O il valore inviato non ha alcun filtro (List<Long>, List<String>), oppure il valore ricevuto non ne ha (List<CoreCategory>, List<CoreProduct>). Le entità con un filtro JSON sono solo [Category] e [Product].

18.5.2.3. Il metodo [getShortEntitiesById]

Il metodo [getShortEntitiesById] restituisce le versioni abbreviate delle categorie di cui riceve come parametri le chiavi primarie:


    @Override
    protected List<Categorie> getShortEntitiesById(List<Long> ids) {
        try {
            // filters jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // get a category without its products
            Object map = client.<List<Categorie>, List<Long>> getResponse("/getShortCategoriesById", HttpMethod.POST, 204, ids);
            // the category
            return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
            });
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(223, e2, simpleClassName);
        }
}
  • riga 5: il mappatore JSON che serializzerà il valore inviato (un elenco di chiavi primarie) e deserializzerà la risposta restituita dalla classe [Client] (categorie nelle loro versioni abbreviate). Il filtro scelto non avrà alcun effetto sul valore inviato poiché non esiste alcun filtro per gli elementi nell'elenco inviato;
  • riga 7: chiamiamo il metodo [getResponse] della classe padre. Questo metodo gestisce la comunicazione con il servizio web / JSON. I suoi parametri sono i seguenti:
    • l'URL del servizio interrogato [/getShortCategoriesById];
    • il metodo [POST] da utilizzare;
    • il codice di errore da utilizzare in caso di errore (204);
    • il valore inviato. In questo caso, si tratta di un elenco di chiavi primarie;
  • riga 7: nell'espressione [client.<List<Category>, List<Long>>], specifichiamo i parametri effettivi dei tipi generici [T1, T2] per il metodo [getResponse]. Ricordiamo che [T1] è il tipo della risposta prevista e [T2] è il tipo del valore inviato. In questo caso, ci aspettiamo un risultato di tipo [List<Category>] e il valore inviato è un elenco di chiavi primarie di tipo [List<Long>];
  • Riga 7: Il risultato restituito dal metodo [getResponse] viene memorizzato in un tipo [Object];
  • Riga 9: Viene restituito l'elenco delle categorie. A tal fine, l'oggetto [map] [mapper.writeValueAsString(map)] viene serializzato in una stringa JSON, che viene poi deserializzata in un tipo [List<Category>];

18.5.2.4. Il metodo [saveEntities]

Il metodo [saveEntities] salva le categorie nel database. Il suo codice è il seguente:


@Override
    protected List<Categorie> saveEntities(List<Categorie> entities) {
        try {
            // filters jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // add categories
            Object map = client.<List<CoreCategorie>, List<Categorie>> getResponse("/saveCategories", HttpMethod.POST, 200,
                    entities);
            // list of added core categories
            List<CoreCategorie> coreCategories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<CoreCategorie>>() {
                    });
            // categories are updated with the information received
            for (int i = 0; i < entities.size(); i++) {
                Categorie categorie = entities.get(i);
                CoreCategorie coreCategorie = coreCategories.get(i);
                categorie.setId(coreCategorie.getId());
                List<Produit> produits = categorie.getProduits();
                if (produits != null) {
                    List<CoreProduit> coreProduits = coreCategorie.getCoreProduits();
                    for (int j = 0; j < produits.size(); j++) {
                        Produit produit = produits.get(j);
                        produit.setId(coreProduits.get(j).getId());
                        produit.setIdCategorie(categorie.getId());
                        produit.setCategorie(categorie);
                    }
                }
            }
            return entities;
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(220, e2, simpleClassName);
        }
    }
  • riga 2: il metodo [saveEntities] 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 ultimi vengono salvati;
  • riga 5: il mappatore JSON che serializzerà il valore inviato (un elenco di categorie nelle loro versioni lunghe) e deserializzerà la risposta restituita dalla classe [Client] (oggetti [CoreCategory]). Il filtro scelto non avrà alcun effetto sul risultato poiché gli elementi nell'elenco ricevuto come risposta non vengono filtrati;
  • riga 7: chiamiamo il metodo [getResponse] del genitore per gestire la comunicazione con il servizio web / JSON;
    • il primo parametro è l'URL [/saveCategories];
    • il secondo parametro è il metodo HTTP da utilizzare, in questo caso un [POST];
    • il terzo parametro è il codice di errore da utilizzare in caso di errore (200);
    • l'ultimo parametro è il valore inviato, in questo caso l'elenco delle categorie da salvare;
  • riga 7: i parametri generici [T1, T2] del metodo [getResponse] sono qui [List<CoreCategory>, List<Category>]. Il primo tipo è quello della risposta attesa, il secondo è il tipo del valore inviato;
  • riga 7: memorizziamo la risposta ottenuta in un tipo [Object];
  • riga 9: ricostruiamo la risposta di tipo [List<CoreCategory>]. La risposta da restituire è di tipo [List<Category>] (riga 2) e non [List<CoreCategory>]. La risposta ricevuta è l'elenco delle chiavi primarie per le categorie e i prodotti salvati;
  • righe 14–28: le chiavi primarie ricevute vengono assegnate alle categorie e ai prodotti (righe 17, 23, 24). Inoltre, vengono ricostruite le relazioni [Product] → [Category] (righe 24–25);

Tutti gli altri metodi seguono lo stesso schema.

18.6. 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 i test JUnit che sono stati utilizzati per testare il livello [DAO] [4]:

  

Questi tre test vengono eseguiti utilizzando le seguenti configurazioni di test:

 

I risultati dei tre test sono i seguenti:

  • in [1], il test [JUnitTestCheckArguments];
  • in [2], il test [JUnitTestDao];
  • in [3], il test [JUnitTestPushTheLimits] eseguito sul lato client (progetto [spring-webjson-client-generic]);
  • in [3], il test [JUnitTestPushTheLimits] eseguito sul lato server (progetto [spring-jdbc-generic-04]). Si osserva che il livello di rete causa un rallentamento molto limitato rispetto a quello causato dall'accesso al DBMS;

18.7. Implementazione di servizi web / JSON / JPA / Hibernate

Esamineremo ora la seguente architettura:

La modifica è riportata in [1]. Il livello [DAO] del server si basa su un'implementazione JPA. Utilizzeremo inizialmente un'implementazione JPA / Hibernate.

18.7.1. Il progetto Eclipse

Per ora, i progetti caricati in Eclipse sono i seguenti:

  

Il progetto [spring-webjson-server-jdbc-generic] si basava sul progetto [spring-jdbc-generic-04], che configura il livello DAO/JDBC per l'accesso al DBMS MySQL. Creeremo un nuovo progetto [spring-webjson-server-jpa-generic], che si baserà sul progetto [spring-jpa-generic] che configura il livello DAO/JPA/JDBC per l'accesso al DBMS MySQL. Sappiamo che in entrambi i casi il livello [DAO] implementa la stessa interfaccia [IDao]. Il codice per il livello [web] rimane quindi invariato.

Possiamo creare il progetto [spring-webjson-server-jpa-generic] copiando e incollando dal progetto [spring-webjson-server-jdbc-generic]:

  • in [1], specificare una cartella creata appositamente per il nuovo progetto;
  

Ci sono tre tipi di modifiche da apportare. Le prime riguardano il file di configurazione Maven del progetto [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>dvp.spring.database</groupId>
    <artifactId>spring-webjson-server-jpa-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>
 
    <name>spring-webjson-server-jpa-generic</name>
    <description>démo spring mvc</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>
 
    <dependencies>
        <!-- web layer -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- layer [DAO] -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  • riga 5: modificare il nome dell'artefatto Maven;
  • righe 24–28: la dipendenza è ora sul progetto [spring-jpa-generic] e non più su [spring-jdbc-generic-04];

Alla fine, le dipendenze sono le seguenti:

  

Una volta fatto ciò, risolviamo tutti i problemi di importazione emersi nelle varie classi. Ad esempio, le entità [Product, Category] non si trovano più nel progetto [spring-jdbc-generic-04] ma nel progetto [spring-jpa-generic]. È sufficiente premere [Ctrl-Shift-O] nel codice di una classe per rigenerare le importazioni.

L'ultima modifica deve essere apportata nel file di configurazione [AppConfig]:


package spring.webjson.server.config;
 
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
 
@Configuration
@ComponentScan(basePackages = { "spring.webjson.server.service" })
@Import({ spring.data.config.AppConfig.class, WebConfig.class })
public class AppConfig {
 
}
  • Riga 9: Ora importiamo la configurazione dal progetto [spring-jpa-generic] anziché dal progetto [spring-jdbc-generic-04];

Ecco fatto, siamo pronti. Avviamo il servizio web con la configurazione [spring-webjson-server-jpa-generic-hibernate-eclipselink]:

Quindi eseguiamo i tre test per il client generico [spring-webjson-client-generic]:

  • in [1], il test [JUnitTestCheckArguments] (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • in [2], il test [JUnitTestDao] (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestDao]);
  • in [3], il test [JUnitTestPushTheLimits] eseguito sul lato client (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • in [4], il test [JUnitTestPushTheLimits] eseguito sul lato server (configurazione di esecuzione [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);

18.7.2. Perché funziona?

Funziona, eppure, se si osserva attentamente il codice, è sorprendente che funzioni. Sebbene i livelli [DAO] implementati dai progetti [spring-jdbc-generic-04] e [spring-jpa-generic] presentino effettivamente la stessa interfaccia, non manipolano le stesse entità [Category] e [Product]: nel progetto [spring-jpa-generic], queste entità hanno un campo aggiuntivo [EntityType entityType] che può assumere due possibili valori:

  • EntityType.POJO: l'entità è un oggetto normale i cui campi possono essere utilizzati liberamente;
  • EntityType.PROXY: l'entità è un oggetto PROXY reso dal livello [JPA]. In questo caso, alcuni campi (in realtà i getter per questi campi) non si comportano come al solito, e sono state stabilite le seguenti regole:
    • se [Category.entityType == EntityType.PROXY], allora il metodo [getProducts] non deve essere utilizzato;
    • se [Product.entityType == EntityType.PROXY], allora il metodo [getCategory] non deve essere utilizzato;

Tuttavia, abbiamo appena migrato il progetto [spring-webjson-server-jdbc-generic] a [spring-webjson-server-jpa-generic] senza modificare il codice. Com'è possibile?

Esaminiamo il codice del metodo [saveCategories]:


    @RequestMapping(value = "/saveCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreCategorie>> saveCategories(HttpServletRequest request) {
...
            // retrieve the posted value
            String body = CharStreams.toString(request.getReader());
            // we deserialize it
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
            });
            // we persist categories
            categories = daoCategorie.saveEntities(categories);
            ...
}
  • riga 8: viene creato un oggetto List<Category> da una stringa JSON:
    • Nel valore inviato, i prodotti non hanno un campo [category]. In effetti non è necessario inviare questo campo. Se lo inviasimo, la deserializzazione costruirebbe un oggetto [Product] con un campo [category] che punta a un oggetto [Category] appena creato. Per n prodotti, avremmo quindi n oggetti [Category] creati, mentre ne serve solo uno. Inoltre, il campo [category] dei prodotti non punterebbe all'oggetto [Category] corretto, ovvero quello a cui appartengono. Pertanto, in questo caso i prodotti hanno un campo [category==null];
    • Nelle classi [Categoria] e [Prodotto], il campo [EntityType entityType] è definito come segue:

    protected EntityType entityType = EntityType.POJO;

Pertanto, le entità [Category] e [Product] create tramite serializzazione sono tutte di tipo POJO.

  • Riga 11: Persistiamo le categorie. Questo non dovrebbe funzionare. Infatti, mentre nell'implementazione JDBC il campo [Product.category] non è necessario per la persistenza (viene utilizzato invece il campo [categoryId]), nell'implementazione JPA è assolutamente necessario. Questo campo deve puntare a un'entità [Category], ma qui è nullo.

Esaminiamo il codice del metodo [DaoCategorie.saveEntities] nel livello [DAO / JPA]:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // on note les produits qui vont être insérés
        List<Produit> insertedProduits = new ArrayList<Produit>();
        for (Categorie categorie : categories) {
            EntityType categorieType = categorie.getEntityType();
            List<Produit> produits = null;
            if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
                for (Produit produit : produits) {
                    if (produit.getId() == null) {
                        insertedProduits.add(produit);
                    }
                    // on en profite pour rétablir (si besoin est) la relation produit --> categorie
                    produit.setCategorie(categorie);
                }
            }
        }
        // on persiste les catégories / produits
        try {
            categoriesRepository.save(categories);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // on met à jour le champ [idCategorie] des produits insérés
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // résultat
        return categories;
    }
  • Righe 13–14: Possiamo vedere che la relazione [Prodotto] → [Categoria] viene ristabilita per le entità POJO (riga 8), come avviene in questo caso. Questo spiega perché la persistenza delle categorie ha funzionato. Questo approccio è utile in altre situazioni: non si può mai essere sicuri che l'utente abbia correttamente collegato i prodotti alle categorie. Quindi lo facciamo noi per loro;

Ora esaminiamo il metodo [ProductController.saveProducts] che persiste i prodotti:


@RequestMapping(value = "/saveProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreProduit>> saveProduits(HttpServletRequest request) {
    ...
            // retrieve the posted value
            String body = CharStreams.toString(request.getReader());
            // we deserialize it
            ObjectMapper mapper = context.getBean("jsonMapperShortProduit", ObjectMapper.class);
            List<Produit> produits = mapper.readValue(body, new TypeReference<List<Produit>>() {
            });
            // we persist products
            produits = daoProduit.saveEntities(produits);
            List<CoreProduit> coreProduits = new ArrayList<CoreProduit>();
            for (Produit produit : produits) {
                coreProduits.add(new CoreProduit(produit.getId()));
            }
            // we return the answer
            return new Response<List<CoreProduit>>(0, null, coreProduits);
...
    }
  • Riga 8: un oggetto List<Product> viene ricostruito dal valore inviato. Per i motivi spiegati in precedenza, ogni oggetto [Product] avrà un campo:
    • [EntityType entityType] uguale a [EntityType.POJO];
    • [Category category] uguale a null;
  • riga 11: la persistenza dei prodotti dovrebbe fallire. Infatti, con JPA, la persistenza di un prodotto è possibile solo se il suo campo [category] punta a un'entità [Category];

Diamo un'occhiata al codice del metodo [DaoProduit.saveEntities] nel livello [DAO / JPA]:


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        // on rétablit (si besoin est) le lien entre un produits et sa catégorie
        for (Produit produit : entities) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // on persiste les produits
        try {
            return Lists.newArrayList(produitsRepository.save(entities));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
}
  • Righe 3–8: Per ogni [Product] di tipo POJO, viene creato un collegamento a un oggetto [Category] con la chiave primaria corretta e una versione non nulla. Ciò è sufficiente affinché il livello JPA persista correttamente il prodotto;

Esaminiamo un ultimo punto. Gli oggetti [Category] e [Product] hanno un campo aggiuntivo [EntityType entityType] che verrà serializzato in JSON quando questi oggetti vengono inviati al client. Possiamo verificarlo con [Advanced Rest Client]:

Sul lato client, le entità [Category] e [Product] sono state definite senza il campo [EntityType entityType]. Ciò è normale poiché gli oggetti [Category] e [Product] vengono serializzati senza le loro parti PROXY [Category.products], [Product.category]. Sul lato client, quindi, non esiste il concetto di entità PROXY. Esistono solo oggetti normali.

Sul lato client, la stringa JSON [1] viene ricevuta dal seguente metodo [DaoCategorie.getAllShortEntities]:


    @Override
    public List<Categorie> getAllShortEntities() {
...
            // filters jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // get all categories
            Object map = client.<List<Categorie>, Void> getResponse("/getAllShortCategories", HttpMethod.GET, 202, null);
            // the List<Categorie> category list
            return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
            });
...
}
  • riga 5: configuriamo il mapper JSON dell'oggetto [RestTemplate] per gestire i filtri JSON [jsonFilterCategorie] dell'oggetto [Category] e il filtro [jsonFilterProduct] dell'oggetto [Product];
  • riga 7: il valore inviato (qui non ce n'è uno) e il valore ricevuto (List<Category>) vengono serializzati/deserializzati utilizzando questo mappatore. Si noti che la presenza del campo [entityType] nella stringa JSON ricevuta, anche se questo campo non esiste nelle entità [Category] e [Product] sul lato client, non causa un errore. Viene ignorato. Se avesse causato un errore, avremmo modificato i filtri sul lato client per ignorarlo.

Per implementare il servizio web / JSON / JPA / EclipseLink, è sufficiente modificare l'implementazione JPA:

  

Nota: premere Alt-F5, quindi rigenerare tutti i progetti Maven.

Avvieremo il servizio web utilizzando la configurazione di runtime [spring-webjson-server-jpa-generic-hibernate-eclipselink] già utilizzata per Hibernate. Una volta fatto ciò, esegui i tre test per il client generico [spring-webjson-client-generic]:

  • in [1], il test [JUnitTestCheckArguments];
  • in [2], il test [JUnitTestDao];
  • in [3], il test [JUnitTestPushTheLimits] eseguito sul lato client (progetto [spring-webjson-client-generic]);
  • in [4], il test [JUnitTestPushTheLimits] eseguito sul lato server (configurazione di esecuzione [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);

18.9. Implementazione del servizio web / JSON / JPA / OpenJpa

Per implementare il servizio web / JSON / JPA / OpenJPA, è sufficiente modificare l'implementazione JPA:

  

Nota: premere Alt-F5, quindi rigenerare tutti i progetti Maven.

Avvieremo il servizio web utilizzando la configurazione di runtime [spring-webjson-server-jpa-generic-openpa]:

Una volta fatto ciò, eseguire i tre test per il client generico [spring-webjson-client-generic]:

  • in [1], il test [JUnitTestCheckArguments] (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • in [2], il test [JUnitTestDao] (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestDao]);
  • in [3], il test [JUnitTestPushTheLimits] eseguito sul lato client (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • in [4], il test [JUnitTestPushTheLimits] eseguito sul lato server (configurazione di esecuzione [spring-jpa-generic-JUnitTestPushTheLimits-openpa]);

Per far funzionare i test, è stato necessario apportare delle modifiche al livello DAO/JPA. Infatti, per qualche motivo inspiegabile, i metodi [DaoCategorie.saveEntities] e [DaoProduit.saveEntities] fallivano durante il popolamento del database, indicando che le entità distaccate non potevano essere persistite. Un'entità distaccata è un'entità che presenta:

  • una chiave primaria non nulla;
  • una versione non nulla;

Nessuno di questi casi è stato verificato. Non sapendo dove cercare, ho duplicato le entità da salvare in un elenco nuovo di zecca, e a quel punto i test hanno funzionato. Questa modifica avrebbe potuto essere apportata:

  • nel livello [DAO / JPA];
  • nel livello [web] che crea le entità da salvare;

Ho scelto di farlo nel livello [DAO / JPA]. Ovviamente c'è una perdita di prestazioni, ma è del tutto trascurabile rispetto ai tempi di risposta del DBMS. Le modifiche sono le seguenti:

Nella classe [DaoCategorie] del progetto [spring-jpa-generic]:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // ***************************************************************************************
        // on clone la liste des catégories -- nécessaire parfois pour OpenJpa -- bug non compris
        // ***************************************************************************************
        List<Categorie> categories2 = new ArrayList<Categorie>();
        for (Categorie categorie : categories) {
            // catégorie
            Categorie categorie2 = new Categorie(categorie.getId(), categorie.getVersion(), categorie.getNom(), null);
            EntityType categorieType = categorie.getEntityType();
            categorie2.setEntityType(categorieType);
            categories2.add(categorie2);
            // produits
            List<Produit> produits = null;
            if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
                List<Produit> produits2 = new ArrayList<Produit>();
                for (Produit produit : produits) {
                    Produit produit2 = new Produit(produit.getId(), produit.getVersion(), produit.getNom(),
                            produit.getIdCategorie(), produit.getPrix(), produit.getDescription(), produit.getCategorie());
                    produit2.setEntityType(produit.getEntityType());
                    produits2.add(produit2);
                }
                categorie2.setProduits(produits2);
            }
        }
        // on note les produits qui vont être insérés
        List<Produit> insertedProduits = new ArrayList<Produit>();
        for (Categorie categorie : categories2) {
            EntityType categorieType = categorie.getEntityType();
            List<Produit> produits = null;
            if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
                for (Produit produit : produits) {
                    if (produit.getId() == null) {
                        insertedProduits.add(produit);
                    }
                    // on en profite pour rétablir (si besoin est) la relation produit --> categorie
                    produit.setCategorie(categorie);
                }
            }
        }
        // on persiste les catégories / produits
        try {
            categoriesRepository.save(categories2);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // on met à jour le champ [idCategorie] des produits insérés
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // résultat
        return categories2;
    }
  • righe 3–25: l'elenco [categories] ricevuto come parametro (riga 2) viene duplicato nell'elenco [categories2] (riga 6). È questo elenco che viene salvato e restituito al chiamante (riga 52). Ciò ha una conseguenza importante: viene restituito un elenco diverso da quello passato come parametro, quindi dove prima potevamo scrivere:
List<Categorie> categories=...
daoCategorie.saveEntities(categories)
// exploitation de [categories]

Ora dobbiamo scrivere:


List<Categorie> categories=...
categories=daoCategorie.saveEntities(categories)
// exploitation de [categories]

Nella classe [DaoProduct] del progetto [spring-jpa-generic], il metodo [saveEntities] viene modificato in modo analogo:


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        // ***************************************************************************************
        // on clone la liste des produits -- nécessaire parfois pour OpenJpa -- bug non compris
        // ***************************************************************************************
        List<Produit> produits2 = new ArrayList<Produit>();
        for (Produit produit : entities) {
            Produit produit2 = new Produit(produit.getId(), produit.getVersion(), produit.getNom(), produit.getIdCategorie(),
                    produit.getPrix(), produit.getDescription(), produit.getCategorie());
            produit2.setEntityType(produit.getEntityType());
            produits2.add(produit2);
        }
 
        // on rétablit (si besoin est) le lien entre un produits et sa catégorie
        for (Produit produit : produits2) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // on persiste les produits
        try {
            return Lists.newArrayList(produitsRepository.save(produits2));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
}

Per implementare il servizio web / JSON / JPA / EclipseLink / PostgreSQL, è necessario installare:

  • il progetto [postgresql-config-jdbc] per configurare il livello JDBC di PostgreSQL;
  • il progetto [postgresql-config-jpa-eclipselink] per configurare il livello JPA di PostgreSQL;
  • Premere Alt-F5 e rigenerare tutti i progetti Maven;
  

Avvia il DBMS PostgreSQL e avvia il servizio web utilizzando la configurazione di runtime [spring-webjson-server-jpa-generic-hibernate-eclipselink] utilizzata in precedenza. Una volta fatto ciò, esegui i tre test per il client generico [spring-webjson-client-generic]:

  • in [1], il test [JUnitTestCheckArguments] (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • in [2], il test [JUnitTestDao] (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestDao]);
  • in [3], il test [JUnitTestPushTheLimits] eseguito sul lato client (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • in [4], il test [JUnitTestPushTheLimits] eseguito sul lato server (configurazione di esecuzione [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);