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.















