Skip to content

15. [TD]: Erstellung eines Clients für den Webservice

Stichworte: mehrschichtige Architektur, Spring, Abhängigkeitsinjektion, Webdienst / jSON, Client / Server

15.1. Support

  

Die Projekte dieses Kapitels befinden sich im Ordner [support / chap-15].

15.2. Die Client-Server-Architektur

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

Die Schicht [ui] entspricht der bereits in den Abschnitten 9 und 10 entwickelten Schicht. Dies ist möglich, da die oben genannte Schicht [métier] dieselbe Schnittstelle [IElectionsMetier] implementiert wie die Schicht [métier] aus Abschnitt 8:


package elections.client.metier;

import elections.client.entities.ListeElectorale;

public interface IElectionsMetier {

    // die Liste der konkurrierenden Einträge abrufen
    public ListeElectorale[] getListesElectorales();

    // Anzahl der zu vergebenden Sitze
    public int getNbSiegesAPourvoir();

    // die Wahlhürde
    public double getSeuilElectoral();

    // Erfassung der Ergebnisse
    public void recordResultats(ListeElectorale[] listesElectorales);

    // die Sitzberechnung
    public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);

}

In Absatz 7 tauschte die Schicht [DAO] Daten mit einer SGBD aus. Hier tauscht die Schicht [DAO] Daten mit einem Webserver / jSON aus.

Zunächst betrachten wir die folgende Architektur:

15.3. Das Eclipse-Projekt

Das Eclipse-Projekt sieht wie folgt aus:

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

15.4. Maven-Konfiguration

Sie entspricht der in Abschnitt 13.6.2 beschriebenen.

15.5. Implementierung der Schicht [DAO]

  
  • Das Paket [elections.client.config] enthält die Spring-Konfiguration der Schicht [DAO];
  • Das Paket [elections .client.dao] enthält die Implementierung der Schicht [DAO];
  • Das Paket [elections .client.entities] enthält die mit dem Webdienst / jSON ausgetauschten Objekte;
  • Das Paket [elections .client.metier] enthält die Schicht [métier]
  • Das Paket [elections .client.ui] enthält die Schicht [UI]

15.5.1. Konfiguration der Schicht [métier]

  

Die Klasse [MetierConfig] übernimmt die Spring-Konfiguration der Schicht [métier]. 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 {

    // Konstanten
    static private final int TIMEOUT = 1000;
    static private final String URL_WEBJSON = "http://localhost:8080";

    @Bean
    public RestTemplate restTemplate(int timeout) {
        // Erstellung der Komponente RestTemplate
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // Timeout beim Datenaustausch
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // Ergebnis
        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 Filter jSON verwaltet werden müssen.

15.5.2. Die Entitäten

  

Die von den Schichten [DAO] und [métier] verarbeiteten Entitäten sind diejenigen, die sie mit dem Webdienst /jSON austauschen. Es handelt sich um Objekte vom Typ [ElectionsConfig] und [ListeElectorale]. Auf der Serverseite wiesen diese Entitäten Persistenz-Annotationen vom Typ JPA auf. Hier wurden diese Annotationen entfernt. Zur Erinnerung geben wir den Code der Entitäten noch einmal wieder:

[AbstractEntity]


package spring.webjson.client.entities;


public abstract class AbstractEntity {
    // Eigenschaften
    protected Long id;
    protected Long version;

    // Konstruktoren
    public AbstractEntity() {

    }

    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }

    // Neudefinition von [equals] und [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 und Setter
    ...
}

[ElectionsConfig]


package elections.webjson.client.entities;


public class ElectionsConfig extends AbstractEntity {

    // Felder
    private int nbSiegesAPourvoir;
    private double seuilElectoral;

    // Konstruktoren
    public ElectionsConfig() {

    }

    public ElectionsConfig(int nbSiegesAPourvoir, double seuilElectoral) {
        this.nbSiegesAPourvoir = nbSiegesAPourvoir;
        this.seuilElectoral = seuilElectoral;
    }

    // Getter und Setter
    ...
}

[ListeElectorale]


package elections.webjson.client.entities;


public class ListeElectorale extends AbstractEntity {

    // Felder
    private String nom;
    private int voix;
    private int sieges;
    private boolean elimine;

    // Konstruktoren
    public ListeElectorale() {
    }

    public ListeElectorale(String nom, int voix, int sieges, boolean elimine) {
        setNom(nom);
        setVoix(voix);
        setSieges(sieges);
        setElimine(elimine);
    }

    // Getter und Setter
    ...
}

15.5.3. Die Schnittstelle der Schicht [DAO]

  

Die Schicht [DAO] weist die folgende Schnittstelle [IClientDao] auf:


package elections.client.dao;

public interface IClientDao {

    // generische Abfrage
    String getResponse(String url, String jsonPost);

}

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

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

15.5.4. Implementierung des Datenaustauschs 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 {

    // Daten
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // generische Abfrage
    @Override
    public String getResponse(String url, String jsonPost) {

        try {
            // URL: URL – Kontakt
            // jsonPost: Der Wert jSON ist zu übermitteln

            // Anfrage ausführen
            RequestEntity<?> request;
            if (jsonPost != null) {
                // Anfrage POST
                request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
            } else {
                // Anfrage GET
                request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON).build();
            }
            // Die Abfrage wird ausgeführt
            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 wurde in Abschnitt 13.6.3.6 beschrieben.

15.6. Implementierung der Schicht [métier]

 

Wie bereits erwähnt, weist die Schicht [métier] dieselbe Schnittstelle [IElectionsMetier] auf wie in Abschnitt 8.4:


package elections.client.metier;

import elections.client.entities.ListeElectorale;

public interface IElectionsMetier {

    // um die Teilnehmerlisten abzurufen
    public ListeElectorale[] getListesElectorales();

    // die Anzahl der zu besetzenden Sitze
    public int getNbSiegesAPourvoir();

    // die Wahlhürde
    public double getSeuilElectoral();

    // Erfassung der Ergebnisse
    public void recordResultats(ListeElectorale[] listesElectorales);

    // Berechnung der Sitze
    public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);

}

Diese Schnittstelle wird durch die folgende 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;

    // Konfiguration der Wahl
    private ElectionsConfig electionsConfig;

    @PostConstruct
    public void init() {
        // Zuordnungsfunktionen jSON
        ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
        try {
            // Abfrage
            Response<ElectionsConfig> response = mapperResponse.readValue(dao.getResponse("/getElectionsConfig", null),
                    new TypeReference<Response<ElectionsConfig>>() {
                    });
            // Fehler?
            if (response.getStatus() != 0) {
                // Es wird 1 Ausnahme ausgelöst
                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) {
...
    }

    // Liste der Fehlermeldungen einer Ausnahme
    private List<String> getMessagesForException(Exception exception) {
        // Die Liste der Fehlermeldungen der Ausnahme wird abgerufen
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            // Die Meldung wird nur abgerufen, wenn sie != null und nicht leer ist
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // nächste Ursache
            cause = cause.getCause();
        }
        return erreurs;
    }
}

Der in Zeile 37 verwendete Typ [Response] ist die Antwort des Webservers / jSON, die in Abschnitt 14.7 beschrieben wird;


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


15.7. Der JUnit-Test

Kehren wir zur Client-Server-Architektur zurück, die wir gerade aufbauen:

Die Schicht [JUnit] [1] kommuniziert über die Schichten [2-4] mit der Schicht [Métier] des Servers [5]. Indem sichergestellt wird, dass die Schichten [Métier], [2] und [5] dieselbe Schnittstelle haben, werden die Schichten [2-4] transparent. Die Schicht [1] hat den Eindruck, direkt mit der Schicht [5] zu kommunizieren. Interessant ist, dass wir in [1] den Test JUnit verwenden können, der zuvor zum Testen der Schichten [Métier] und [5] verwendet wurde.

  

Zu erledigende Aufgabe: Führen Sie den Test JUnit des Projekts durch, um Ihre Implementierung sowie den Server und dessen Client zu überprüfen.


15.8. Implementierung der Schicht [UI]

Kommen wir zurück zu der Architektur, die wir aufbauen wollen:

Nachdem nun die Schicht [métier] [2] erstellt und getestet wurde, können wir die Schicht [ui] [1] erstellen.

Da die Schnittstelle [IElectionsMetier] der Schicht [métier] mit der des in Absatz 8 beschriebenen Projekts identisch ist, können wir in [3] das Projekt aus der Schicht [ui] aus Abschnitt 10 kopieren. Bei diesem Projekt handelte es sich um ein NetBeans-Projekt. Es genügt, die betreffenden Java-Klassen aus NetBeans in Eclipse zu kopieren und einzufügen. Anschließend müssen einige Anpassungen an den Paketen und Importen vorgenommen werden.

Das Gleiche gilt für die ausführbaren Klassen der Pakete [elections.client.boot] und [4].

Die Klasse [AbstractBootElections] lautet wie folgt:


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 {

    // Abruf des Spring-Kontexts
    protected AnnotationConfigApplicationContext ctx;

    public void run() {
        // Instanziierung der Schicht [ui]
        IElectionsUI electionsUI = null;
        try {
            // Abruf des Spring-Kontexts
            ctx = new AnnotationConfigApplicationContext(UiConfig.class);
            // Abruf der Schicht [ui]
            electionsUI = getUI();

...
  • Zeile 19: Der durch die Konfigurationsklasse [UiConfig] definierte Spring-Kontext wird instanziiert. Diese Klasse lautet wie folgt:
  

Die Klasse [UiConfig] lautet wie folgt:


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: Die Beans der Schicht [métier] werden importiert;
  • Zeile 7: Es wird angegeben, dass sich Spring-Beans im Paket [elections.client.ui] befinden;

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