15. [TD]: creazione di un client per il servizio web
Parole chiave: architettura multistrato, Spring, iniezione di dipendenze, servizio web / jSON, client / server
15.1. Support
![]() |
I progetti di questo capitolo si trovano nella cartella [support / chap-15].
15.2. L'architettura client/server
Vogliamo creare la seguente architettura client/server:
![]() |
Il livello [ui] sarà quello già sviluppato nei paragrafi 9 e 10. Ciò sarà possibile poiché il livello [métier] sopra indicato implementerà la stessa interfaccia [IElectionsMetier] del livello [métier] del paragrafo 8:
package elections.client.metier;
import elections.client.entities.ListeElectorale;
public interface IElectionsMetier {
// si ottengono gli elenchi in competizione
public ListeElectorale[] getListesElectorales();
// il numero di seggi da assegnare
public int getNbSiegesAPourvoir();
// la soglia elettorale
public double getSeuilElectoral();
// registrazione dei risultati
public void recordResultats(ListeElectorale[] listesElectorales);
// il calcolo dei seggi
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
}
Nel paragrafo 7, il livello [DAO] scambiava dati con un SGBD. Qui il livello [DAO] scambia dati con un server web / jSON.
In una prima fase, ci concentreremo sulla seguente architettura:
![]() |
15.3. Il progetto Eclipse
Il progetto Eclipse è il seguente:
![]() | ![]() | ![]() |
Questa struttura riprende quella del progetto di esempio del paragrafo 13.6.1. Seguiremo lo stesso approccio.
15.4. Configurazione Maven
È quella descritta nel paragrafo 13.6.2.
15.5. Implementazione del livello [DAO]
![]() |
![]() |
- il pacchetto [elections.client.config] contiene la configurazione Spring del livello [DAO];
- il pacchetto [elections .client.dao] contiene l'implementazione del livello [DAO];
- il pacchetto [elections .client.entities] contiene gli oggetti scambiati con il servizio web / jSON;
- il pacchetto [elections .client.metier] contiene il livello [métier]
- il pacchetto [elections .client.ui] contiene il livello [UI]
15.5.1. Configurazione del livello [métier]
![]() |
La classe [MetierConfig] gestisce la configurazione Spring del livello [métier]. Il suo codice è il seguente:
package elections.client.config;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Scope;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
@ComponentScan({ "elections.client.dao","elections.client.metier" })
public class MetierConfig {
// costanti
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8080";
@Bean
public RestTemplate restTemplate(int timeout) {
// creazione del componente RestTemplate
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// timeout degli scambi
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// risultato
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
// mappatore jSON
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
Questo codice è stato spiegato nel paragrafo 13.6.3.1. È più semplice perché qui non ci sono filtri jSON da gestire.
15.5.2. Le entità
![]() |
Le entità gestite dai livelli [DAO] e [métier] sono quelle che scambiano con il servizio web / jSON. Si tratta degli oggetti di tipo [ElectionsConfig] e [ListeElectorale]. Sul lato server, queste entità presentavano annotazioni di persistenza JPA. In questo caso, tali annotazioni sono state rimosse. Riportiamo il codice delle entità a titolo di promemoria:
[AbstractEntity]
package spring.webjson.client.entities;
public abstract class AbstractEntity {
// proprietà
protected Long id;
protected Long version;
// costruttori
public AbstractEntity() {
}
public AbstractEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
// ridefinizione di [equals] e [hashcode]
@Override
public int hashCode() {
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return id != null && this.id == other.id.longValue();
}
// getter e setter
...
}
[ElectionsConfig]
package elections.webjson.client.entities;
public class ElectionsConfig extends AbstractEntity {
// campi
private int nbSiegesAPourvoir;
private double seuilElectoral;
// costruttori
public ElectionsConfig() {
}
public ElectionsConfig(int nbSiegesAPourvoir, double seuilElectoral) {
this.nbSiegesAPourvoir = nbSiegesAPourvoir;
this.seuilElectoral = seuilElectoral;
}
// getter e setter
...
}
[ListeElectorale]
package elections.webjson.client.entities;
public class ListeElectorale extends AbstractEntity {
// campi
private String nom;
private int voix;
private int sieges;
private boolean elimine;
// costruttori
public ListeElectorale() {
}
public ListeElectorale(String nom, int voix, int sieges, boolean elimine) {
setNom(nom);
setVoix(voix);
setSieges(sieges);
setElimine(elimine);
}
// getter e setter
...
}
15.5.3. L'interfaccia del livello [DAO]
![]() |
Il livello [DAO] presenta la seguente interfaccia [IClientDao]:
package elections.client.dao;
public interface IClientDao {
// richiesta generica
String getResponse(String url, String jsonPost);
}
L'interfaccia presenta un unico metodo [getResponse]:
- il primo parametro è l'URL del server da interrogare;
- il secondo parametro è il valore jSON del dato da inviare, null se non c'è nulla da inviare;
- il risultato è la stringa jSON di un oggetto [Response<T>], la cui classe [Response] è stata descritta al paragrafo 14.7;
15.5.4. Implementazione degli scambi con il servizio web / jSON
![]() |
La classe [ClientDao] implementa l'interfaccia [IClientDao] nel modo seguente:
package elections.client.dao;
import java.net.URI;
import java.net.URISyntaxException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import elections.client.entities.ElectionsException;
@Component
public class ClientDao implements IClientDao {
// dati
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// richiesta generica
@Override
public String getResponse(String url, String jsonPost) {
try {
// URL: URL da contattare
// jsonPost: il valore jSON da inviare
// esecuzione della richiesta
RequestEntity<?> request;
if (jsonPost != null) {
// richiesta POST
request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
} else {
// richiesta GET
request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
.accept(MediaType.APPLICATION_JSON).build();
}
// si esegue la query
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e1) {
throw new ElectionsException(200, e1);
} catch (RuntimeException e2) {
throw new ElectionsException(201, e2);
}
}
}
Questo codice è stato descritto nel paragrafo 13.6.3.6.
15.6. Implementazione del livello [métier]
![]() |
Come già detto, il livello [métier] presenta la stessa interfaccia [IElectionsMetier] descritta nel paragrafo 8.4:
package elections.client.metier;
import elections.client.entities.ListeElectorale;
public interface IElectionsMetier {
// per ottenere gli elenchi dei candidati
public ListeElectorale[] getListesElectorales();
// il numero di seggi da assegnare
public int getNbSiegesAPourvoir();
// la soglia elettorale
public double getSeuilElectoral();
// registrazione dei risultati
public void recordResultats(ListeElectorale[] listesElectorales);
// il calcolo dei seggi
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
}
Tale interfaccia è implementata dalla seguente classe [ElectionsMetier]:
package elections.client.metier;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import elections.client.dao.IClientDao;
import elections.client.entities.ElectionsConfig;
import elections.client.entities.ElectionsException;
import elections.client.entities.ListeElectorale;
@Component
public class ElectionsMetier implements IElectionsMetier {
@Autowired
private IClientDao dao;
@Autowired
private ApplicationContext context;
// configurazione delle elezioni
private ElectionsConfig electionsConfig;
@PostConstruct
public void init() {
// mappatori jSON
ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
try {
// richiesta
Response<ElectionsConfig> response = mapperResponse.readValue(dao.getResponse("/getElectionsConfig", null),
new TypeReference<Response<ElectionsConfig>>() {
});
// errore?
if (response.getStatus() != 0) {
// viene generata 1 eccezione
throw new ElectionsException(response.getStatus(), response.getMessages());
} else {
electionsConfig = response.getBody();
}
} catch (ElectionsException e1) {
throw e1;
} catch (Exception e2) {
throw new ElectionsException(100, getMessagesForException(e2));
}
}
@Override
public ListeElectorale[] getListesElectorales() {
...
}
@Override
public int getNbSiegesAPourvoir() {
return electionsConfig.getNbSiegesAPourvoir();
}
@Override
public double getSeuilElectoral() {
return electionsConfig.getSeuilElectoral();
}
@Override
public void recordResultats(ListeElectorale[] listesElectorales) {
...
}
@Override
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales) {
...
}
// elenco dei messaggi di errore di un'eccezione
private List<String> getMessagesForException(Exception exception) {
// si recupera l'elenco dei messaggi di errore dell'eccezione
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
// si recupera il messaggio solo se !=null e non è vuoto
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// causa successiva
cause = cause.getCause();
}
return erreurs;
}
}
Il tipo [Response] utilizzato alla riga 37 è la risposta del server web / jSON descritta nel paragrafo 14.7;
Compito da svolgere: seguendo il paragrafo 13.6.3.7, completate la classe [ElectionsMetier];
15.7. Il test Junit
Torniamo all’architettura client/server che stiamo realizzando:
![]() |
Il livello [JUnit] [1] comunica con il livello [Métier] del server [5] attraverso i livelli [2-4]. Assicurando che i livelli [Métier], [2] e [5] abbiano la stessa interfaccia, si rendono trasparenti i livelli [2-4]. Il livello [1] sembra comunicare direttamente con il livello [5]. Il punto interessante è che in [1] potremo utilizzare il test JUnit che era stato utilizzato per testare i livelli [Métier] e [5].
![]() |
Compito da svolgere: eseguire il test JUnit del progetto per verificare l’implementazione sia del server che del client.
15.8. Implementazione del livello [UI]
Torniamo all’architettura che vogliamo realizzare:
![]() |
Ora che il livello [métier] [2] è stato realizzato e testato, possiamo realizzare il livello [ui] [1].
![]() |
Poiché l’interfaccia [IElectionsMetier] del livello [métier] è identica a quella del progetto descritto al paragrafo 8, in [3] possiamo copiare il progetto del livello [ui] del paragrafo 10. Si trattava di un progetto NetBeans. È sufficiente copiare e incollare le classi Java interessate da NetBeans a Eclipse. Una volta fatto ciò, occorre apportare alcune modifiche ai pacchetti e alle importazioni.
Si procederà allo stesso modo per le classi eseguibili dei pacchetti [elections.client.boot] e [4].
La classe [AbstractBootElections] è la seguente:
package elections.client.boot;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import elections.client.config.UiConfig;
import elections.client.entities.ElectionsException;
import elections.client.ui.IElectionsUI;
public abstract class AbstractBootElections {
// recupero del contesto Spring
protected AnnotationConfigApplicationContext ctx;
public void run() {
// istanziazione del livello [ui]
IElectionsUI electionsUI = null;
try {
// recupero del contesto Spring
ctx = new AnnotationConfigApplicationContext(UiConfig.class);
// recupero del livello [ui]
electionsUI = getUI();
...
- riga 19: viene istanziato il contesto Spring definito dalla classe di configurazione [UiConfig]. Questa classe è la seguente:
![]() |
La classe [UiConfig] è la seguente:
package elections.client.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
@Import(MetierConfig.class)
@ComponentScan(basePackages = { "elections.client.ui" })
public class UiConfig {
}
- riga 6: si importano i bean del livello [métier];
- riga 7: si indica che nel pacchetto [elections.client.ui] sono presenti dei bean Spring;
Compito da svolgere: verificare che le versioni console e Swing del livello [ui] funzionino.















