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.
18.8. Implementazione di servizi web / JSON / JPA / EclipseLink
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);
}
}
18.10. Implementazione del servizio web / JSON / JPA / EclipseLink / PostgreSQL
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]);







































