Skip to content

15. [TD]: Erstellen eines Clients für den Webdienst

Stichworte: mehrschichtige Architektur, Spring, Dependency Injection, Webservice / JSON, Client / Server

15.1. Support

  

Die Projekte zu diesem Kapitel finden Sie im Ordner [support / chap-15].

15.2. Die Client-Server-Architektur

Wir möchten die folgende Client-Server-Architektur erstellen:

Die [ui]-Schicht wird diejenige sein, die bereits in den Abschnitten 9 und 10 entwickelt wurde. Dies wird möglich sein, da die darüber liegende [business]-Schicht dieselbe Schnittstelle [IElectionsMetier] implementieren wird wie die [business]-Schicht in Abschnitt 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);
 
}

In Abschnitt 7 tauschte die [DAO]-Schicht Daten mit einem DBMS aus. Hier tauscht die [DAO]-Schicht Daten mit einem Webserver / JSON aus.

Zunächst konzentrieren wir uns auf die folgende Architektur:

15.3. Das Eclipse-Projekt

Das Eclipse-Projekt sieht wie folgt aus:

Diese Struktur entspricht der des Beispielprojekts in Abschnitt 13.6.1. Wir werden denselben Ansatz verfolgen.

15.4. Maven-Konfiguration

Dies ist die in Abschnitt 13.6.2 beschriebene Konfiguration.

15.5. Implementierung der [DAO]-Schicht

  
  • Das Paket [elections.client.config] enthält die Spring-Konfiguration für die [DAO]-Schicht;
  • Das Paket [elections.client.dao] enthält die Implementierung der [DAO]-Schicht;
  • Das Paket [elections.client.entities] enthält die Objekte, die mit dem Webservice / JSON ausgetauscht werden;
  • Das Paket [elections.client.business] enthält die [Business]-Schicht
  • Das Paket [elections.client.ui] enthält die [UI]-Schicht

15.5.1. Konfiguration der [business]-Schicht

  

Die Klasse [MetierConfig] übernimmt die Spring-Konfiguration der [business]-Schicht. Ihr Code lautet wie folgt:


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();
    }
}

Dieser Code wurde in Abschnitt 13.6.3.1 erläutert. Er ist einfacher, da hier keine JSON-Filter verwaltet werden müssen.

15.5.2. Die Entitäten

  

Die Entitäten, die von den Schichten [DAO] und [business] verarbeitet werden, sind diejenigen, die sie mit dem Webservice / JSON austauschen. Dabei handelt es sich um Objekte vom Typ [ElectionsConfig] und [VoterList]. Auf der Serverseite waren diese Entitäten mit JPA-Persistenz-Annotationen versehen. Hier wurden diese Annotationen entfernt. Wir fügen den Entitätscode zur Veranschaulichung noch einmal ein:

[Abstrakte Entität]


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

[VoterList]


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. Die Schnittstelle der [DAO]-Schicht

  

Die [DAO]-Schicht verfügt über die folgende [IClientDao]-Schnittstelle:


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

Die Schnittstelle verfügt nur über eine Methode [getResponse]:

  • Der erste Parameter ist die URL des abzufragenden Servers;
  • der zweite Parameter ist der zu sendende JSON-Wert, null, wenn nichts zu senden ist;
  • das Ergebnis ist die JSON-Zeichenkette eines [Response<T>]-Objekts, wobei die [Response]-Klasse in Abschnitt 14.7 beschrieben wurde;

15.5.4. Implementierung der Kommunikation mit dem Webdienst / JSON

  

Die Klasse [ClientDao] implementiert die Schnittstelle [IClientDao] wie folgt:


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);
        }
    }
}

Dieser Code wird in Abschnitt 13.6.3.6 beschrieben.

15.6. Implementierung der [Business]-Schicht

 

Wie bereits erwähnt, verfügt die [Business]-Schicht über dieselbe Schnittstelle [IElectionsMetier] wie in Abschnitt 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);
 
}

Diese Schnittstelle wird von der folgenden Klasse [ElectionsMetier] implementiert:


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;
    }
}
 

Der in Zeile 37 verwendete Typ [Response] ist die in Abschnitt 14.7 beschriebene Webserver-/JSON-Antwort;


Aufgabe: Vervollständigen Sie gemäß Abschnitt 13.6.3.7 die Klasse [ElectionsMetier];


15.7. Der JUnit-Test

Kehren wir zu der derzeit im Aufbau befindlichen Client/Server-Architektur zurück:

Die [JUnit]-Schicht [1] kommuniziert über die Schichten [2–4] mit der [Business]-Schicht [5] des Servers. Indem wir sicherstellen, dass die [Business]-Schichten [2] und [5] dieselbe Schnittstelle haben, machen wir die Schichten [2–4] transparent. Es sieht so aus, als würde die Schicht [1] direkt mit der Schicht [5] kommunizieren. Das Interessante daran ist, dass wir in [1] den JUnit-Test verwenden können, der zum Testen der [Business]-Schicht [5] verwendet wurde.

  

Aufgabe: Führen Sie den JUnit-Test des Projekts aus, um Ihre Implementierung sowohl des Servers als auch des Clients zu überprüfen.


15.8. Implementierung der [UI]-Schicht

Kehren wir zu der Architektur zurück, die wir erstellen möchten:

Nachdem die [Business]-Schicht [2] nun erstellt und getestet wurde, können wir die [UI]-Schicht [1] erstellen.

Da die [IElectionsMetier]-Schnittstelle der [Business]-Schicht mit der des in Absatz 8 beschriebenen Projekts identisch ist, können wir [3] das [UI]-Schicht-Projekt aus Absatz 10 kopieren. Bei diesem Projekt handelte es sich um ein NetBeans-Projekt. Kopieren Sie einfach die relevanten Java-Klassen aus NetBeans und fügen Sie sie in Eclipse ein. Anschließend müssen einige Anpassungen an den Paketen und Importen vorgenommen werden.

Das Gleiche werden wir für die ausführbaren Klassen im Paket [elections.client.boot] tun [4].

Die Klasse [AbstractBootElections] sieht wie folgt aus:


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();
 
...
  • Zeile 19: Der durch die Konfigurationsklasse [UiConfig] definierte Spring-Kontext wird instanziiert. Diese Klasse sieht wie folgt aus:
  

Die Klasse [UiConfig] sieht wie folgt aus:


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 {
}
  • Zeile 6: Importiert die Beans aus der [business]-Schicht;
  • Zeile 7: Wir geben an, dass sich Spring-Beans im Paket [elections.client.ui] befinden;

Aufgabe: Überprüfen Sie, ob die Konsolen- und Swing-Versionen der [ui]-Schicht funktionieren.