Skip to content

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.