20. Asynchrone Programmierung mit RxJava
Empfohlene Lektüre: [Einführung in RxJava. Anwendung in Swing- und Android-Umgebungen.]
In diesem Kapitel greifen wir Kapitel 17.6 wieder auf, in dem wir eine Client-Server-Anwendung mit der folgenden Architektur erstellt haben:
![]() |
Bestimmte Benutzeraktionen auf der Swing-Oberfläche in [1] lösen über ein HTTP-Netzwerk [2] Aktionen aus, die bis zur Datenbank in [3] reichen. Aus diesem Grund kann es mehr oder weniger lange dauern, bis die Antwort auf die Benutzeraktion eintrifft. Es wäre hilfreich, einen Ladeindikator in die Benutzeroberfläche zu integrieren, der die Möglichkeit bietet, den gestarteten Vorgang abzubrechen, falls er zu lange dauert. In Kapitel 17.6 ist jede Benutzeraktion, die einen Datenaustausch mit dem Server erfordert, synchron. Der vom Code ausgeführte Ereignis-Handler wird erst abgeschlossen, wenn die Antwort empfangen wurde. Während dieser Zeit ist die grafische Oberfläche eingefroren: Sie reagiert nicht auf neue Benutzeraktionen. Diese werden einfach in eine Warteschlange gestellt, um verarbeitet zu werden, sobald der aktuell ausgeführte Ereignis-Handler beendet ist. Wenn also eine Abbrechen-Schaltfläche angezeigt würde, könnte der Benutzer darauf klicken, aber es würde nichts passieren, bis der aktuelle Vorgang beendet ist. Die Abbrechen-Schaltfläche hätte dann keinen Zweck.
Damit der Klick auf die Schaltfläche „Abbrechen“ wirksam wird, muss der aktuelle Vorgang abgeschlossen sein. Um dies zu erreichen, muss der möglicherweise lang andauernde Vorgang asynchron gestartet werden:
- Der Ereignisbehandler initiiert den lang andauernden Vorgang, wartet jedoch nicht auf dessen Ergebnis und gibt die Kontrolle an den UI-Thread zurück, der die Ereignisse der grafischen Benutzeroberfläche verarbeitet. Der lang andauernde Vorgang wird in einem anderen Thread als dem UI-Thread gestartet, wodurch verhindert wird, dass dieser blockiert wird;
- Wenn der Benutzer auf die Schaltfläche „Abbrechen“ klickt, bevor der lang andauernde Vorgang abgeschlossen ist, kann der inaktive UI-Thread dieses Ereignis verarbeiten. Der lang andauernde Vorgang kann dann abgebrochen werden, indem sein Ergebnis ignoriert wird;
- Wenn der lang andauernde Vorgang nicht abgebrochen wurde, löst das Eintreffen der Antwort ein Ereignis im UI-Thread aus. Befindet sich der UI-Thread im Leerlauf, führt er den mit diesem Ereignis verbundenen Code aus, der die Antwort verarbeitet;
Die Benutzeroberfläche funktioniert wie zuvor. Sind die Antwortzeiten des Servers kurz, wird der Benutzer keinen Unterschied bemerken. Sind sie spürbar, wird dem Benutzer eine Abbrechen-Schaltfläche angezeigt, und er hat die Möglichkeit, den aktuellen Vorgang abzubrechen.
Die [Rx]-Bibliothek ermöglicht asynchrone Programmierung. Ihr Hauptvorteil liegt darin, dass sie auf zahlreiche Umgebungen (Java, .NET, JS usw.) portiert wurde und dass Kenntnisse in einer Umgebung leicht auf eine andere übertragen werden können. Hier verweisen wir auf Kapitel 2 des Dokuments [Einführung in RxJava. Anwendung in Swing- und Android-Umgebungen]. Dem Leser wird empfohlen, dieses zu lesen. In den folgenden Abschnitten werden wir Code aus den Beispielen dieses Kapitels verwenden.
Wir werden die Architektur der Anwendung wie folgt weiterentwickeln:
![]() |
- In [1] fügen wir eine [RxJava]-Schicht zwischen der [Swing]-Schicht und der [Geschäftslogik]-Schicht ein. Die Methoden in letzterer werden nun asynchron aufgerufen;
Wir gehen in mehreren Schritten vor:
- Schritt 1: Die [Geschäftslogik, DAO]-Schicht stellt derzeit eine synchrone Schnittstelle zur [UI]-Schicht dar. Wir werden sie in eine asynchrone [RxJava, Geschäftslogik, DAO]-Schicht umwandeln;
- Schritt 2: Wir wandeln die synchrone Konsolenanwendung in eine Anwendung um, die zwar weiterhin synchron bleibt, aber die asynchrone Schnittstelle [RxJava, Geschäftslogik, DAO] nutzt;
- Schritt 3: Wir werden die synchrone Swing-Anwendung in eine asynchrone Swing-Anwendung umwandeln;
20.1. Schritt 1
Wir wandeln die derzeitige synchrone Schicht [Geschäftslogik, DAO] in eine asynchrone Schicht [RxJava, Geschäftslogik, DAO] um.
20.1.1. Erstellung
Wir beginnen mit dem Maven-Projekt aus Kapitel 17.4, das wir in NetBeans öffnen:
![]() | ![]() |
Wir duplizieren dieses Projekt [1] (Kopieren/Einfügen) in ein neues Projekt [elections-rxjava-business-dao-security-webjson] [2].
20.1.2. Maven-Konfiguration
Wir aktualisieren die Datei [pom.xml] des neuen Projekts, um die Abhängigkeit zur Bibliothek [RxJava] hinzuzufügen:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.elections</groupId>
<artifactId>elections-metier-dao-security-rxjava-webjson</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>Client jUnit du serveur web / jSON</description>
<name>elections-metier-dao-security-rxjava-webjson</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- jSON library used by Spring -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- component used by Spring RestTemplate -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
<scope>test</scope>
</dependency>
<!-- log library -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.reactivex/rxjava -->
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- Zeilen 65–70: Wir haben die Abhängigkeit zur RxJava-Bibliothek hinzugefügt;
20.1.3. Asynchrone Implementierung der [Business]-Schicht
![]() |
Um die [RxJava, Geschäfts-]Ebene zu implementieren, fügen wir dem Projekt eine asynchrone Schnittstelle [IRxElectionsMetier] [1] und deren Implementierung [RxElectionsMetier] [2] hinzu:
![]() |
Die Schnittstelle [IRxElectionsMetier] ist die asynchrone Schnittstelle für die [RxJava, business]-Schicht. Ihr Code lautet wie folgt:
package elections.security.client.metier;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import rx.Observable;
public interface IRxElectionsMetier {
// authentication
Observable<Void> authenticate(User user);
// get the lists in competition
Observable<ListeElectorale[]> getListesElectorales(User user);
// the number of seats to be filled
Observable<Integer> getNbSiegesAPourvoir(User user);
// the electoral threshold
Observable<Double> getSeuilElectoral(User user);
// recording results
Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales);
// calculating seats
Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales);
}
Die Schnittstelle [IRxElectionsMetier] erbt die Methoden der Schnittstelle [IElectionsMetier]; während jedoch eine Methode M in der Schnittstelle [IElectionsMetier] ein Ergebnis vom Typ T zurückgab, gibt die Methode M in der Schnittstelle [IRxElectionsMetier] ein Ergebnis vom Typ Observable<T> zurück. Der Typ [Observable] wird von der RxJava-Bibliothek bereitgestellt. Ein Observable<T>-Typ stellt die Methode [subscribe] bereit, die den Typ T asynchron abruft. Mit dieser Methode sind drei Ereignisse verbunden:
- onSuccess(T result), das meldet, dass ein Ergebnis vom Typ T verfügbar ist. Die asynchrone Operation kann mehrere Ergebnisse zurückgeben;
- onError(Throwable th), das meldet, dass bei der asynchronen Operation ein Fehler aufgetreten ist;
- onCompleted(), das meldet, dass der asynchrone Vorgang abgeschlossen ist;
Solange die Methode [Observable.subscribe] nicht aufgerufen wird, wird der mit dem Observable verbundene asynchrone Vorgang nicht initiiert. Der Code, der eine Methode M der Schnittstelle [IRxElectionsMetier] aufruft, erhält nicht das erwartete Ergebnis T, sondern einen Typ Observable<T>, der es ihm später ermöglicht, das Ergebnis T durch Aufruf der Methode [Observable.subscribe] abzurufen.
Die [RxElectionsMetier]-Implementierung der [IRxElectionsMetier]-Schnittstelle lautet wie folgt:
package elections.security.client.metier;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.Observable;
@Component
public class RxElectionsMetier implements IRxElectionsMetier {
@Autowired
private IElectionsMetier metier;
@Override
public Observable<Void> authenticate(User user) {
...
}
@Override
public Observable<ListeElectorale[]> getListesElectorales(User user) {
return Observable.create(subscriber -> {
try {
// call synchronous method then reply to subscriber
subscriber.onNext(metier.getListesElectorales(user));
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<Integer> getNbSiegesAPourvoir(User user) {
...
}
@Override
public Observable<Double> getSeuilElectoral(User user) {
...
}
@Override
public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
...
}
@Override
public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
...
}
}
- Zeilen 12–13: Spring-Injektion der synchronen Geschäftsschicht;
- Zeilen 20–34: Wir werden die Methode [getVoterLists] kommentieren, die statt eines Typs [VoterList[]] einen Typ [Observable<VoterList[]>] zurückgibt;
- Zeilen 22–32: Die statische Methode [Observable.create] ermöglicht es Ihnen, ein Observable aus einem Typ [Subscriber] zu erstellen. Der Typ [Subscriber] repräsentiert einen Abonnenten der von dem beobachteten Prozess (dem Observable) erzeugten Ergebnisströme. Er stellt drei Methoden bereit:
- [Subscriber.onNext] (Zeile 25), um ein Ergebnis vom beobachteten Prozess zu empfangen;
- [Subscriber.onError] (Zeile 30) zum Empfangen einer Ausnahme vom beobachteten Prozess. Nach einer Ausnahme gibt der Typ [Observable] keine Ergebnisse mehr aus;
- [Subscriber.onCompleted] (Zeile 27) zum Empfangen des Signals für das Ende der Ausgabe vom beobachteten Prozess. Hier gibt der beobachtete Prozess nur ein Element aus. Beachten Sie, dass dieses Signal nicht ausgegeben wird, wenn eine Ausnahme auftritt. Dies ist das Standardverhalten von Observables: Die Ausgabe einer Ausnahme signalisiert auch das Ende der Ausgaben. Abonnenten sind sich dessen bewusst;
- Zeilen 22–34: Die Methode [Observable.create] nimmt einen Typ [Observable.OnSubscribe] als Parameter entgegen. Dieser Typ ist eine funktionale Schnittstelle. Dieses Konzept wurde mit Java 8 eingeführt und bezieht sich auf eine Schnittstelle mit einer einzigen Methode. Hier lautet die einzige Methode der Schnittstelle [Observable.OnSubscribe] wie folgt:
Um eine funktionale Schnittstelle m(param1, param2, ..., paramn) mit einer einzigen Methode zu implementieren, können Sie die folgende vereinfachte Syntax verwenden:
Dies geschieht in den Zeilen 22–34:
- [subscriber] ist der Parameter der Methode [Observable.OnSubscribe.call];
- Zeilen 23–32: der Code, den wir der Methode [call] übergeben möchten;
- Zeile 25: Wir fordern die Wählerlisten synchron von der in Zeile 13 injizierten [business]-Schicht an. Wir müssen daher auf das Ergebnis warten. Sobald es empfangen wird, wird es an die [onNext]-Methode des Abonnenten übergeben;
- Zeile 28: Im Falle eines Fehlers wird die Ausnahme an die [onError]-Methode des Abonnenten übergeben;
- Zeile 31: Wir warten auf ein einzelnes Ergebnis. Sobald es vorliegt (entweder die Wählerlisten oder eine Ausnahme), benachrichtigen wir den Abonnenten, dass der beobachtete Prozess keine weiteren Ergebnisse mehr ausgibt;
Es ist wichtig zu beachten, dass die Methode [RxElectionsMetier] einen Typ Observable<VoterList[]> zurückgibt und nicht den Typ VoterList[] selbst. Der aufrufende Code muss die Methode Observable<VoterList[]>.subscribe aufrufen, damit der Code in den Zeilen 23–33 ausgeführt wird und die Wählerlisten über Zeile 25 zurückgibt.
Der Code für die anderen Methoden ist ähnlich:
package elections.security.client.metier;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.Observable;
@Component
public class RxElectionsMetier implements IRxElectionsMetier {
@Autowired
private IElectionsMetier metier;
@Override
public Observable<Void> authenticate(User user) {
return Observable.create(subscriber -> {
try {
// synchronous method call
metier.authenticate(user);
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<ListeElectorale[]> getListesElectorales(User user) {
return Observable.create(subscriber -> {
try {
// call synchronous method then reply to subscriber
subscriber.onNext(metier.getListesElectorales(user));
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<Integer> getNbSiegesAPourvoir(User user) {
return Observable.create(subscriber -> {
try {
// call synchronous method then reply to subscriber
subscriber.onNext(metier.getNbSiegesAPourvoir(user));
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<Double> getSeuilElectoral(User user) {
return Observable.create(subscriber -> {
try {
// call synchronous method then reply to subscriber
subscriber.onNext(metier.getSeuilElectoral(user));
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
return Observable.create(subscriber -> {
try {
// synchronous method call
metier.recordResultats(user, listesElectorales);
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
return Observable.create(subscriber -> {
try {
// call synchronous method then reply to subscriber
subscriber.onNext(metier.calculerSieges(user, listesElectorales));
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
}
- Zeilen 20 und 81: Die [onNext]-Methode des Abonnenten wird nicht aufgerufen, da der Abonnent keine Ergebnisse erwartet;
20.1.4. JUnit-Tests für die [Business]-Schicht
![]() |
20.1.4.1. Test01
Wir greifen den in Abschnitt 17.4.4 besprochenen Unit-Test [Test01] wieder auf. Er wurde entwickelt, um synchrone Aufrufe an die Schnittstelle [IElectionsMetier] zu senden. Wir ändern ihn so, dass er synchrone Aufrufe an die neue Schnittstelle [IRxElectionsMetier] sendet. Es ist tatsächlich möglich, synchrone Aufrufe an eine asynchrone RxJava-Schnittstelle zu senden. Der Code sieht nun wie folgt aus:
package elections.security.client.metier.junit;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import elections.security.client.config.MetierConfig;
import elections.security.client.entities.ElectionsException;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import elections.security.client.metier.IRxElectionsMetier;
import rx.observables.BlockingObservable;
@SpringApplicationConfiguration(classes = MetierConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {
// layer [electionsMetier]
@Autowired
private IRxElectionsMetier electionsMetier;
// mapper jSON
private final ObjectMapper mapper = new ObjectMapper();
// users
static private User admin;
static private User user;
static private User unknown;
@BeforeClass
public static void initTest() {
admin = new User("admin", "admin");
user = new User("user", "user");
unknown = new User("x", "y");
}
@Test()
public void checkUserUser() {
ElectionsException se = null;
try {
BlockingObservable.from(electionsMetier.authenticate(user)).firstOrDefault(null);
} catch (ElectionsException e) {
se = e;
}
Assert.assertNotNull(se);
Assert.assertEquals("403 Forbidden", se.getErreurs().get(0));
}
@Test()
public void checkUserUnknown() {
ElectionsException se = null;
try {
BlockingObservable.from(electionsMetier.authenticate(unknown)).firstOrDefault(null);
} catch (ElectionsException e) {
se = e;
}
Assert.assertNotNull(se);
Assert.assertEquals("401 Unauthorized", se.getErreurs().get(0));
}
@Test()
public void checkUserAdmin() {
ElectionsException se = null;
try {
BlockingObservable.from(electionsMetier.authenticate(admin)).firstOrDefault(null);
} catch (ElectionsException e) {
se = e;
}
Assert.assertNull(se);
}
/**
* vérification 1 : méthode de calcul des sièges on fixe en dur les listes
*/
@Test
public void calculSieges1() {
// create the table of 7 candidate lists
ListeElectorale[] listes = new ListeElectorale[7];
listes[0] = new ListeElectorale("A", 32000, 0, false);
listes[1] = new ListeElectorale("B", 25000, 0, false);
listes[2] = new ListeElectorale("C", 16000, 0, false);
listes[3] = new ListeElectorale("D", 12000, 0, false);
listes[4] = new ListeElectorale("E", 8000, 0, false);
listes[5] = new ListeElectorale("F", 4500, 0, false);
listes[6] = new ListeElectorale("G", 2500, 0, false);
// the seats for each list are calculated
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// check results
Assert.assertEquals(2, listes[0].getSieges());
Assert.assertFalse(listes[0].isElimine());
Assert.assertEquals(2, listes[1].getSieges());
Assert.assertFalse(listes[1].isElimine());
Assert.assertEquals(1, listes[2].getSieges());
Assert.assertFalse(listes[2].isElimine());
Assert.assertEquals(1, listes[3].getSieges());
Assert.assertFalse(listes[3].isElimine());
Assert.assertEquals(0, listes[4].getSieges());
Assert.assertFalse(listes[4].isElimine());
Assert.assertEquals(0, listes[5].getSieges());
Assert.assertTrue(listes[5].isElimine());
Assert.assertEquals(0, listes[6].getSieges());
Assert.assertTrue(listes[6].isElimine());
}
/**
* vérification 2 : méthode de calcul des sièges on demande les listes à la couche [metier] puis on fixe en dur les
* voix
*/
@Test
public void calculSieges2() {
// create the table of 7 candidate lists
ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// the voices are hard-fixed
listes[0].setVoix(32000);
listes[1].setVoix(25000);
listes[2].setVoix(16000);
listes[3].setVoix(12000);
listes[4].setVoix(8000);
listes[5].setVoix(4500);
listes[6].setVoix(2500);
// the seats obtained by each list are calculated
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// check results
Assert.assertEquals(2, listes[0].getSieges());
Assert.assertFalse(listes[0].isElimine());
Assert.assertEquals(2, listes[1].getSieges());
Assert.assertFalse(listes[1].isElimine());
Assert.assertEquals(1, listes[2].getSieges());
Assert.assertFalse(listes[2].isElimine());
Assert.assertEquals(1, listes[3].getSieges());
Assert.assertFalse(listes[3].isElimine());
Assert.assertEquals(0, listes[4].getSieges());
Assert.assertFalse(listes[4].isElimine());
Assert.assertEquals(0, listes[5].getSieges());
Assert.assertTrue(listes[5].isElimine());
Assert.assertEquals(0, listes[6].getSieges());
Assert.assertTrue(listes[6].isElimine());
}
/**
* vérification 3 méthode de calcul des sièges on provoque une exception
*/
@Test(expected = ElectionsException.class)
public void calculSieges3() {
// we create a table of 24 candidate lists, each with 1 vote
ListeElectorale[] listes = new ListeElectorale[25];
// all 25 lists will have the same number of votes (4%)
for (int i = 0; i < listes.length; i++) {
listes[i] = new ListeElectorale("Liste" + (i + 1), 1, 0, false);
}
// calculation of seats - normally there should be a ElectionsException
// with an electoral threshold of 5%
BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
}
/**
* enregistrement des résultats de l'élection
*
* @throws JsonProcessingException
*/
@Test
public void ecritureResultatsElections() throws JsonProcessingException {
// create the table of 7 candidate lists
ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// the voices are hard-fixed
listes[0].setVoix(32000);
listes[1].setVoix(25000);
listes[2].setVoix(16000);
listes[3].setVoix(12000);
listes[4].setVoix(8000);
listes[5].setVoix(4500);
listes[6].setVoix(2500);
// the seats obtained by each list are calculated
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// display results
for (int i = 0; i < listes.length; i++) {
System.out.println(mapper.writeValueAsString(listes[i]));
}
// results are entered into the database
BlockingObservable.from(electionsMetier.recordResultats(admin, listes)).firstOrDefault(null);
// check results
listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// display results
for (int i = 0; i < listes.length; i++) {
System.out.println(mapper.writeValueAsString(listes[i]));
}
Assert.assertEquals(2, listes[0].getSieges());
Assert.assertFalse(listes[0].isElimine());
Assert.assertEquals(2, listes[1].getSieges());
Assert.assertFalse(listes[1].isElimine());
Assert.assertEquals(1, listes[2].getSieges());
Assert.assertFalse(listes[2].isElimine());
Assert.assertEquals(1, listes[3].getSieges());
Assert.assertFalse(listes[3].isElimine());
Assert.assertEquals(0, listes[4].getSieges());
Assert.assertFalse(listes[4].isElimine());
Assert.assertEquals(0, listes[5].getSieges());
Assert.assertTrue(listes[5].isElimine());
Assert.assertEquals(0, listes[6].getSieges());
Assert.assertTrue(listes[6].isElimine());
}
}
Sehen wir uns die Änderungen an:
- Zeile 48: Die statische Methode [BlockingObservable.from(Observable).first]:
- abonniert das Observable, das als Parameter an [from] übergeben wurde;
- löst die Ausführung des mit dem Observable verbundenen Codes aus;
- wartet auf den Empfang des ersten Ergebnisses. Es handelt sich also um eine synchrone Operation;
Wir verwenden hier die Methode [firstOrDefault(null)], da das Observable [metier.authenticate] bei der Ausführung kein Ergebnis zurückgibt. Das Ergebnis der Methode [firstOrDefault(null)] ist daher null, ein Wert, der hier nicht verwendet wird;
Wir wiederholen dieses Muster im restlichen Code immer dann, wenn wir die [business]-Schicht aufrufen möchten.
Der Unit-Test [Test01] muss erfolgreich sein:
![]() |
Aufgabe: Überprüfen Sie, ob der Test [Test01] erfolgreich ist.
20.1.4.2. Test02
Wir ändern den Test [Test01] so, dass nun die asynchrone Schnittstelle [IRxElectionsMetier] getestet wird, indem wir asynchrone Aufrufe an ihre Methoden senden.
Sehen wir uns einen ersten Test an:
// thread synchronization semaphore
private CountDownLatch latch;
// -----------------------------------
private ElectionsException checkUserUserException;
@Test()
public void checkUserUser() throws InterruptedException {
// 1" semaphore
latch = new CountDownLatch((1));
// asynchronous operation
electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
.subscribe((result) -> {
},
(th) -> {
checkUserUserException = (ElectionsException) th;
latch.countDown();
},
() -> {
latch.countDown();
});
// waiting for semaphore
latch.await();
// checking results
Assert.assertNotNull(checkUserUserException);
Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
- Zeile 2: Ein Semaphor ist ein Werkzeug, das zur Synchronisation von Threads untereinander dient. Threads sind parallel ablaufende Ausführungsabläufe. Um die Aufgabe T1 auszuführen, benötigt der Thread [Thread1] möglicherweise den Abschluss der Aufgabe T2, die vom Thread [Thread2] ausgeführt wird. Er wartet dann darauf, dass der Thread [Thread2] ihm ein Signal sendet, das anzeigt, dass die Aufgabe T2 abgeschlossen ist. Es gibt verschiedene Möglichkeiten, diese Synchronisation zwischen zwei Threads zu verwalten. Die hier verwendete Methode ist wie folgt:
- Zeile 10: Thread [Thread1] erstellt ein Semaphor mit dem Wert 1;
- Zeile 12: Thread [Thread1] erstellt und startet einen Thread [Thread2]. Dies wird mit folgender Syntax erreicht:
electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
Die Methode [Observable.subscribeOn] legt den Thread fest, auf dem der beobachtete Prozess ausgeführt wird. Der Parameter von [subscribeOn] ist ein Thread-Pool. Die RxJava-Bibliothek stellt mehrere Pools bereit, die für unterschiedliche Situationen geeignet sind. Der Pool [Schedulers.io()] wird für Netzwerkoperationen empfohlen;
- (Fortsetzung)
- Zeilen 12–13: die Operation
electionsMetier.authenticate(user).subscribeOn(Schedulers.io()).subscribe(...)
führt die in dem Observable [authenticate(user)] gekapselte synchrone Operation aus. Da diese synchrone Operation jedoch auf einem anderen Thread als [Thread1] gestartet wird, wartet dieser nicht auf die Antwort der Methode [subscribe] und fährt mit der nächsten Anweisung fort;
- (Fortsetzung)
- Zeile 23: Der Thread [Thread1] hält an und wartet darauf, dass der Semaphor auf 0 geht (er steht derzeit auf 1);
- Zeilen 13–21: Die Methode [subscribe] nimmt drei Lambda-Funktionen als Parameter entgegen:
- Die erste [(result)->{...}] wird jedes Mal aufgerufen, wenn das Observable [authenticate(user)] ein Ergebnis [result] ausgibt. Hier haben wir ein Observable [authenticate(user)], das etwas ausführt, aber kein Ergebnis ausgibt. Die Lambda-Funktion [(result)->{}] wird daher niemals aufgerufen. Deshalb ist ihr Code hier leer [{}];
- die zweite [(th)->{...}] nimmt einen Typ [Throwable] als Parameter. Sie wird aufgerufen, wenn bei der Ausführung des Observables eine Ausnahme auftritt. Hier behandeln wir den Parameter [Throwable th] wie folgt:
- Zeile 16: Wir speichern ihn in einem Feld der Testklasse vom Typ [ElectionsException], da das ausgeführte Observable nur diesen Ausnahmetyp auslöst;
- Zeile 17: Wir setzen den Semaphor auf 0, um anzuzeigen, dass der Thread [Thread2] seine Arbeit beendet hat;
- Das dritte [()->{...}] wird aufgerufen, wenn das Observable keine Elemente mehr auszugeben hat. Wir behandeln dieses Ereignis wie folgt:
- Zeile 20: Wir setzen das Semaphor auf 0, um anzuzeigen, dass der Thread [Thread2] seine Arbeit beendet hat;
Beachten Sie, dass das dritte Lambda nicht aufgerufen wird, wenn eine Ausnahme auftritt. Deshalb mussten wir das Semaphor auch in Zeile 17 auf 0 setzen;
- Zeile 25: Wenn wir diese Zeile erreichen, hat das Observable seine Arbeit beendet. Wir können dann die gleichen Prüfungen wie im Test [Test01] durchführen;
Betrachten wir einen weiteren Test:
// -----------------------------------
private ElectionsException calculSieges1Exception;
private ListeElectorale[] listesCalculSieges1;
@Test
public void calculSieges1() throws InterruptedException {
// create the table of 7 candidate lists
ListeElectorale[] listes = new ListeElectorale[7];
listes[0] = new ListeElectorale("A", 32000, 0, false);
listes[1] = new ListeElectorale("B", 25000, 0, false);
listes[2] = new ListeElectorale("C", 16000, 0, false);
listes[3] = new ListeElectorale("D", 12000, 0, false);
listes[4] = new ListeElectorale("E", 8000, 0, false);
listes[5] = new ListeElectorale("F", 4500, 0, false);
listes[6] = new ListeElectorale("G", 2500, 0, false);
// 1" semaphore
latch = new CountDownLatch((1));
// asynchronous operation
// the seats for each list are calculated
electionsMetier.calculerSieges(admin, listes).subscribeOn(Schedulers.io())
.subscribe((result) -> {
listesCalculSieges1 = result;
},
(th) -> {
calculSieges1Exception = (ElectionsException) th;
latch.countDown();
},
() -> {
latch.countDown();
});
// waiting for semaphore
latch.await();
// check results
Assert.assertNull(calculSieges1Exception);
Assert.assertEquals(2, listesCalculSieges1[0].getSieges());
Assert.assertFalse(listesCalculSieges1[0].isElimine());
Assert.assertEquals(2, listesCalculSieges1[1].getSieges());
Assert.assertFalse(listesCalculSieges1[1].isElimine());
Assert.assertEquals(1, listesCalculSieges1[2].getSieges());
Assert.assertFalse(listesCalculSieges1[2].isElimine());
Assert.assertEquals(1, listesCalculSieges1[3].getSieges());
Assert.assertFalse(listesCalculSieges1[3].isElimine());
Assert.assertEquals(0, listesCalculSieges1[4].getSieges());
Assert.assertFalse(listesCalculSieges1[4].isElimine());
Assert.assertEquals(0, listesCalculSieges1[5].getSieges());
Assert.assertTrue(listesCalculSieges1[5].isElimine());
Assert.assertEquals(0, listesCalculSieges1[6].getSieges());
Assert.assertTrue(listesCalculSieges1[6].isElimine());
}
- Zeilen 20–30: asynchrone Ausführung des Observables [electionsMetier.calculateSeats(admin, lists)];
- Zeilen 21–23: Die Ausführung des Observables gibt einen Typ [VoterList[]] zurück, der in einem Feld der Testklasse in Zeile 3 gespeichert wird;
- Zeilen 34–48: Diese Prüfungen stammen aus dem Test [Test01], dem wir die Prüfung in Zeile 34 hinzugefügt haben, die sicherstellt, dass keine Ausnahme aufgetreten ist;
Der gesamte Test [Test02] ist in den Kursunterlagen verfügbar.
Aufgabe: Führen Sie den Test [Test02] aus und überprüfen Sie, ob er erfolgreich ist.
20.1.4.3. Test03
Der Test [Test03] macht dasselbe wie der Test [Test01]: Er testet die Schnittstelle [IRxElectionsMetier] mithilfe synchroner Aufrufe an diese Schnittstelle. Es handelt sich um eine Kopie des Tests [Test02] mit zwei Unterschieden:
- Die Observables werden nicht mehr in einem anderen Thread ausgeführt als dem, in dem die Tests laufen. Wenn Thread [Thread1] die [subscribe]-Methode eines Observables ausführt, initiiert er eine HTTP-Operation zum Server, ebenfalls auf Thread [Thread1]. Die gesamte [subscribe]-Methode wird dann synchron;
- da es nun nur noch einen Thread gibt, wird die Thread-Synchronisation überflüssig und das Semaphor verschwindet;
Hier sind zwei Beispiele für Tests:
// -----------------------------------
private ElectionsException checkUserUserException;
@Test()
public void checkUserUser() throws InterruptedException {
// synchronous operation
electionsMetier.authenticate(user)
.subscribe((result) -> {
},
(th) -> {
checkUserUserException = (ElectionsException) th;
},
() -> {
});
// checking results
Assert.assertNotNull(checkUserUserException);
Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
- Zeile 7: Standardmäßig wird die Methode [electionsMetier.authenticate(user).subscribe] im Thread des aufrufenden Codes ausgeführt. Es handelt sich also um einen synchronen Vorgang;
// -----------------------------------
private ElectionsException calculSieges1Exception;
private ListeElectorale[] listesCalculSieges1;
@Test
public void calculSieges1() throws InterruptedException {
// create the table of 7 candidate lists
ListeElectorale[] listes = new ListeElectorale[7];
listes[0] = new ListeElectorale("A", 32000, 0, false);
listes[1] = new ListeElectorale("B", 25000, 0, false);
listes[2] = new ListeElectorale("C", 16000, 0, false);
listes[3] = new ListeElectorale("D", 12000, 0, false);
listes[4] = new ListeElectorale("E", 8000, 0, false);
listes[5] = new ListeElectorale("F", 4500, 0, false);
listes[6] = new ListeElectorale("G", 2500, 0, false);
// synchronous operation
// the seats for each list are calculated
electionsMetier.calculerSieges(admin, listes)
.subscribe((result) -> {
listesCalculSieges1 = result;
},
(th) -> {
calculSieges1Exception = (ElectionsException) th;
},
() -> {
});
// check results
Assert.assertNull(calculSieges1Exception);
Assert.assertEquals(2, listesCalculSieges1[0].getSieges());
Assert.assertFalse(listesCalculSieges1[0].isElimine());
Assert.assertEquals(2, listesCalculSieges1[1].getSieges());
Assert.assertFalse(listesCalculSieges1[1].isElimine());
Assert.assertEquals(1, listesCalculSieges1[2].getSieges());
Assert.assertFalse(listesCalculSieges1[2].isElimine());
Assert.assertEquals(1, listesCalculSieges1[3].getSieges());
Assert.assertFalse(listesCalculSieges1[3].isElimine());
Assert.assertEquals(0, listesCalculSieges1[4].getSieges());
Assert.assertFalse(listesCalculSieges1[4].isElimine());
Assert.assertEquals(0, listesCalculSieges1[5].getSieges());
Assert.assertTrue(listesCalculSieges1[5].isElimine());
Assert.assertEquals(0, listesCalculSieges1[6].getSieges());
Assert.assertTrue(listesCalculSieges1[6].isElimine());
}
Aufgabe: Führen Sie den Test [Test03] aus und überprüfen Sie, ob er erfolgreich ist.
20.2. Schritt 2
Wir werden nun die synchrone Konsolenanwendung aus Kapitel 17.5 in eine Anwendung umwandeln, die zwar weiterhin synchron bleibt, aber die asynchrone Schnittstelle [RxJava, Geschäftslogik, DAO] nutzt;
![]() |
Wir beginnen mit dem Projekt [elections-console-business-dao-security-webjson] [1] aus Kapitel 17.5, das wir in ein neues Projekt [elections-console-rxjava-business-dao-security-webjson] [2] duplizieren:
![]() | ![]() |
- In [3-4] entfernen wir im neuen Projekt die Abhängigkeit von der alten synchronen [business]-Schicht;
![]() | ![]() |
- In [5-9] fügen wir eine Abhängigkeit von der neuen asynchronen [Business]-Schicht hinzu;
![]() | ![]() |
- in [10-14] benennen wir die Klasse [ElectionsConsole] in [ElectionsConsole01] um;
Ebenso benennen wir die Klasse [BootElectionsConsole] in [BootElectionsConsole01] um:
![]() |
Der aktuelle Code für die Klasse [BootElectionsConsole01] lautet wie folgt:
package elections.security.client.boot;
import elections.security.client.console.IElectionsUI;
public class BootElectionsConsole01 extends AbstractBootElections{
public static void main(String[] arguments) {
new BootElectionsConsole01().run();
}
@Override
protected IElectionsUI getUI() {
return ctx.getBean("electionsConsole",IElectionsUI.class);
}
}
- Zeile 13: Da wir den Klassennamen von [ElectionsConsole] in [ElectionsConsole01] geändert haben, müssen wir nun schreiben:
return ctx.getBean("electionsConsole01",IElectionsUI.class);
Kehren wir zum Code für die Klasse [ElectionsConsole01] zurück:
@Component
public class ElectionsConsole01 implements IElectionsUI {
@Autowired
private IElectionsMetier electionsMetier;
@Autowired
private User admin;
@Override
public void run() {
// competing lists
ListeElectorale[] listes;
// data entry
try (Scanner clavier = new Scanner(System.in)) {
// lists in competition are requested from the [metier] layer
listes = electionsMetier.getListesElectorales(admin);
...
// we calculate the number of seats
listes=electionsMetier.calculerSieges(admin,listes);
// we record the results
electionsMetier.recordResultats(admin,listes);
...
}
Wenn wir dem Beispiel des Tests [Test01] in Abschnitt 20.1.4.1 folgen, ändern sich die Zeilen 5, 17, 20 und 22 wie folgt:
@Component
public class ElectionsConsole01 implements IElectionsUI {
@Autowired
private IRxElectionsMetier electionsMetier;
@Autowired
private User admin;
@Override
public void run() {
// competing lists
ListeElectorale[] listes;
// data entry
try (Scanner clavier = new Scanner(System.in)) {
// lists in competition are requested from the [metier] layer
listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
...
// we calculate the number of seats
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// we record the results
BlockingObservable.from(electionsMetier.recordResultats(admin, listes));
...
}
Aufgabe: Konfigurieren Sie das Projekt so, dass die Klasse [BootElectionsConsole01] mit den drei Parametern [SS, Arbeitsstunden, Arbeitstage] ausgeführt wird, und überprüfen Sie, ob die Ausführung des Projekts in der konfigurierten Form die erwarteten Ergebnisse liefert.
Aufgabe: Konfigurieren Sie das Projekt so, dass das Paar [BootElectionsConsole02, ElectionsConsole02] ausgeführt wird, wobei die Klasse [ElectionsConsole02] nach dem Vorbild des Tests [Test02] in Abschnitt 20.1.4.2 geschrieben wurde.
Aufgabe: Konfigurieren Sie das Projekt so, dass das Paar [BootElectionsConsole03, ElectionsConsole03] ausgeführt wird, wobei die Klasse [ElectionsConsole03] nach dem Vorbild des Tests [Test03] in Abschnitt 20.1.4.3 geschrieben wurde.
20.3. Schritt 3
Wir werden nun damit fortfahren, die Swing-Anwendung in eine asynchrone Umgebung zu portieren.
![]() |
Zunächst duplizieren wir das Projekt [elections-swing-metier-dao-security-webjson] [1] aus Kapitel 17.6 in ein neues Projekt [elections-swing-rxjava-metier-dao-security-webjson] [2]:
![]() | ![]() |
- In [3, 4] entfernen wir die Abhängigkeit von der synchronen [console]-Schicht;
![]() | ![]() |
- in [5–9] fügen wir eine Abhängigkeit von der asynchronen Konsolenschicht hinzu;
Die [swing]-Schicht führt echte asynchrone Aufrufe an die [business]-Schicht durch. Beim Aufruf einer Methode in letzterer gibt es zwei Threads:
- den UI-Thread, der Ereignisse verarbeitet;
- ein E/A-Thread, der den HTTP-Aufruf an den Server ausführt;
Während des gesamten asynchronen Aufrufs sollten wir ein Ladesymbol und eine Schaltfläche zum Abbrechen anzeigen. Das werden wir hier nicht tun, und es wird Ihnen als Verbesserung der Anwendung vorgeschlagen. Die Änderungen werden in den beiden Klassen vorgenommen, die Aufrufe an die [Business]-Schicht senden:
![]() |
20.3.1. Maven-Konfiguration
Hier verwenden wir die [RxSwing]-Bibliothek, die die [RxJava]-Bibliothek um Funktionen erweitert, die nur in einer Swing-Umgebung verfügbar sind. Dazu ändern wir die Datei [pom.xml] wie folgt:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.elections</groupId>
<artifactId>elections-swing-rxjava-metier-dao-security-webjson</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>elections-swing-rxjava-metier-dao-security-webjson</name>
<description>couche swing asynchrone du client web / jSON</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- RxSwing -->
<!-- https://mvnrepository.com/artifact/io.reactivex/rxswing -->
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxswing</artifactId>
<version>0.27.0</version>
</dependency>
<!-- lower layers -->
<dependency>
<groupId>istia.st.elections</groupId>
<artifactId>elections-console-rxjava-metier-dao-security-webjson</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
20.3.2. Die Klasse [ElectionsConnectForm]
Bei einer asynchronen Operation sieht die Klasse [ElectionsConnectForm] wie folgt aus:
package elections.security.client.swing;
import elections.security.client.console.IElectionsUI;
import elections.security.client.entities.User;
import java.awt.Dimension;
import java.awt.Toolkit;
import javax.swing.SwingUtilities;
import elections.security.client.metier.IRxElectionsMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.schedulers.Schedulers;
import rx.schedulers.SwingScheduler;
@Component
public class ElectionsConnectForm extends AbstractElectionsConnectForm implements IElectionsUI {
private static final long serialVersionUID = 1L;
// reference to the asynchronous [business] layer
@Autowired
private IRxElectionsMetier metier;
// logged-in user
private User user;
// main form
@Autowired
private ElectionsMainForm electionsMainForm;
// session UI
@Autowired
private UiSession uiSession;
@Override
protected void doConnect() {
if (isPageValid()) {
// user authentication
metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(
// there is no answer
(result) -> {
},
// exception management
(th) -> {
// we note the error
String info = getInfoForException("Les erreurs suivantes se sont produites :", th);
// display info
jTextPaneErreurs.setText(info);
jTextPaneErreurs.setCaretPosition(0);
},
// authentication is complete
() -> {
// the user is stored in the session
uiSession.setUser(user);
// connection view is hidden
setVisible(false);
// the main view is displayed
electionsMainForm.run();
});
}
}
// initializations
@Override
protected void init() {
...
}
@Override
public void run() {
// the graphical interface is displayed
SwingUtilities.invokeLater(new Runnable() {
public void run() {
init();
setVisible(true);
}
});
}
private boolean isPageValid() {
...
}
private String getInfoForException(String message, Throwable ex) {
...
}
}
- Zeilen 36–63: Die Methode [doConnect] wird ausgeführt, wenn der Benutzer auf die Menüoption [Connect] tippt:
![]() |
Es steht alles in Zeile 40:
metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(...)
- Der beobachtete Prozess ist [metier.authenticate(user)];
- er wird auf einem I/O-Thread ausgeführt, der aus dem [Schedulers.io()]-Pool entnommen wird;
- er wird im UI-Thread beobachtet, der die Ereignisse der Swing-Oberfläche verarbeitet [observeOn(SwingScheduler.getInstance())]. Dieser Thread wird über die Methode [SwingScheduler.getInstance()] abgerufen, wobei [SwingScheduler] eine von der [RxSwing]-Bibliothek bereitgestellte Klasse ist. Dies ist zwingend erforderlich. Wenn das Ergebnis der asynchronen Operation vorliegt, wird es häufig verwendet, um Elemente der Swing-Oberfläche zu ändern. Die Swing-Oberfläche kann jedoch nur im UI-Thread geändert werden; andernfalls wird eine Ausnahme ausgelöst. Die Zeilen 41–61 müssen daher im UI-Thread ausgeführt werden. Dies wird hier durch die Methode [observeOn(SwingScheduler.getInstance())] sichergestellt;
Lassen Sie uns den Rest des Codes kommentieren:
- Zeilen 42–43: Diese Zeilen dienen dazu, die Syntax der Methode [subscribe] einzuhalten. Sie werden niemals ausgeführt, da der Prozess [metier.authenticate(user)] kein Ergebnis zurückgibt;
- Zeilen 35–52: Wenn eine Ausnahme empfangen wird, wird sie angezeigt;
- Zeilen 54–61: werden ausgeführt, wenn der Prozess [metier.authenticate(user)] das Ende seiner Emissionen signalisiert;
20.3.3. Die Klasse [ElectionsMainForm]
![]() |
20.3.3.1. Initialisierung der grafischen Benutzeroberfläche
package elections.security.client.swing;
import elections.security.client.console.IElectionsUI;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import elections.security.client.metier.IRxElectionsMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.schedulers.Schedulers;
import rx.schedulers.SwingScheduler;
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
@Component
public class ElectionsMainForm extends AbstractElectionsMainForm implements IElectionsUI {
private static final long serialVersionUID = 1L;
// reference to the asynchronous [business] layer
@Autowired
private IRxElectionsMetier metier;
// session UI
@Autowired
private UiSession uiSession;
// logged-in user
private User user;
// list templates JList
private DefaultListModel<String> modèleNomsVoix = null;
private DefaultListModel<String> modèleRésultats = null;
// competing lists
private ListeElectorale[] listes;
// user-entered lists
private final List<ListeElectorale> listesSaisies = new ArrayList<>();
private ListeElectorale[] tListesSaisies;
// initializations
@Override
protected void init() {
// generation of components by the parent class
super.init();
// form status
Utilitaires.setEnabled(new JLabel[]{jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer}, false);
Utilitaires.setEnabled(
new JMenuItem[]{jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer}, false);
// center window
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
Dimension frameSize = getSize();
if (frameSize.height > screenSize.height) {
frameSize.height = screenSize.height;
}
if (frameSize.width > screenSize.width) {
frameSize.width = screenSize.width;
}
setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2);
// logged-in user
user = uiSession.getUser();
// local initializations
modèleNomsVoix = new DefaultListModel<>();
jListNomsVoix.setModel(modèleNomsVoix);
modèleRésultats = new DefaultListModel<>();
jListResultats.setModel(modèleRésultats);
// lists are requested from the [business] layer
metier.getListesElectorales(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// answer
listesElectorales -> {
// memorize lists
listes = listesElectorales;
},
// exception
(th) -> showException(th),
// observable purpose
() -> {
// next step
doInitStep2();
});
}
...
- Zeile 46: Die Methode [init] wird ausgeführt, wenn das zugehörige Fenster angezeigt werden soll. Ihr Zweck ist es, die folgenden Komponenten [1-3] zu initialisieren:
![]() |
- Zeilen 71–85: Die Kandidatenlisten werden asynchron angefordert (Komponente [1]);
- Zeile 71: Der beobachtete Prozess ist [metier.getListesElectorales(user)]. Er wird auf einem I/O-Thread ausgeführt [subscribeOn(Schedulers.io())] und auf dem UI-Thread beobachtet [observeOn(SwingScheduler.getInstance())];
- Zeilen 74–77: Das vom beobachteten Prozess zurückgegebene Ergebnis wird im Feld [listes] in Zeile 38 gespeichert;
- Zeile 79: Jede Ausnahme wird von der folgenden Methode behandelt:
private void showException(Throwable th) {
// exception is displayed
jTextPaneMessages.setText(getInfoForException("Les erreurs suivantes se sont produites : ", th));
jTextPaneMessages.setCaretPosition(0);
}
- Zeilen 81–84: Am Ende des beobachteten Prozesses werden die Zeilen 81–84 ausgeführt. Diese Zeilen werden nicht ausgeführt, wenn eine Ausnahme aufgetreten ist. Die Methode [doInitStep2] führt Schritt 2 der Initialisierung wie folgt durch:
private void doInitStep2() {
// associate list names with the jComboBoxNomsListes combo
for (int i = 0; i < listes.length; i++) {
jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
}
// number of seats to be filled
metier.getNbSiegesAPourvoir(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// answer
nbSiegesAPourvoir -> {
// initialize the label linked to this information
jLabelSAP.setText(jLabelSAP.getText() + nbSiegesAPourvoir);
},
// exception
(th) -> showException(th),
// observable purpose
() -> {
// next step
doInitStep3();
});
}
- Zeilen 3–5: Wir verwenden das Ergebnis aus dem vorherigen Schritt, um die Dropdown-Liste mit den Namen der Kandidatenlisten zu füllen;
- Zeilen 7–20: Wir fordern die Anzahl der zu besetzenden Sitze asynchron an;
- Zeile 7: Der beobachtete Prozess ist [metier.getNbSiegesAPourvoir(user)]. Er wird auf einem I/O-Thread ausgeführt [subscribeOn(Schedulers.io())] und auf dem UI-Thread beobachtet [observeOn(SwingScheduler.getInstance())];
- Zeilen 10–13: Das vom Prozess zurückgegebene Ergebnis wird zur Aktualisierung der grafischen Benutzeroberfläche verwendet;
- Zeile 15: Eventuelle Ausnahmen werden angezeigt;
- Zeilen 17–20: Nach Erhalt des Endsignals vom Observable fahren wir mit Schritt 3 des Initialisierungsprozesses fort;
Schritt 3 der Initialisierung wird durch den folgenden Code abgewickelt:
private void doInitStep3() {
// electoral threshold
metier.getSeuilElectoral(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// answer
seuilElectoral -> {
// initialize the label linked to this information
jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
},
// exception
(th) -> showException(th),
// observable purpose
() -> {
});
}
- Zeilen 3–4: Die Wahlschwelle wird asynchron angefordert;
- Zeile 3: Der beobachtete Prozess ist [business.getVotingThreshold(user)]. Er wird auf einem I/O-Thread ausgeführt [subscribeOn(Schedulers.io())] und auf dem UI-Thread beobachtet [observeOn(SwingScheduler.getInstance())];
- Zeilen 6–9: Das vom Prozess zurückgegebene Ergebnis wird zur Aktualisierung der GUI verwendet;
- Zeile 11: Eventuelle Ausnahmen werden angezeigt;
- Zeilen 13–14: Nach Erhalt des Endsignals vom Observable wird keine Aktion ausgeführt: Der Initialisierungsprozess der GUI ist abgeschlossen;
20.3.3.2. Berechnung der von den verschiedenen Listen gewonnenen Sitze
Die Methode [doCalculer] ist für die Berechnung der Anzahl der von den verschiedenen Listen gewonnenen Sitze zuständig:
@Override
protected void doCalculer() {
tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
// calcul des sièges
String info = null;
metier.calculerSieges(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// traitement résultat
result -> consumeResultSieges(result),
// traitement exception
th -> showException(th),
// fin observable
() -> {
}
);
}
- Zeilen 6–15: Die durch die verschiedenen Listen erhaltenen Sitzplätze werden asynchron berechnet;
- Zeile 6: Der beobachtete Prozess ist [metier.calculateSeats(user, tListsEntered)]. Er wird auf einem I/O-Thread ausgeführt [subscribeOn(Schedulers.io())] und auf dem UI-Thread beobachtet [observeOn(SwingScheduler.getInstance())];
- Zeile 9: Das vom Prozess zurückgegebene Ergebnis wird von der Methode [consumeResultSeats] verwendet;
- Zeile 11: Eventuelle Ausnahmen werden angezeigt;
- Zeilen 13–14: Nach Erhalt des Endsignals vom Observablen wird keine Aktion ausgeführt;
Zeile 9: Die Methode [consumeResultSieges] verarbeitet das vom beobachteten Prozess zurückgegebene Ergebnis und aktualisiert die Kandidatenlisten mit ihren Feldern [seats, eliminated]:
private void consumeResultSieges(ListeElectorale[] tListesSaisies) {
// the result is stored
this.tListesSaisies = tListesSaisies;
// display of results
modèleRésultats.clear();
for (int i = 0; i < tListesSaisies.length; i++) {
modèleRésultats.addElement(tListesSaisies[i].toString());
}
// maj state form
Utilitaires.setEnabled(new JLabel[]{jLabelEnregistrer}, true);
Utilitaires.setEnabled(new JLabel[]{jLabelCalculer}, false);
Utilitaires.setEnabled(new JMenuItem[]{jMenuItemEnregistrer}, true);
Utilitaires.setEnabled(new JMenuItem[]{jMenuItemCalculer}, false);
jTextPaneMessages.setText("Calcul terminé");
}
- Zeilen 4–14: Das Ergebnis wird zur Aktualisierung der Benutzeroberfläche verwendet;
20.3.3.3. Speichern der Wahlergebnisse
Die Wahlergebnisse werden mit der folgenden [doEnregistrer]-Methode gespeichert:
@Override
protected void doEnregistrer() {
// on demande l'enregistrement à la couche [métier]
metier.recordResultats(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// traitement du résultat - il n'y en a pas ici
(param) -> {
},
// traitement de l'exception
(th) -> showException(th),
// fin observable
() -> {
// maj du formulaire
Utilitaires.setEnabled(new JLabel[]{jLabelEnregistrer}, false);
Utilitaires.setEnabled(new JMenuItem[]{jMenuItemEnregistrer}, false);
jTextPaneMessages.setText("Enregistrement des résultats réalisé");
}
);
}
- Zeilen 4–17: Die Wahlergebnisse werden asynchron gespeichert;
- Zeile 4: Der beobachtete Prozess ist [business.recordResults(user, tEnteredLists)]. Er wird auf einem E/A-Thread ausgeführt [subscribeOn(Schedulers.io())] und auf dem UI-Thread beobachtet [observeOn(SwingScheduler.getInstance())];
- Zeilen 7–8: Diese Zeilen werden niemals ausgeführt, da der beobachtete Prozess kein Ergebnis zurückgibt;
- Zeile 10: Jede Ausnahme wird angezeigt;
- Zeilen 14–16: Nach Erhalt des Endsignals vom Observable wird die GUI aktualisiert;
Aufgabe: Überprüfen Sie, ob die Swing-Anwendung funktioniert. Passen Sie anschließend die GUI und den Code so an, dass während einer asynchronen Operation mit dem Webserver/JSON ein Lade-Symbol erscheint, zusammen mit einer Option zum Abbrechen der aktuellen Operation.























