Skip to content

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:
T call(Subscriber<T> subscriber)

Um eine funktionale Schnittstelle m(param1, param2, ..., paramn) mit einer einzigen Methode zu implementieren, können Sie die folgende vereinfachte Syntax verwenden:

(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, 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.