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.















