Skip to content

20. Asynchrone Programmierung mit RxJava

Lesetipp: [Introduction à RxJava. Application aux environnements Swing et Android.]

In diesem Kapitel greifen wir Kapitel 17.6 wieder auf, in dem wir eine Client-Server-Anwendung mit folgender Architektur erstellt hatten:

Bestimmte Benutzeraktionen auf der Swing-Oberfläche in [1] lösen über ein Netzwerk (HTTP, [2]) Aktionen bis hin zur Datenbank in [3] aus. Aufgrund dessen kann es mehr oder weniger lange dauern, bis die Reaktion auf die Benutzeraktion erfolgt. Es wäre sinnvoll, auf der Benutzeroberfläche einen Warteknopf einzubinden, mit dem der gestartete Vorgang abgebrochen werden kann, falls dieser zu lange dauert. In Kapitel 17.6 erfolgt jede Benutzeraktion, die einen Informationsaustausch mit dem Server erfordert, synchron. Der vom Code ausgeführte Ereignis-Handler ist erst abgeschlossen, wenn die Antwort empfangen wurde. Während dieser gesamten Zeit ist die grafische Benutzeroberfläche eingefroren: Sie reagiert nicht auf neue Aktionen des Benutzers. Diese werden einfach in eine Warteschlange gestellt, um bearbeitet zu werden, sobald der aktuell ausgeführte Ereignis-Handler beendet ist. Würde man also eine Abbrechen-Schaltfläche anzeigen, könnte der Benutzer zwar darauf klicken, es würde jedoch nichts geschehen, solange der laufende Vorgang nicht abgeschlossen ist. Die Abbrechen-Schaltfläche hätte dann keinen Sinn.

Damit der Klick auf die Abbrechen-Schaltfläche Wirkung zeigt, muss der laufende Vorgang abgeschlossen sein. Dazu muss er den potenziell langwierigen Vorgang asynchron starten:

  • Der Ereignis-Handler startet den langwierigen Vorgang, wartet jedoch nicht auf dessen Ergebnis und übergibt die Kontrolle an den Thread von UI, der die Ereignisse der grafischen Benutzeroberfläche verwaltet. Der langwierige Vorgang wird auf einem anderen Thread als dem von UI gestartet, wodurch dieser nicht blockiert wird;
  • Wenn der Benutzer vor Abschluss des langwierigen Vorgangs auf die Schaltfläche „Abbrechen“ klickt, kann der derzeit nicht ausgelastete Thread von UI dieses Ereignis verarbeiten. Der langwierige Vorgang kann dann abgebrochen werden, ohne sein Ergebnis zu berücksichtigen;
  • Wurde der lang andauernde Vorgang nicht abgebrochen, löst das Eintreffen der Antwort ein Ereignis im Thread von UI aus. Ist dieser Thread nicht ausgelastet, führt er den mit diesem Ereignis verbundenen Code aus, der die Antwort verarbeitet;

Die Benutzeroberfläche funktioniert wie bisher. Sind die Antwortzeiten des Servers kurz, wird der Benutzer keinen Unterschied bemerken. Sind sie hingegen spürbar, erscheint für den Benutzer eine Abbrechen-Schaltfläche, über die er den laufenden Vorgang unterbrechen kann.

Die Bibliothek [Rx] ermöglicht asynchrone Programmierung. Ihr großer Vorteil liegt darin, dass sie in zahlreiche Umgebungen portiert wurde (Java, .NET, JS, ...) portiert wurde und dass die in einer Umgebung erworbenen Kenntnisse leicht auf eine andere Umgebung übertragen werden können. Wir stützen uns hier auf Kapitel 2 des Dokuments [Introduction à RxJava. Application aux environnements Swing et Android]. Der Leser wird gebeten, dieses zu lesen. Im Folgenden greifen wir auf Code aus den Beispielen dieses Kapitels zurück.

Wir werden die Architektur der Anwendung wie folgt weiterentwickeln:

  • in [1] fügen wir eine Schicht [RxJava] zwischen der Schicht [swing] und der Schicht [métier] ein. Die Methoden dieser Schicht werden fortan asynchron aufgerufen;

Wir gehen dabei in mehreren Schritten vor:

  • Schritt 1: Die Schicht [metier, DAO] verfügt derzeit über eine synchrone Schnittstelle zur Schicht [ui]. Wir werden sie in eine asynchrone Schicht [RxJava, metier, DAO] umwandeln;
  • Schritt 2: Wir werden die synchrone Konsolenanwendung in eine weiterhin synchrone Anwendung umwandeln, die jedoch die asynchrone Schnittstelle [RxJava, metier, DAO] nutzt;
  • Schritt 3: Wir werden die synchrone Swing-Anwendung in eine asynchrone Swing-Anwendung umwandeln;

20.1. Schritt 1

Wir wandeln die aktuelle synchrone Schicht [metier, DAO] in eine asynchrone Schicht [RxJava, metier, DAO] um.

20.1.1. Erstellung

Wir gehen vom Maven-Projekt aus Kapitel 17.4 aus, das wir mit NetBeans öffnen:

Wir duplizieren dieses Projekt [1] (Kopieren/Einfügen) in ein neues Projekt [elections-rxjava-metier-dao-security-webjson] [2].

20.1.2. Maven-Konfiguration

Wir passen die Datei [pom.xml] des neuen Projekts an, 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>
    <!-- von Spring verwendete Bibliothek jSON -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
    </dependency>
    <!-- von Spring verwendete Komponente 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>
    <!-- Protokollbibliothek -->
    <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 Bibliothek RxJava hinzugefügt;

20.1.3. Asynchrone Implementierung der Schicht [métier]

Um die Schicht [RxJava, métier] 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 der Schicht [RxJava, métier]. 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 {

  // Authentifizierung
  Observable<Void> authenticate(User user);

  // Wahllisten abrufen
  Observable<ListeElectorale[]> getListesElectorales(User user);

  // Anzahl der zu vergebenden Sitze
  Observable<Integer> getNbSiegesAPourvoir(User user);

  // Wahlhürde
  Observable<Double> getSeuilElectoral(User user);

  // Erfassung der Ergebnisse
  Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales);

  // Berechnung der Sitze
  Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales);

}

Die Schnittstelle [IRxElectionsMetier] übernimmt die Methoden der Schnittstelle [IElectionsMetier], doch während eine Methode M der Schnittstelle [IElectionsMetier] ein Ergebnis vom Typ T zurückgab, gibt die Methode M der Schnittstelle [IRxElectionsMetier] ein Ergebnis vom Typ Observable<T> zurück. Der Typ [Observable] wird von der Bibliothek RxJava bereitgestellt. Ein Typ „Observable<T>“ 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 liefern;
  • 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 die mit dem Observable verbundene asynchrone Operation nicht gestartet. 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 Implementierung [RxElectionsMetier] der Schnittstelle [IRxElectionsMetier] 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 {
        // Aufruf der synchronen Methode und anschließende Antwort an den Abonnenten
        subscriber.onNext(metier.getListesElectorales(user));
        // das Ende des Beobachtbaren wird gemeldet
        subscriber.onCompleted();
      } catch (Exception e) {
        // Die Ausnahme wird weitergeleitet
        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äftslogik;
  • Zeilen 20–34: Wir werden die Methode [getListesElectorales] auskommentieren, die anstelle eines Typs [ListeElectorale[]] einen Typ [Observable<ListeElectorale[]>] zurückgibt;
  • Zeilen 22–32: Die statische Methode [Observable.create] ermöglicht es, aus einem Typ [Subscriber] ein Observable zu erstellen. Der Typ [Subscriber] stellt einen Abonnenten der vom beobachteten Prozess (dem Observable) erzeugten Ergebnis-Streams dar. Er stellt drei Methoden bereit:
    • [Subscriber.onNext] (Zeile 25) zum Empfangen eines Ergebnisses des beobachteten Prozesses;
    • [Subscriber.onError] (Zeile 30) zum Empfangen einer Ausnahme des beobachteten Prozesses. Nach einer Ausnahme sendet der Typ [Observable] keine Ergebnisse mehr;
    • [Subscriber.onCompleted] (Zeile 27) zum Empfangen des Signals „Ende der Emission“ des beobachteten Prozesses. Hier emittiert der beobachtete Prozess nur ein Element. Dabei ist zu beachten, dass dieses Signal nicht emittiert wird, wenn eine Ausnahme auftritt. Dies ist das Standardverhalten von Observables: Das Auslösen einer Ausnahme signalisiert gleichzeitig das Ende der Emissionen. Die Abonnenten wissen das;
  • Zeilen 22–34: Die Methode [Observable.create] akzeptiert als Parameter einen Typ [Observable.OnSubscribe]. Dieser Typ ist eine funktionale Schnittstelle. Dieser Begriff wurde mit Java 8 eingeführt und bezeichnet eine Schnittstelle mit einer einzigen Methode. Hier lautet die einzige Methode der Schnittstelle [Observable.OnSubscribe] wie folgt:
T call(Subscriber<T> subscriber)

Um eine funktionale Schnittstelle mit einer einzigen Methode m(param1, param2, ..., paramn) zu implementieren, kann die folgende vereinfachte Syntax verwendet werden:

(param1, param2, ..., paramn) -> { code de la méthode m}

Dies geschieht in den Zeilen 22–34:

  • [subscriber] ist der Parameter der Methode [Observable.OnSubscribe.call];
  • Zeilen 23–32: der Code, der der Methode [call] zugewiesen werden soll;
  • Zeile 25: Die Wählerlisten werden synchron von der in Zeile 13 eingebundenen Schicht [métier] angefordert. Es wird also auf das Ergebnis gewartet. Sobald dieses empfangen wird, wird es an die Methode [onNext] des Abonnenten übergeben;
  • Zeile 28: Im Fehlerfall wird die Ausnahme an die Methode [onError] des Abonnenten übergeben;
  • Zeile 31: Es wird nur auf ein Ergebnis gewartet. Sobald dieses vorliegt (die Wählerlisten oder eine Ausnahme), wird dem Abonnenten mitgeteilt, dass der beobachtete Prozess keine weiteren Ergebnisse mehr ausgibt;

Es sei daran erinnert, dass die Methode [RxElectionsMetier] einen Typ Observable<ListeElectorale[]> zurückgibt und nicht den Typ ListeElectorale[] selbst. Der aufrufende Code muss die Methode Observable&lt;ListeElectorale[]&gt;.**subscribe** aufrufen, damit der Code in den Zeilen 23–33 ausgeführt wird und die Wählerlisten über Zeile 25 zurückgibt.

Der Code der anderen Methoden ist analog:


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 {
        // synchroner Methodenaufruf
        metier.authenticate(user);
        // Das Ende des Observables wird gemeldet
        subscriber.onCompleted();
      } catch (Exception e) {
        // Die Ausnahme wird weitergeleitet
        subscriber.onError(e);
      }
    });

  }

  @Override
  public Observable<ListeElectorale[]> getListesElectorales(User user) {
    return Observable.create(subscriber -> {
      try {
        // Aufruf einer synchronen Methode, anschließend Antwort an den Abonnenten
        subscriber.onNext(metier.getListesElectorales(user));
        // Das Ende des Observables wird gemeldet
        subscriber.onCompleted();
      } catch (Exception e) {
        // Die Ausnahme wird weitergeleitet
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Integer> getNbSiegesAPourvoir(User user) {
    return Observable.create(subscriber -> {
      try {
        // Aufruf einer synchronen Methode, anschließend Antwort an den Abonnenten
        subscriber.onNext(metier.getNbSiegesAPourvoir(user));
        // Das Ende des Observables wird gemeldet
        subscriber.onCompleted();
      } catch (Exception e) {
        // Die Ausnahme wird weitergeleitet
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Double> getSeuilElectoral(User user) {
    return Observable.create(subscriber -> {
      try {
        // Aufruf einer synchronen Methode, anschließend Antwort an den Abonnenten
        subscriber.onNext(metier.getSeuilElectoral(user));
        // Das Ende des Observables wird gemeldet
        subscriber.onCompleted();
      } catch (Exception e) {
        // Die Ausnahme wird weitergeleitet
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
    return Observable.create(subscriber -> {
      try {
        // Aufruf einer synchronen Methode
        metier.recordResultats(user, listesElectorales);
        // Das Ende des Observables wird gemeldet
        subscriber.onCompleted();
      } catch (Exception e) {
        // Die Ausnahme wird weitergeleitet
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
    return Observable.create(subscriber -> {
      try {
        // Aufruf einer synchronen Methode, anschließend Antwort an den Abonnenten
        subscriber.onNext(metier.calculerSieges(user, listesElectorales));
        // Das Ende des Observables wird gemeldet
        subscriber.onCompleted();
      } catch (Exception e) {
        // Die Ausnahme wird weitergeleitet
        subscriber.onError(e);
      }
    });
  }
}
  • Zeilen 20 und 81: Die Methode [onNext] des Abonnenten wird nicht aufgerufen, da dieser keine Ergebnisse erwartet;

20.1.4. Die Tests JUnit der Schicht [métier]

  

20.1.4.1. Test01

Wir greifen den in Abschnitt 17.4.4 behandelten Unit-Test [Test01] wieder auf. Er wurde dafür vorgesehen, synchrone Aufrufe an die Schnittstelle [IElectionsMetier] durchzuführen. Wir passen ihn so an, dass er synchrone Aufrufe an die neue Schnittstelle [IRxElectionsMetier] durchführt. Es ist nämlich möglich, synchrone Aufrufe an eine asynchrone Schnittstelle RxJava durchzuführen. 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 {

  // Schicht [electionsMetier]
  @Autowired
  private IRxElectionsMetier electionsMetier;

  // Mapper jSON
  private final ObjectMapper mapper = new ObjectMapper();

  // Benutzer
  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() {
    // Es wird die Tabelle mit den 7 Kandidatenlisten erstellt
    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);
    // die Sitze jeder Liste werden berechnet
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    // die Ergebnisse werden überprüft
    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() {
    // Es wird eine Tabelle mit den 7 Kandidatenlisten erstellt
    ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
    // Die Stimmen werden festgeschrieben
    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);
    // Die Sitze, die jede der Listen erhalten hat, werden berechnet
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    // Die Ergebnisse werden überprüft
    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() {
    // Es wird eine Tabelle mit 24 Kandidatenlisten erstellt, die jeweils 1 Stimme erhalten
    ListeElectorale[] listes = new ListeElectorale[25];
    // Alle 25 Listen erhalten die gleiche Stimmenzahl (4 %)
    for (int i = 0; i < listes.length; i++) {
      listes[i] = new ListeElectorale("Liste" + (i + 1), 1, 0, false);
    }
    // Sitzberechnung – normalerweise sollte man ein ElectionsException erhalten
    // bei einer Wahlhürde von 5 %
    BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
  }

  /**
   * enregistrement des résultats de l'élection
   *
   * @throws JsonProcessingException
   */
  @Test
  public void ecritureResultatsElections() throws JsonProcessingException {
    // Es wird eine Tabelle mit den 7 Kandidatenlisten erstellt
    ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
    // die Stimmen werden fest vorgegeben
    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);
    // die von jeder Liste erzielten Sitze werden berechnet
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    // Die Ergebnisse werden angezeigt
    for (int i = 0; i < listes.length; i++) {
      System.out.println(mapper.writeValueAsString(listes[i]));
    }
    // Die Ergebnisse werden in der Datenbank gespeichert
    BlockingObservable.from(electionsMetier.recordResultats(admin, listes)).firstOrDefault(null);
    // Die Ergebnisse werden überprüft
    listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
    // Die Ergebnisse werden angezeigt
    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 den Beobachtbaren-Parameter von [from];
    • löst die Ausführung des mit dem Observable verknüpften 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 seiner Ausführung kein Ergebnis zurückgibt. Das Ergebnis der Methode [firstOrDefault(null)] ist somit null, ein Wert, der hier nicht weiterverwendet wird;

Wir übernehmen dieses Schema im restlichen Code jedes Mal, wenn wir die Schicht [métier] aufrufen wollen.

Der Unit-Test [Test01] muss erfolgreich sein:

 

Zu erledigende Aufgabe: Überprüfen, ob der Test [Test01] erfolgreich ist.


20.1.4.2. Test02

Wir passen den Test [Test01] so an, dass er nun die asynchrone Schnittstelle [IRxElectionsMetier] testet, indem er asynchrone Aufrufe an deren Methoden durchführt.

Betrachten wir einen ersten Test:


  // Semaphor zur Thread-Synchronisation
  private CountDownLatch latch;

  // -----------------------------------
  private ElectionsException checkUserUserException;

  @Test()
  public void checkUserUser() throws InterruptedException {
    // Semaphor mit Wert 1
    latch = new CountDownLatch((1));
    // asynchrone Operation
    electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
            .subscribe((result) -> {
            },
                    (th) -> {
                      checkUserUserException = (ElectionsException) th;
                      latch.countDown();
                    },
                    () -> {
                      latch.countDown();
                    });
    // Warten auf Semaphor
    latch.await();
    // Ergebnisüberprüfung
    Assert.assertNotNull(checkUserUserException);
    Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
  • Zeile 2: Ein Semaphor ist ein Werkzeug, das zur Synchronisation von Threads untereinander verwendet wird. Threads sind parallel ablaufende Ausführungsströme. Um eine Aufgabe T1 auszuführen, muss der Thread [Thread1] möglicherweise warten, bis eine Aufgabe T2, die von einem Thread [Thread2] ausgeführt wird, abgeschlossen ist. Er wartet daher darauf, dass der Thread [Thread2] ihm ein Signal sendet, das anzeigt, dass die Aufgabe T2 beendet ist. Es gibt verschiedene Möglichkeiten, diese Synchronisation zwischen zwei Threads zu handhaben. Die hier verwendete Methode ist die folgende:
    • Zeile 10: Der Thread [Thread1] erstellt ein Semaphor mit dem Wert 1;
    • Zeile 12: Der Thread [Thread1] erstellt und startet einen Thread [Thread2]. Dies wird durch folgende 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 Bibliothek RxJava stellt mehrere davon 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 den synchronen Vorgang aus, der im Observable [authenticate(user)] gekapselt ist. Da diese synchrone Operation jedoch in einem anderen Thread als dem Thread [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 wechselt (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 zwar etwas ausführt, aber kein Ergebnis ausgibt. Das Lambda [(result)->{}] wird daher niemals aufgerufen. Deshalb ist sein Code hier leer: [{}];
    • das zweite Lambda [(th)->{...}] erhält als Parameter einen Typ [Throwable]. Es wird aufgerufen, wenn bei der Ausführung des Observables eine Ausnahme auftritt. Hier verarbeiten 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 das Semaphor auf 0, um anzuzeigen, dass der Thread [Thread2] seine Arbeit beendet hat;
    • Das dritte Observable [()->{...}] wird aufgerufen, wenn das Observable keine Elemente mehr auszugeben hat. Wir behandeln dieses Ereignis wie folgt:
      • Zeile 20: Wir setzen den Semaphor auf 0, um anzuzeigen, dass der Thread [Thread2] seine Arbeit beendet hat;

Es ist zu beachten, dass das dritte Lambda-Ausdruck nicht aufgerufen wird, wenn eine Ausnahme auftritt. Aus diesem Grund mussten wir das Semaphor bereits 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 Überprüfungen durchführen wie im Test [Test01];

Betrachten wir einen weiteren Test:


// -----------------------------------
  private ElectionsException calculSieges1Exception;
  private ListeElectorale[] listesCalculSieges1;

  @Test
  public void calculSieges1() throws InterruptedException {
    // Erstellung des Arrays mit den 7 Kandidatenlisten
    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);
    // Semaphor auf 1
    latch = new CountDownLatch((1));
    // asynchrone Operation    
    // Berechnung der Sitze für jede der Listen
    electionsMetier.calculerSieges(admin, listes).subscribeOn(Schedulers.io())
            .subscribe((result) -> {
              listesCalculSieges1 = result;
            },
                    (th) -> {
                      calculSieges1Exception = (ElectionsException) th;
                      latch.countDown();
                    },
                    () -> {
                      latch.countDown();
                    });
    // Warten auf Semaphor
    latch.await();
    // Die Ergebnisse werden überprüft
    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.calculerSieges(admin, listes)];
  • Zeilen 21–23: Die Ausführung des Observables liefert einen Typ [ListeElectorale[]] zurück, der in einem Feld der Testklasse in Zeile 3 gespeichert wird;
  • Zeilen 34–48: Diese Prüfungen entsprechen denen des Tests [Test01], ergänzt um die Prüfung in Zeile 34, die sicherstellt, dass keine Ausnahme aufgetreten ist;

Der gesamte Test [Test02] ist in den Kursunterlagen verfügbar.


Aufgabe: Führen Sie den Test [Test02] durch 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] durch synchrone Aufrufe dieser 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 ausgeführt werden. Wenn der Thread [Thread1] die Methode [subscribe] eines Observables ausführt, startet diese eine Operation HTTP zum Server, ebenfalls auf dem Thread [Thread1]. Die gesamte Methode [subscribe] wird dadurch synchron;
  • da nun nur noch ein Thread vorhanden ist, wird die Thread-Synchronisation überflüssig und das Semaphor verschwindet;

Hier sind zwei Testbeispiele:


  // -----------------------------------
  private ElectionsException checkUserUserException;

  @Test()
  public void checkUserUser() throws InterruptedException {
    // synchrone Operation
    electionsMetier.authenticate(user)
            .subscribe((result) -> {
            },
                    (th) -> {
                      checkUserUserException = (ElectionsException) th;
                    },
                    () -> {
                    });
    // Überprüfung der Ergebnisse
    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 eine synchrone Operation;

  // -----------------------------------
  private ElectionsException calculSieges1Exception;
  private ListeElectorale[] listesCalculSieges1;

  @Test
  public void calculSieges1() throws InterruptedException {
    // Es wird die Tabelle mit den 7 Kandidatenlisten erstellt
    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);
    // synchrone Operation    
    // Die Sitze jeder Liste werden berechnet
    electionsMetier.calculerSieges(admin, listes)
            .subscribe((result) -> {
              listesCalculSieges1 = result;
            },
                    (th) -> {
                      calculSieges1Exception = (ElectionsException) th;
                    },
                    () -> {
                    });
    // Überprüfung der Ergebnisse
    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] durch und überprüfen Sie, ob er erfolgreich ist.


20.2. Schritt 2

Wir portieren nun die synchrone Konsolenanwendung aus Kapitel 17.5 in eine weiterhin synchrone Anwendung, die jedoch die asynchrone Schnittstelle [RxJava, metier, DAO] verwendet;

Wir gehen von dem Projekt [elections-console-metier-dao-security-webjson] [1] aus Kapitel 17.5 aus, das wir in ein neues Projekt [elections-console-rxjava- metier-dao-security-webjson] [2] duplizieren:

  • in [3-4]; im neuen Projekt wird die Abhängigkeit von der alten synchronen Ebene [métier] entfernt;
  • in [5-9] wird eine Abhängigkeit von der neuen asynchronen Schicht [métier] hinzugefügt;
  • In [10-14] wird die Klasse [ElectionsConsole] in [ElectionsConsole01] umbenannt;

Ebenso wird die Klasse [BootElectionsConsole] in [BootElectionsConsole01] umbenannt:

 

Der aktuelle Code der 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 der Name der Klasse [ElectionsConsole] in [ElectionsConsole01] geändert wurde, muss nun Folgendes geschrieben werden:

        return ctx.getBean("electionsConsole01",IElectionsUI.class);

Kehren wir zum Code der Klasse [ElectionsConsole01] zurück:


@Component
public class ElectionsConsole01 implements IElectionsUI {

    @Autowired
    private IElectionsMetier electionsMetier;

  @Autowired
  private User admin;
  
    @Override
    public void run() {
        // Die konkurrierenden Listen
        ListeElectorale[] listes;
        // Dateneingabe
        try (Scanner clavier = new Scanner(System.in)) {
         // Die im Wettbewerb stehenden Listen werden von der Ebene [metier] angefordert
         listes = electionsMetier.getListesElectorales(admin);
            ...
        // Die Sitzverteilung wird berechnet
        listes=electionsMetier.calculerSieges(admin,listes);
        // Die Ergebnisse werden gespeichert
        electionsMetier.recordResultats(admin,listes);
        ...
}

Folgt man dem Beispiel des Tests [Test01] aus Abschnitt 20.1.4.1, ä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() {
    // die Listen im Wettbewerb
    ListeElectorale[] listes;
    // Dateneingabe
    try (Scanner clavier = new Scanner(System.in)) {
      // die konkurrierenden Listen werden von der Ebene [metier] angefordert
      listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
      ...
    // Berechnung der Sitze
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    // Die Ergebnisse werden gespeichert
    BlockingObservable.from(electionsMetier.recordResultats(admin, listes));
    ...
  }

Aufgabe: Konfigurieren Sie das Projekt so, dass die Klasse [BootElectionsConsole01] mit den drei Parametern [SS, Heures travaillées, Jours travaillés] ausgeführt wird, und überprüfen Sie, ob die Ausführung des so konfigurierten Projekts 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] aus 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] aus Abschnitt 20.1.4.3 geschrieben wurde.


20.3. Schritt 3

Wir wenden uns nun der Portierung der Swing-Anwendung in eine asynchrone Umgebung zu.

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 Schicht [console];
  • in [5-9] fügen wir eine Abhängigkeit zur asynchronen Konsolenschicht hinzu;

Die Schicht [swing] führt echte asynchrone Aufrufe an die Schicht [métier] durch. Beim Aufruf einer Methode dieser Schicht gibt es zwei Threads:

  • der Thread von UI, der die Ereignisse verarbeitet;
  • ein E/A-Thread, der den Aufruf von HTTP an den Server ausführt;

Während der gesamten Dauer des asynchronen Aufrufs sollten wir ein Ladebild sowie eine Schaltfläche zum Abbrechen anzeigen. Dies werden wir hier jedoch nicht tun; es wird Ihnen als Verbesserung der Anwendung vorgeschlagen. Die Änderungen erfolgen in den beiden Klassen, die Aufrufe an die Schicht [métier] ausführen:

 

20.3.1. Maven-Konfiguration

Wir werden hier die Bibliothek [RxSwing] verwenden, die die Bibliothek [RxJava] 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>
    <!-- untere Schichten -->
    <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]

Im asynchronen Betrieb 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;

  // Referenz zur asynchronen Schicht [métier]
  @Autowired
  private IRxElectionsMetier metier;

  // angemeldeter Benutzer
  private User user;

  // Hauptformular
  @Autowired
  private ElectionsMainForm electionsMainForm;

  // Sitzung UI
  @Autowired
  private UiSession uiSession;

  @Override
  protected void doConnect() {
    if (isPageValid()) {
      // Benutzerauthentifizierung
      metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(
        // keine Antwort
        (result) -> {
        },
        // Ausnahmebehandlung
        (th) -> {
          // Fehler wird protokolliert
          String info = getInfoForException("Les erreurs suivantes se sont produites :", th);
          // Information wird angezeigt
          jTextPaneErreurs.setText(info);
          jTextPaneErreurs.setCaretPosition(0);

        },
        // Die Authentifizierung ist abgeschlossen
        () -> {
          // Der Benutzer wird in der Sitzung gespeichert
          uiSession.setUser(user);
          // Die Anmeldeseite wird ausgeblendet
          setVisible(false);
          // Die Hauptansicht wird angezeigt
          electionsMainForm.run();
        });
    }
  }

  // Initialisierungen
  @Override
  protected void init() {
    ...
  }

  @Override
  public void run() {
    // Die grafische Benutzeroberfläche wird angezeigt
    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 die Menüoption [Connexion] auswählt:
 

Alles steht in Zeile 40:


      metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(...)
  • Der beobachtete Prozess ist [metier.authenticate(user)];
  • er wird auf einem E/A-Thread aus dem Pool [Schedulers.io()] ausgeführt;
  • er wird im Thread von UI beobachtet, der die Ereignisse der Swing-Schnittstelle [observeOn(SwingScheduler.getInstance())] verwaltet. Dieser Thread wird über die Methode [SwingScheduler.getInstance()] abgerufen, wobei [SwingScheduler] eine von der Bibliothek [RxSwing] bereitgestellte Klasse ist. Dies ist zwingend erforderlich. Wenn das Ergebnis der asynchronen Operation vorliegt, wird dieses häufig verwendet, um Elemente der Swing-Oberfläche zu ändern. Diese darf jedoch nur im Thread von UI geändert werden, andernfalls wird eine Ausnahme ausgelöst. Daher müssen die Zeilen 41–61 im Thread von UI ausgeführt werden. Dies wird hier durch die Methode [observeOn(SwingScheduler.getInstance())] sichergestellt;

Erläutern wir nun den Rest des Codes:

  • 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: Bei Empfang einer Ausnahme wird diese angezeigt;
  • Zeilen 54–61: Diese Zeilen werden ausgeführt, wenn der Prozess [metier.authenticate(user)] das Ende seiner Ausgabe meldet;

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;

  // Verweis auf die asynchrone Schicht [métier]
  @Autowired
  private IRxElectionsMetier metier;

  // Sitzung UI
  @Autowired
  private UiSession uiSession;

  // angemeldeter Benutzer
  private User user;

  // Listenvorlagen JList
  private DefaultListModel<String> modèleNomsVoix = null;
  private DefaultListModel<String> modèleRésultats = null;

  // die Listen im Wettbewerb
  private ListeElectorale[] listes;

  // Vom Benutzer eingegebene Listen
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;

  // Initialisierungen
  @Override
  protected void init() {
    // Generierung der Komponenten durch die übergeordnete Klasse
    super.init();
    // Formularstatus
    Utilitaires.setEnabled(new JLabel[]{jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer}, false);
    Utilitaires.setEnabled(
            new JMenuItem[]{jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer}, false);
    // Fenster zentrieren
    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);
    // angemeldeter Benutzer
    user = uiSession.getUser();
    // lokale Initialisierungen
    modèleNomsVoix = new DefaultListModel<>();
    jListNomsVoix.setModel(modèleNomsVoix);
    modèleRésultats = new DefaultListModel<>();
    jListResultats.setModel(modèleRésultats);
    // Listen werden von der Schicht [métier] angefordert
    metier.getListesElectorales(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // Antwort
                    listesElectorales -> {
                      // Die Listen werden gespeichert
                      listes = listesElectorales;
                    },
                    // Ausnahme
                    (th) -> showException(th),
                    // beobachtbares Ende
                    () -> {
                      // nächster Schritt
                      doInitStep2();
                    });
  }
...
  • Zeile 46: Die Methode [init] wird ausgeführt, wenn das zugehörige Fenster angezeigt wird. Sie dient dazu, 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 E/A-Thread [subscribeOn(Schedulers.io())] ausgeführt und auf dem Thread UI [observeOn(SwingScheduler.getInstance()] beobachtet;
  • Zeilen 74–77: Das vom beobachteten Prozess zurückgegebene Ergebnis wird im Feld [listes] in Zeile 38 gespeichert;
  • Zeile 79: Eine eventuell auftretende Ausnahme wird von der folgenden Methode behandelt:

  private void showException(Throwable th) {
    // die Ausnahme wird angezeigt
    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() {
    // Die Namen der Listen werden dem Kombinationsfeld zugeordnet jComboBoxNomsListes
    for (int i = 0; i < listes.length; i++) {
      jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
    }
    // Anzahl der zu besetzenden Sitze
    metier.getNbSiegesAPourvoir(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // Antwort
                    nbSiegesAPourvoir -> {
                      // Die mit dieser Information verknüpfte Beschriftung wird initialisiert
                      jLabelSAP.setText(jLabelSAP.getText() + nbSiegesAPourvoir);
                    },
                    // Ausnahme
                    (th) -> showException(th),
                    // Beobachtbares Ende
                    () -> {
                      // nächster Schritt
                      doInitStep3();
                    });
}
  • Zeilen 3–5: Das Ergebnis des vorherigen Schritts wird verwendet, um die Dropdown-Liste mit den Namen der Kandidatenlisten zu füllen;
  • Zeilen 7–20: Die Anzahl der zu besetzenden Sitze wird asynchron abgefragt;
  • Zeile 7: Der beobachtete Prozess ist [metier.getNbSiegesAPourvoir(user)]. Er wird auf einem E/A-Thread [subscribeOn(Schedulers.io())] ausgeführt und auf dem Thread UI [observeOn(SwingScheduler.getInstance()] beobachtet;
  • Zeilen 10–13: Das vom Prozess zurückgegebene Ergebnis wird zur Aktualisierung der grafischen Benutzeroberfläche verwendet;
  • Zeile 15: Eine eventuell aufgetretene Ausnahme wird angezeigt;
  • Zeilen 17–20: Nach Empfang des Endsignals des Observables wird zu Schritt 3 des Initialisierungsprozesses übergegangen;

Schritt 3 der Initialisierung wird durch den folgenden Code ausgeführt:


  private void doInitStep3() {
    // Wahlhürde
    metier.getSeuilElectoral(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // Antwort
                    seuilElectoral -> {
                      // Das mit dieser Information verknüpfte Label wird initialisiert
                      jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
                    },
                    // Ausnahme
                    (th) -> showException(th),
                    // Beobachtbares Ende
                    () -> {
                    });
}
  • Zeilen 3–4: Die Wahlschwelle wird asynchron abgefragt;
  • Zeile 3: Der beobachtete Prozess ist [metier.getSeuilElectoral(user)]. Er wird auf einem E/A-Thread [subscribeOn(Schedulers.io())] ausgeführt und auf dem Thread UI [observeOn(SwingScheduler.getInstance()] beobachtet;
  • Zeilen 6–9: Das vom Prozess zurückgegebene Ergebnis wird zur Aktualisierung der grafischen Benutzeroberfläche verwendet;
  • Zeile 11: Eine eventuell aufgetretene Ausnahme wird angezeigt;
  • Zeilen 13–14: Beim Empfang des Endsignals des Observables wird nichts unternommen: Der Initialisierungsprozess der grafischen Benutzeroberfläche ist abgeschlossen;

20.3.3.2. Berechnung der von den verschiedenen Listen erzielten Sitze

Die Methode [doCalculer] dient dazu, die Anzahl der von den verschiedenen Listen gewonnenen Sitze zu berechnen:


  @Override
  protected void doCalculer() {
    tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
    // Berechnung der Sitze
    String info = null;
    metier.calculerSieges(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // Ergebnisverarbeitung
                    result -> consumeResultSieges(result),
                    // Ausnahmebehandlung
                    th -> showException(th),
                    // beobachtbares Ende
                    () -> {
                    }
            );
}
  • Zeilen 6–15: Die von den verschiedenen Listen gewonnenen Sitze werden asynchron berechnet;
  • Zeile 6: Der beobachtete Prozess ist [metier.calculerSieges(user, tListesSaisies)]. Er wird auf einem E/A-Thread [subscribeOn(Schedulers.io())] ausgeführt und auf dem Thread von UI [observeOn(SwingScheduler.getInstance()] beobachtet;
  • Zeile 9: Das vom Prozess zurückgegebene Ergebnis wird von der Methode [consumeResultSieges] verwendet;
  • Zeile 11: Eine eventuell auftretende Ausnahme wird angezeigt;
  • Zeilen 13–14: Beim Empfang des Endsignals des Observables wird nichts unternommen;

Zeile 9: Die Methode [consumeResultSieges] verarbeitet das vom beobachteten Prozess zurückgegebene Ergebnis sowie die Kandidatenlisten mit ihren aktualisierten Feldern [sieges, elimine]:


  private void consumeResultSieges(ListeElectorale[] tListesSaisies) {
    // Ergebnis wird gespeichert
    this.tListesSaisies = tListesSaisies;
    // Anzeige der Ergebnisse
    modèleRésultats.clear();
    for (int i = 0; i < tListesSaisies.length; i++) {
      modèleRésultats.addElement(tListesSaisies[i].toString());
    }
    // Formularstatus aktualisieren
    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 erhaltene Ergebnis wird zur Aktualisierung der grafischen Benutzeroberfläche verwendet;

20.3.3.3. Erfassung der Wahlergebnisse

Die Speicherung der Wahlergebnisse erfolgt über die folgende Methode [doEnregistrer]:


  @Override
  protected void doEnregistrer() {
    // Speicheranforderung an die Schicht [métier]
    metier.recordResultats(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // Ergebnisverarbeitung – hier gibt es kein Ergebnis
                    (param) -> {
                    },
                    // Ausnahmebehandlung
                    (th) -> showException(th),
                    // Beobachtbares Ende
                    () -> {
                      // Aktualisierung des Formulars
                      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 [metier.recordResultats(user, tListesSaisies)]. Er wird auf einem E/A-Thread [subscribeOn(Schedulers.io())] ausgeführt und auf dem Thread UI [observeOn(SwingScheduler.getInstance()] beobachtet;
  • Zeilen 7–8: Diese Zeilen werden niemals ausgeführt, da der beobachtete Prozess kein Ergebnis zurückgibt;
  • Zeile 10: Eine eventuell auftretende Ausnahme wird angezeigt;
  • Zeilen 14–16: Nach Empfang des Endsignals des Observables wird die grafische Benutzeroberfläche aktualisiert;

Aufgabe: Überprüfen Sie, ob die Swing-Anwendung funktioniert. Passen Sie anschließend die grafische Benutzeroberfläche und den Code so an, dass bei einer asynchronen Operation mit dem Webserver / jSON ein Ladebild sowie eine Option zum Abbrechen der laufenden Operation angezeigt werden.