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. Assistenza

  

I progetti relativi a 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 nelle sezioni 9 e 10. Ciò sarà possibile perché il livello [business] sovrastante implementerà la stessa interfaccia [IElectionsMetier] del livello [business] nella sezione 8:


package elections.client.metier;
 
import elections.client.entities.ListeElectorale;

public interface IElectionsMetier {
 
    // get the lists in competition
    public ListeElectorale[] getListesElectorales();
 
    // the number of seats to be filled
    public int getNbSiegesAPourvoir();
 
    // the electoral threshold
    public double getSeuilElectoral();
 
    // recording results
    public void recordResultats(ListeElectorale[] listesElectorales);
 
    // calculating seats
    public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
 
}

Nella Sezione 7, il livello [DAO] scambiava dati con un DBMS. In questo caso, il livello [DAO] scambia dati con un server web / JSON.

Inizialmente, ci concentreremo sulla seguente architettura:

15.3. Il progetto Eclipse

Il progetto Eclipse è il seguente:

Questa struttura rispecchia quella del progetto di esempio nella Sezione 13.6.1. Seguiremo lo stesso approccio.

15.4. Configurazione di Maven

È quella descritta nella Sezione 13.6.2.

15.5. Implementazione del livello [DAO]

  
  • Il pacchetto [elections.client.config] contiene la configurazione Spring per il 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.business] contiene il livello [business]
  • Il pacchetto [elections.client.ui] contiene il livello [UI]

15.5.1. Configurazione del livello [business]

  

La classe [MetierConfig] gestisce la configurazione Spring del livello [business]. 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 {
 
    // constants
    static private final int TIMEOUT = 1000;
    static private final String URL_WEBJSON = "http://localhost:8080";
 
    @Bean
    public RestTemplate restTemplate(int timeout) {
        // creation of the RestTemplate component
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // exchange timeout
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // result
        return restTemplate;
    }
 
    @Bean
    public int timeout() {
        return TIMEOUT;
    }
 
    @Bean
    public String urlWebJson() {
        return URL_WEBJSON;
    }
 
    // mapper jSON
    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }
}

Questo codice è stato spiegato nella Sezione 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 [business] sono quelle che scambiano con il servizio web / JSON. Si tratta di oggetti di tipo [ElectionsConfig] e [VoterList]. Sul lato server, queste entità presentavano annotazioni di persistenza JPA. Qui, tali annotazioni sono state rimosse. Riportiamo nuovamente il codice delle entità a titolo di riferimento:

[Entità astratta]


package spring.webjson.client.entities;
 
 
public abstract class AbstractEntity {
    // properties
    protected Long id;
    protected Long version;
 
    // manufacturers
    public AbstractEntity() {
 
    }
 
    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }
 
    // redefine [equals] and [hashcode]
    @Override
    public int hashCode() {
        return (id != null ? id.hashCode() : 0);
    }
 
    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return id != null && this.id == other.id.longValue();
    }
 
    // getters and setters
    ...
}

[ElectionsConfig]


package elections.webjson.client.entities;
 
 
public class ElectionsConfig extends AbstractEntity {
 
    // fields
    private int nbSiegesAPourvoir;
    private double seuilElectoral;
 
    // manufacturers
    public ElectionsConfig() {
 
    }
 
    public ElectionsConfig(int nbSiegesAPourvoir, double seuilElectoral) {
        this.nbSiegesAPourvoir = nbSiegesAPourvoir;
        this.seuilElectoral = seuilElectoral;
    }
 
    // getters and setters
    ...
}

[Elenco degli elettori]


package elections.webjson.client.entities;
 
 
public class ListeElectorale extends AbstractEntity {
 
    // fields
    private String nom;
    private int voix;
    private int sieges;
    private boolean elimine;
 
    // manufacturers
    public ListeElectorale() {
    }
 
    public ListeElectorale(String nom, int voix, int sieges, boolean elimine) {
        setNom(nom);
        setVoix(voix);
        setSieges(sieges);
        setElimine(elimine);
    }
 
    // getters and setters
    ...
}

15.5.3. L'interfaccia del livello [DAO]

  

Il livello [DAO] presenta la seguente interfaccia [IClientDao]:


package elections.client.dao;
 
public interface IClientDao {
 
    // generic request
    String getResponse(String url, String jsonPost);
 
}

L'interfaccia ha un solo metodo [getResponse]:

  • il primo parametro è l'URL del server da interrogare;
  • il secondo parametro è il valore JSON da inviare, null se non c'è nulla da inviare;
  • il risultato è la stringa JSON di un oggetto [Response<T>], dove la classe [Response] è stata descritta nella Sezione 14.7;

15.5.4. Implementazione della comunicazione con il servizio web / JSON

  

La classe [ClientDao] implementa l'interfaccia [IClientDao] come segue:


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 {
 
    // data
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;
 
    // generic request
    @Override
    public String getResponse(String url, String jsonPost) {
 
        try {
            // url : URL to contact
            // jsonPost: the jSON value to be posted
 
            // request execution
            RequestEntity<?> request;
            if (jsonPost != null) {
                // query POST
                request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
            } else {
                // query GET
                request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON).build();
            }
            // execute the query
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e1) {
            throw new ElectionsException(200, e1);
        } catch (RuntimeException e2) {
            throw new ElectionsException(201, e2);
        }
    }
}

Questo codice è descritto nella sezione 13.6.3.6.

15.6. Implementazione del livello [business]

 

Come accennato, il livello [business] presenta la stessa interfaccia [IElectionsMetier] descritta nella sezione 8.4:


package elections.client.metier;
 
import elections.client.entities.ListeElectorale;
 
public interface IElectionsMetier {
 
    // get the lists in competition
    public ListeElectorale[] getListesElectorales();
 
    // the number of seats to be filled
    public int getNbSiegesAPourvoir();
 
    // the electoral threshold
    public double getSeuilElectoral();
 
    // recording results
    public void recordResultats(ListeElectorale[] listesElectorales);
 
    // calculating seats
    public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
 
}

Questa 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;
 
    // election configuration
    private ElectionsConfig electionsConfig;
 
    @PostConstruct
    public void init() {
        // mappers jSON
        ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
        try {
            // request
            Response<ElectionsConfig> response = mapperResponse.readValue(dao.getResponse("/getElectionsConfig", null),
                    new TypeReference<Response<ElectionsConfig>>() {
                    });
            // mistake?
            if (response.getStatus() != 0) {
                // 1 exception is thrown
                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) {
...
    }
 
    // list of exception error messages
    private List<String> getMessagesForException(Exception exception) {
        // retrieve the list of exception error messages
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            // the message is retrieved only if it is !=null and not blank
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // next cause
            cause = cause.getCause();
        }
        return erreurs;
    }
}
 

Il tipo [Response] utilizzato alla riga 37 è la risposta del server web/JSON descritta nella sezione 14.7;


Compito: seguendo la Sezione 13.6.3.7, completare la classe [ElectionsMetier];


15.7. Il test JUnit

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

Il livello [JUnit] [1] comunica con il livello [Business] [5] del server attraverso i livelli [2–4]. Assicurando che i livelli [Business] [2] e [5] abbiano la stessa interfaccia, rendiamo 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 è stato utilizzato per testare il livello [Business] [5].

  

Compito: Esegui 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 [Business] [2] è stato realizzato e testato, possiamo realizzare il livello [UI] [1].

Poiché l'interfaccia [IElectionsMetier] del livello [Business] è identica a quella del progetto descritto al paragrafo 8, possiamo [3] copiare il progetto del livello [UI] dal paragrafo 10. Si trattava di un progetto NetBeans. È sufficiente copiare e incollare le classi Java pertinenti da NetBeans a Eclipse. Una volta fatto ciò, occorre apportare alcune modifiche ai pacchetti e alle importazioni.

Faremo lo stesso per le classi eseguibili nel pacchetto [elections.client.boot] [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 {
 
    // spring context retrieval
    protected AnnotationConfigApplicationContext ctx;
 
    public void run() {
        // instantiation layer [ui]
        IElectionsUI electionsUI = null;
        try {
            // spring context retrieval
            ctx = new AnnotationConfigApplicationContext(UiConfig.class);
            // ui] layer recovery
            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: Importa i bean dal livello [business];
  • Riga 7: Indichiamo che nel pacchetto [elections.client.ui] sono presenti bean Spring;

Compito: Verificare che le versioni console e Swing del livello [ui] funzionino.