20. Programowanie asynchroniczne z wykorzystaniem RxJava
Dokument do przeczytania: [Introduction à RxJava. Application aux environnements Swing et Android.]
W niniejszym rozdziale powracamy do rozdziału 17.6, w którym stworzyliśmy aplikację klient-serwer o następującej architekturze:
![]() |
Niektóre działania użytkownika w interfejsie Swing w [1] wyzwalają działania sięgające aż do bazy danych w [3] poprzez sieć HTTP [2]. Z tego powodu odpowiedź na działanie użytkownika może pojawić się z mniejszym lub większym opóźnieniem. Dobrze byłoby umieścić na interfejsie użytkownika wskaźnik oczekiwania wraz z opcją anulowania rozpoczętej operacji, gdyby trwała ona zbyt długo. W rozdziale 17.6 każda akcja użytkownika wymagająca wymiany informacji z serwerem jest synchroniczna. Obsługa zdarzenia wykonywana przez kod kończy się dopiero po otrzymaniu odpowiedzi. Przez cały ten czas interfejs graficzny jest zawieszony: nie reaguje na nowe działania użytkownika. Działania te są po prostu umieszczane w kolejce, aby zostały przetworzone po zakończeniu aktualnie wykonywanego obsługi zdarzeń. Tak więc, gdyby wyświetlono przycisk anulowania, użytkownik mógłby go kliknąć, ale nic by się nie stało, dopóki trwająca operacja nie zostałaby zakończona. Przycisk anulowania nie miałby wówczas żadnego sensu.
Aby kliknięcie przycisku anulowania przyniosło efekt, bieżąca operacja musi zostać zakończona. W tym celu musi ona uruchomić potencjalnie długotrwałą operację w trybie asynchronicznym:
- obsługa zdarzenia uruchamia długotrwałą operację, ale nie czeka na jej wynik i przekazuje kontrolę wątkowi UI, który zarządza zdarzeniami interfejsu graficznego. Długotrwała operacja jest uruchamiana w wątku innym niż wątek UI, co nie blokuje tego ostatniego;
- jeśli użytkownik kliknie przycisk anulowania przed zakończeniem długotrwałej operacji, wątek UI, który jest w tym momencie wolny, może obsłużyć to zdarzenie. Można wówczas przerwać długotrwałą operację, ignorując jej wynik;
- jeśli długotrwała operacja nie została anulowana, nadejście odpowiedzi wywoła zdarzenie w wątku UI. Jeśli wątek ten jest wolny, wykona kod powiązany z tym zdarzeniem, który przetworzy odpowiedź;
Interfejs użytkownika będzie działał tak jak dotychczas. Jeśli czas odpowiedzi serwera jest krótki, użytkownik nie zauważy różnicy. Jeśli jednak opóźnienie jest zauważalne, użytkownik zobaczy przycisk anulowania i będzie miał możliwość przerwania trwającej operacji.
Biblioteka [Rx] umożliwia programowanie asynchroniczne. Jej wielką zaletą jest to, że została przeniesiona do wielu środowisk (Java, .NET, JS, ...) oraz że wiedza zdobyta w jednym środowisku może być łatwo przeniesiona do innego. Będziemy się tutaj opierać na rozdziale 2 dokumentu [Introduction à RxJava. Application aux environnements Swing et Android]. Zachęcamy czytelnika do zapoznania się z nim. W dalszej części wykorzystamy kod pochodzący z przykładów zawartych w tym rozdziale.
Będziemy rozwijać architekturę aplikacji w następujący sposób:
![]() |
- w [1] wstawiamy warstwę [RxJava] pomiędzy warstwą [swing] a warstwą [métier]. Metody tej warstwy będą odtąd wywoływane asynchronicznie;
Postępujemy w kilku etapach:
- etap 1: warstwa [metier, DAO] posiada obecnie interfejs synchroniczny względem warstwy [ui]. Przekształcimy ją w warstwę asynchroniczną [RxJava, metier, DAO];
- etap 2: przekształcimy synchroniczną aplikację konsolową w aplikację nadal synchroniczną, ale korzystającą z asynchronicznego interfejsu [RxJava, metier, DAO];
- krok 3: przekształcimy synchroniczną aplikację Swing w asynchroniczną aplikację Swing;
20.1. krok 1
Przekształcamy obecną warstwę synchroniczną [metier, DAO] w warstwę asynchroniczną [RxJava, metier, DAO].
20.1.1. Tworzenie
Wychodzimy od projektu Maven z rozdziału 17.4, który otwieramy w programie NetBeans:
![]() | ![]() |
Duplikujemy ten projekt [1] (kopiuj/wklej) do nowego projektu [elections-rxjava-metier-dao-security-webjson] [2].
20.1.2. Konfiguracja Maven
Modyfikujemy plik [pom.xml] w nowym projekcie, aby dodać zależność od biblioteki [RxJava]:
<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>
<!-- biblioteka jSON wykorzystywana przez 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>
<!-- komponent używany przez 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>
<!-- biblioteka logów -->
<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>
- wiersze 65–70: dodaliśmy zależność od biblioteki RxJava;
20.1.3. Asynchroniczna implementacja warstwy [métier]
![]() |
Aby zaimplementować warstwę [RxJava, métier], dodajemy do projektu asynchroniczny interfejs [IRxElectionsMetier] [1] oraz jego implementację [RxElectionsMetier] [2]:
![]() |
Interfejs [IRxElectionsMetier] jest interfejsem asynchronicznym warstwy [RxJava, métier]. Jego kod wygląda następująco:
package elections.security.client.metier;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import rx.Observable;
public interface IRxElectionsMetier {
// uwierzytelnianie
Observable<Void> authenticate(User user);
// pobierz listy kandydatów
Observable<ListeElectorale[]> getListesElectorales(User user);
// liczba mandatów do obsadzenia
Observable<Integer> getNbSiegesAPourvoir(User user);
// próg wyborczy
Observable<Double> getSeuilElectoral(User user);
// rejestrowanie wyników
Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales);
// obliczanie liczby mandatów
Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales);
}
Interfejs [IRxElectionsMetier] przejmuje metody z interfejsu [IElectionsMetier], ale tam, gdzie metoda M z interfejsu [IElectionsMetier] zwracała wynik typu T, metoda M interfejsu [IRxElectionsMetier] zwraca wynik typu Observable<T>. Typ [Observable] jest dostarczany przez bibliotekę RxJava. Typ Observable<T> udostępnia metodę [subscribe], która asynchronicznie pobiera typ T. Z tą metodą powiązane są trzy zdarzenia:
- onSuccess(T result), które informuje, że dostępny jest wynik typu T. Operacja asynchroniczna może zwrócić wiele wyników;
- onError(Throwable th), które informuje, że w operacji asynchronicznej wystąpił błąd;
- onCompleted(), które informuje, że operacja asynchroniczna została zakończona;
Dopóki metoda [Observable.subscribe] nie zostanie wywołana, operacja asynchroniczna powiązana z obserwowalnym nie zostanie uruchomiona. Kod wywołujący metodę M interfejsu [IRxElectionsMetier] nie otrzymuje oczekiwanego wyniku T, lecz typ Observable<T>, który pozwoli mu później uzyskać wynik T poprzez wywołanie metody [Observable.subscribe].
Implementacja [RxElectionsMetier] interfejsu [IRxElectionsMetier] wygląda następująco:
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 {
// wywołanie metody synchronicznej, a następnie odpowiedź dla subskrybenta
subscriber.onNext(metier.getListesElectorales(user));
// zgłaszamy koniec obserwowalnej wartości
subscriber.onCompleted();
} catch (Exception e) {
// przekazywanie wyjątku
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) {
...
}
}
- wiersze 12–13: wstrzyknięcie warstwy biznesowej przez Spring w trybie synchronicznym;
- wiersze 20–34: skomentujemy metodę [getListesElectorales], która zamiast zwracać typ [ListeElectorale[]] zwraca typ [Observable<ListeElectorale[]>];
- wiersze 22–32: statyczna metoda [Observable.create] pozwala utworzyć obiekt typu Observable na podstawie typu [Subscriber]. Typ [Subscriber] reprezentuje subskrybenta strumieni wyników generowanych przez obserwowany proces (Observable). Udostępnia on trzy metody:
- [Subscriber.onNext] (wiersz 25) do odbierania wyniku z obserwowanego procesu;
- [Subscriber.onError] (wiersz 30) do odbierania wyjątku z obserwowanego procesu. Po wystąpieniu wyjątku typ [Observable] nie wysyła już wyników;
- [Subscriber.onCompleted] (wiersz 27) do odbierania sygnału zakończenia emisji z obserwowanego procesu. W tym przypadku obserwowany proces emituje tylko jeden element. Należy zauważyć, że sygnał ten nie jest emitowany, jeśli wystąpi wyjątek. Jest to domyślne zachowanie obiektów typu Observable: wystąpienie wyjątku oznacza również zakończenie emisji. Subskrybenci są tego świadomi;
- wiersze 22–34: metoda [Observable.create] przyjmuje jako parametr typ [Observable.OnSubscribe]. Typ ten jest interfejsem funkcjonalnym. Pojęcie to zostało wprowadzone w Javie 8 i oznacza interfejs posiadający jedną metodę. W tym przypadku jedyną metodą interfejsu [Observable.OnSubscribe] jest następująca:
Aby zaimplementować interfejs funkcjonalny z jedną metodą m(param1, param2, ..., paramn), można użyć następującej uproszczonej składni:
Tak właśnie zrobiono w wierszach 22–34:
- [subscriber] jest parametrem metody [Observable.OnSubscribe.call];
- wiersze 23–32: kod, który chcemy nadać metodzie [call];
- wiersz 25: listy wyborcze są pobierane synchronicznie z warstwą [métier] wstrzykniętą w wierszu 13. Będzie więc nastąpiło oczekiwanie na wynik. Gdy wynik zostanie odebrany, jest on przekazywany do metody [onNext] subskrybenta;
- wiersz 28: w przypadku błędu wyjątek jest przekazywany do metody [onError] subskrybenta;
- wiersz 31: oczekuje się tylko jednego wyniku. Gdy zostanie on uzyskany (listy wyborcze lub wyjątek), subskrybent otrzymuje informację, że obserwowany proces zakończył generowanie wyników;
Należy pamiętać, że metoda [RxElectionsMetier] zwraca typ Observable<ListeElectorale[]>, a nie sam typ ListeElectorale[]. Kod wywołujący musi wywołać metodę Observable<ListeElectorale[]>.subscribe, aby kod z linii 23–33 został wykonany i zwrócił listy wyborcze za pomocą linii 25.
Kod pozostałych metod jest analogiczny:
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 {
// wywołanie metody synchronicznej
metier.authenticate(user);
// zgłaszane jest zakończenie obserwowalnego
subscriber.onCompleted();
} catch (Exception e) {
// przekazywanie wyjątku
subscriber.onError(e);
}
});
}
@Override
public Observable<ListeElectorale[]> getListesElectorales(User user) {
return Observable.create(subscriber -> {
try {
// wywołanie metody synchronicznej, a następnie odpowiedź dla subskrybenta
subscriber.onNext(metier.getListesElectorales(user));
// zgłaszane jest zakończenie obserwowalnego
subscriber.onCompleted();
} catch (Exception e) {
// przekazywanie wyjątku
subscriber.onError(e);
}
});
}
@Override
public Observable<Integer> getNbSiegesAPourvoir(User user) {
return Observable.create(subscriber -> {
try {
// wywołanie metody synchronicznej, a następnie odpowiedź dla subskrybenta
subscriber.onNext(metier.getNbSiegesAPourvoir(user));
// zgłaszane jest zakończenie obserwowalnego
subscriber.onCompleted();
} catch (Exception e) {
// przekazywanie wyjątku
subscriber.onError(e);
}
});
}
@Override
public Observable<Double> getSeuilElectoral(User user) {
return Observable.create(subscriber -> {
try {
// wywołanie metody synchronicznej, a następnie odpowiedź dla subskrybenta
subscriber.onNext(metier.getSeuilElectoral(user));
// zgłaszane jest zakończenie obserwowalnej
subscriber.onCompleted();
} catch (Exception e) {
// przekazywanie wyjątku
subscriber.onError(e);
}
});
}
@Override
public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
return Observable.create(subscriber -> {
try {
// wywołanie metody synchronicznej
metier.recordResultats(user, listesElectorales);
// zgłaszane jest zakończenie obserwowalnego
subscriber.onCompleted();
} catch (Exception e) {
// przekazywanie wyjątku
subscriber.onError(e);
}
});
}
@Override
public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
return Observable.create(subscriber -> {
try {
// wywołanie metody synchronicznej, a następnie odpowiedź dla subskrybenta
subscriber.onNext(metier.calculerSieges(user, listesElectorales));
// zgłaszane jest zakończenie obserwowalnego
subscriber.onCompleted();
} catch (Exception e) {
// przekazywanie wyjątku
subscriber.onError(e);
}
});
}
}
- wiersze 20 i 81: metoda [onNext] subskrybenta nie jest wywoływana, ponieważ subskrybent nie oczekuje żadnych wyników;
20.1.4. Testy JUnit warstwy [métier]
![]() |
20.1.4.1. Test01
Wracamy do testu jednostkowego [Test01] omówionego w paragrafie 17.4.4. Został on zaprojektowany do wykonywania wywołań synchronicznych do interfejsu [IElectionsMetier]. Modyfikujemy go tak, aby wykonywał synchroniczne wywołania nowego interfejsu [IRxElectionsMetier]. Możliwe jest bowiem wykonywanie synchronicznych wywołań asynchronicznego interfejsu RxJava. Kod wygląda następująco:
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 {
// warstwa [electionsMetier]
@Autowired
private IRxElectionsMetier electionsMetier;
// mapujący jSON
private final ObjectMapper mapper = new ObjectMapper();
// użytkownicy
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() {
// tworzy się tabelę 7 list kandydatów
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);
// oblicza się liczbę mandatów dla każdej z list
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// sprawdza się wyniki
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() {
// tworzy się tabelę 7 list wyborczych
ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// ustala się na stałe liczbę głosów
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);
// oblicza się liczbę mandatów uzyskanych przez każdą z list
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// sprawdza się wyniki
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() {
// tworzy się tabelę zawierającą 24 listy kandydatów, z których każda ma po 1 głos
ListeElectorale[] listes = new ListeElectorale[25];
// wszystkie 25 list otrzyma taką samą liczbę głosów (4%)
for (int i = 0; i < listes.length; i++) {
listes[i] = new ListeElectorale("Liste" + (i + 1), 1, 0, false);
}
// obliczenie liczby mandatów – zazwyczaj powinno to dać wynik ElectionsException
// przy progu wyborczym wynoszącym 5%
BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
}
/**
* enregistrement des résultats de l'élection
*
* @throws JsonProcessingException
*/
@Test
public void ecritureResultatsElections() throws JsonProcessingException {
// tworzymy tabelę 7 list wyborczych
ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// ustala się na stałe liczbę głosów
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);
// obliczamy liczbę mandatów uzyskanych przez każdą z list
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// wyświetla się wyniki
for (int i = 0; i < listes.length; i++) {
System.out.println(mapper.writeValueAsString(listes[i]));
}
// zapisuje się wyniki w bazie danych
BlockingObservable.from(electionsMetier.recordResultats(admin, listes)).firstOrDefault(null);
// sprawdza się wyniki
listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// wyświetla się wyniki
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());
}
}
Przyjrzyjmy się zmianom:
- wiersz 48: metoda statyczna [BlockingObservable.from(Observable).first]:
- subskrybuje parametr obserwowalny metody [from];
- uruchamia wykonanie kodu powiązanego z obserwowalnym;
- czeka na otrzymanie pierwszego wyniku. Jest to zatem operacja synchroniczna;
W tym przypadku używamy metody [firstOrDefault(null)], ponieważ obserwowalna [metier.authenticate] nie zwraca wyniku po uruchomieniu. Wynikiem metody [firstOrDefault(null)] będzie zatem null, wartość, która nie jest tutaj wykorzystywana;
Ten schemat powtarzamy w pozostałej części kodu za każdym razem, gdy chcemy wywołać warstwę [métier].
Test jednostkowy [Test01] musi zakończyć się powodzeniem:
![]() |
Zadanie do wykonania: sprawdzić, czy test [Test01] zakończy się powodzeniem.
20.1.4.2. Test02
Modyfikujemy test [Test01], aby odtąd testować interfejs asynchroniczny [IRxElectionsMetier] poprzez wykonywanie asynchronicznych wywołań jego metod.
Przyjrzyjmy się pierwszemu testowi:
// semafor synchronizacji wątków
private CountDownLatch latch;
// -----------------------------------
private ElectionsException checkUserUserException;
@Test()
public void checkUserUser() throws InterruptedException {
// semafor ustawiony na 1
latch = new CountDownLatch((1));
// operacja asynchroniczna
electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
.subscribe((result) -> {
},
(th) -> {
checkUserUserException = (ElectionsException) th;
latch.countDown();
},
() -> {
latch.countDown();
});
// oczekiwanie na semafor
latch.await();
// weryfikacja wyników
Assert.assertNotNull(checkUserUserException);
Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
- wiersz 2: semafor to narzędzie służące do synchronizacji wątków między sobą. Wątki to strumienie wykonywania działające równolegle. Aby wykonać zadanie T1, wątek [Thread1] może wymagać, aby zadanie T2, wykonywane przez wątek [Thread2], zostało zakończone. Wówczas wątek ten oczekuje, aż wątek [Thread2] wyśle mu sygnał wskazujący, że zadanie T2 zostało zakończone. Istnieją różne sposoby zarządzania tą synchronizacją między dwoma wątkami. Zastosowana tutaj metoda jest następująca:
- wiersz 10: wątek [Thread1] tworzy semafor o wartości 1;
- wiersz 12: wątek [Thread1] tworzy i uruchamia wątek [Thread2]. Odbywa się to za pomocą następującej składni:
electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
Metoda [Observable.subscribeOn] określa wątek, na którym będzie wykonywany obserwowany proces. Parametrem metody [subscribeOn] jest pula wątków. Biblioteka RxJava udostępnia kilka takich pul, dostosowanych do różnych sytuacji. Pula [Schedulers.io()] jest zalecana do operacji sieciowych;
- (ciąg dalszy)
- wiersze 12–13: operacja
electionsMetier.authenticate(user).subscribeOn(Schedulers.io()).subscribe(...)
wykonuje operację synchroniczną zawartą w obserwowalnym obiekcie [authenticate(user)]. Ponieważ jednak ta operacja synchroniczna jest uruchamiana w innym wątku niż wątek [Thread1], ten ostatni nie czeka na odpowiedź metody [subscribe] i przechodzi do następnej instrukcji;
- (ciąg dalszy)
- wiersz 23: wątek [Thread1] zatrzymuje się i czeka, aż semafor przyjmie wartość 0 (obecnie ma wartość 1);
- wiersze 13–21: metoda [subscribe] przyjmuje jako parametry trzy funkcje lambda:
- pierwsza, [(result)->{...}], jest wywoływana za każdym razem, gdy obserwowalna [authenticate(user)] emituje wynik [result]. W tym przypadku mamy obserwowalną [authenticate(user)], która wykonuje pewne działanie, ale nie emituje żadnego wyniku. Funkcja lambda [(result)->{}] nigdy więc nie zostanie wywołana. Dlatego jej kod jest tutaj pusty: [{}];
- druga funkcja lambda [(th)->{...}] otrzymuje jako parametr typ [Throwable]. Jest ona wywoływana, gdy podczas wykonywania obserwowalnej funkcji wystąpi wyjątek. W tym przypadku przetwarzamy parametr [Throwable th] w następujący sposób:
- wiersz 16: zapisujemy go w polu klasy testowej typu [ElectionsException], ponieważ wykonywana obserwowalna generuje wyłącznie ten typ wyjątku;
- wiersz 17: ustawiamy semafor na 0, aby wskazać, że wątek [Thread2] zakończył swoją pracę;
- trzeci [()->{...}] jest wywoływany, gdy obserwowalny nie ma już elementów do wysłania. Obsługujemy to zdarzenie w następujący sposób:
- wiersz 20: ustawiamy semafor na 0, aby zasygnalizować, że wątek [Thread2] zakończył swoją pracę;
Należy zauważyć, że trzecia funkcja lambda nie jest wywoływana, jeśli wystąpi wyjątek. Dlatego też musieliśmy ustawić semafor na 0 również w wierszu 17;
- wiersz 25: gdy dochodzimy do tego wiersza, obserwowalny obiekt zakończył swoją pracę. Możemy wówczas przeprowadzić te same sprawdzenia, co w teście [Test01];
Przyjrzyjmy się kolejnemu testowi:
// -----------------------------------
private ElectionsException calculSieges1Exception;
private ListeElectorale[] listesCalculSieges1;
@Test
public void calculSieges1() throws InterruptedException {
// tworzymy tablicę zawierającą 7 list kandydackich
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);
// semafor ustawiony na 1
latch = new CountDownLatch((1));
// operacja asynchroniczna
// obliczamy liczbę mandatów dla każdej z list
electionsMetier.calculerSieges(admin, listes).subscribeOn(Schedulers.io())
.subscribe((result) -> {
listesCalculSieges1 = result;
},
(th) -> {
calculSieges1Exception = (ElectionsException) th;
latch.countDown();
},
() -> {
latch.countDown();
});
// oczekiwanie na semafor
latch.await();
// sprawdzanie wyników
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());
}
- wiersze 20–30: asynchroniczne wykonanie obserwowalnego [electionsMetier.calculerSieges(admin, listes)];
- wiersze 21–23: wykonanie obserwowalnego zwraca typ [ListeElectorale[]], który zapisujemy w polu klasy testowej w wierszu 3;
- wiersze 34–48: są to weryfikacje z testu [Test01], do których dodano weryfikację z wiersza 34, sprawdzającą, czy nie wystąpił żaden wyjątek;
Cały test [Test02] jest dostępny w materiałach szkoleniowych.
Zadanie do wykonania: uruchomić test [Test02] i sprawdzić, czy zakończył się powodzeniem.
20.1.4.3. Test03
Test [Test03] działa tak samo jak test [Test01]: testuje interfejs [IRxElectionsMetier] poprzez synchroniczne wywołania tego interfejsu. Jest to kopia testu [Test02] z dwoma różnicami:
- obserwowalne nie są już wykonywane w wątku innym niż ten, w którym wykonywane są testy. Gdy wątek [Thread1] wywołuje metodę [subscribe] obiektu obserwowalnego, metoda ta inicjuje operację HTTP skierowaną do serwera, również w wątku [Thread1]. Cała metoda [subscribe] staje się wówczas synchroniczna;
- ponieważ pozostaje tylko jeden wątek, synchronizacja wątków staje się zbędna, a semafor znika;
Oto dwa przykłady testów:
// -----------------------------------
private ElectionsException checkUserUserException;
@Test()
public void checkUserUser() throws InterruptedException {
// operacja synchroniczna
electionsMetier.authenticate(user)
.subscribe((result) -> {
},
(th) -> {
checkUserUserException = (ElectionsException) th;
},
() -> {
});
// weryfikacja wyników
Assert.assertNotNull(checkUserUserException);
Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
- wiersz 7: domyślnie metoda [electionsMetier.authenticate(user).subscribe] jest wykonywana w wątku kodu wywołującego. Mamy więc do czynienia z operacją synchroniczną;
// -----------------------------------
private ElectionsException calculSieges1Exception;
private ListeElectorale[] listesCalculSieges1;
@Test
public void calculSieges1() throws InterruptedException {
// tworzy się tabelę 7 list kandydackich
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);
// operacja synchroniczna
// obliczanie liczby mandatów dla każdej z list
electionsMetier.calculerSieges(admin, listes)
.subscribe((result) -> {
listesCalculSieges1 = result;
},
(th) -> {
calculSieges1Exception = (ElectionsException) th;
},
() -> {
});
// weryfikacja wyników
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());
}
Zadanie do wykonania: przeprowadź test [Test03] i sprawdź, czy zakończy się powodzeniem.
20.2. Krok 2
Teraz przekształcamy synchroniczną aplikację konsolową z rozdziału 17.5 w aplikację, która nadal jest synchroniczna, ale wykorzystuje asynchroniczny interfejs [RxJava, metier, DAO];
![]() |
Wychodzimy od projektu [elections-console-metier-dao-security-webjson] [1] z rozdziału 17.5, który powielamy w nowym projekcie [elections-console-rxjava- metier-dao-security-webjson] [2]:
![]() | ![]() |
- do [3-4]; w nowym projekcie usuwamy zależność od starej warstwy synchronicznej [métier];
![]() | ![]() |
- w [5-9] dodano zależność od nowej, asynchronicznej warstwy [métier];
![]() | ![]() |
- w pliku [10-14] zmieniono nazwę klasy [ElectionsConsole] na [ElectionsConsole01];
Podobnie zmienia się nazwę klasy [BootElectionsConsole] na [BootElectionsConsole01]:
![]() |
Obecny kod klasy [BootElectionsConsole01] jest następujący:
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);
}
}
- wiersz 13: ponieważ zmieniono nazwę klasy [ElectionsConsole] na [ElectionsConsole01], należy teraz wpisać:
return ctx.getBean("electionsConsole01",IElectionsUI.class);
Wróćmy do kodu klasy [ElectionsConsole01]:
@Component
public class ElectionsConsole01 implements IElectionsUI {
@Autowired
private IElectionsMetier electionsMetier;
@Autowired
private User admin;
@Override
public void run() {
// listy biorące udział w wyborach
ListeElectorale[] listes;
// wprowadzanie danych
try (Scanner clavier = new Scanner(System.in)) {
// pobieranie list biorących udział w wyborach z warstwy [metier]
listes = electionsMetier.getListesElectorales(admin);
...
// przeprowadza się obliczenia dotyczące mandatów
listes=electionsMetier.calculerSieges(admin,listes);
// zapisywanie wyników
electionsMetier.recordResultats(admin,listes);
...
}
Jeśli posłużymy się przykładem testu [Test01] z paragrafu 20.1.4.1, wiersze 5, 17, 20 i 22 ulegną następującym zmianom:
@Component
public class ElectionsConsole01 implements IElectionsUI {
@Autowired
private IRxElectionsMetier electionsMetier;
@Autowired
private User admin;
@Override
public void run() {
// listy startowe
ListeElectorale[] listes;
// wprowadzanie danych
try (Scanner clavier = new Scanner(System.in)) {
// pobieranie list startowych z warstwy [metier]
listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
...
// przeprowadza się obliczenia dotyczące mandatów
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// zapisywanie wyników
BlockingObservable.from(electionsMetier.recordResultats(admin, listes));
...
}
Zadanie: skonfiguruj projekt tak, aby uruchamiał klasę [BootElectionsConsole01] z trzema parametrami [SS, Heures travaillées, Jours travaillés] i sprawdź, czy wykonanie tak skonfigurowanego projektu daje oczekiwane wyniki.
Zadanie: skonfiguruj projekt tak, aby uruchamiał parę [BootElectionsConsole02, ElectionsConsole02], przy czym klasa [ElectionsConsole02] została napisana zgodnie z wzorem testu [Test02] z paragrafu 20.1.4.2.
Zadanie: skonfiguruj projekt tak, aby uruchomić parę [BootElectionsConsole03, ElectionsConsole03], przy czym klasa [ElectionsConsole03] została napisana zgodnie z wzorem testu [Test03] z paragrafu 20.1.4.3.
20.3. Krok 3
Przechodzimy teraz do przeniesienia aplikacji Swing do środowiska asynchronicznego.
![]() |
Zaczynamy od skopiowania projektu [elections-swing-metier-dao-security-webjson] [1] z rozdziału 17.6 do nowego projektu [elections-swing-rxjava-metier-dao-security-webjson] [2]:
![]() | ![]() |
- w [3, 4] usuwamy zależność od warstwy synchronicznej [console];
![]() | ![]() |
- w warstwie [5-9] dodajemy zależność od asynchronicznej warstwy konsoli;
Warstwa [swing] będzie wykonywać rzeczywiste wywołania asynchroniczne do warstwy [métier]. Podczas wywołania jednej z jej metod będą działały dwa wątki:
- wątek warstwy UI, który obsługuje zdarzenia;
- wątek wejścia/wyjścia, który wykona wywołanie HTTP do serwera;
Przez cały czas trwania wywołania asynchronicznego powinniśmy wyświetlać obrazek oczekiwania oraz przycisk anulowania. Nie zrobimy tego tutaj, a zostanie to zaproponowane jako ulepszenie aplikacji. Zmiany dotyczą dwóch klas, które wywołują warstwę [métier]:
![]() |
20.3.1. Konfiguracja Maven
W tym przypadku wykorzystamy bibliotekę [RxSwing], która rozszerza bibliotekę [RxJava] o funkcje dostępne wyłącznie w środowisku Swing. W tym celu modyfikujemy plik [pom.xml] w następujący sposób:
<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>
<!-- warstwy dolne -->
<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. Klasa [ElectionsConnectForm]
W trybie asynchronicznym klasa [ElectionsConnectForm] przyjmuje następujący kształt:
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;
// odniesienie do warstwy [métier] asynchronicznej
@Autowired
private IRxElectionsMetier metier;
// zalogowany użytkownik
private User user;
// formularz główny
@Autowired
private ElectionsMainForm electionsMainForm;
// sesja UI
@Autowired
private UiSession uiSession;
@Override
protected void doConnect() {
if (isPageValid()) {
// uwierzytelnianie użytkownika
metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(
// brak odpowiedzi
(result) -> {
},
// obsługa wyjątku
(th) -> {
// odnotowano błąd
String info = getInfoForException("Les erreurs suivantes se sont produites :", th);
// wyświetlanie informacji
jTextPaneErreurs.setText(info);
jTextPaneErreurs.setCaretPosition(0);
},
// uwierzytelnianie zakończone
() -> {
// zapisano użytkownika w sesji
uiSession.setUser(user);
// ekran logowania jest ukryty
setVisible(false);
// wyświetlany jest ekran główny
electionsMainForm.run();
});
}
}
// inicjalizacje
@Override
protected void init() {
...
}
@Override
public void run() {
// wyświetlany jest interfejs graficzny
SwingUtilities.invokeLater(new Runnable() {
public void run() {
init();
setVisible(true);
}
});
}
private boolean isPageValid() {
...
}
private String getInfoForException(String message, Throwable ex) {
...
}
}
- wiersze 36–63: metoda [doConnect] jest wykonywana, gdy użytkownik wybierze opcję menu [Connexion]:
![]() |
Wszystko znajduje się w wierszu 40:
metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(...)
- obserwowanym procesem jest [metier.authenticate(user)];
- zostanie on uruchomiony w wątku wejścia/wyjścia pobranym z puli [Schedulers.io()];
- będzie obserwowany w wątku UI, który obsługuje zdarzenia interfejsu Swing [observeOn(SwingScheduler.getInstance())]. Wątek ten jest uzyskiwany za pomocą metody [SwingScheduler.getInstance()], gdzie [SwingScheduler] jest klasą dostarczaną przez bibliotekę [RxSwing]. Jest to obowiązkowe. Po uzyskaniu wyniku operacji asynchronicznej jest on często wykorzystywany do modyfikowania elementów interfejsu Swing. Interfejs ten można jednak modyfikować wyłącznie w wątku klasy UI; w przeciwnym razie wystąpi wyjątek. W związku z tym wiersze 41–61 muszą być wykonywane w wątku klasy UI. Zapewnia to tutaj metoda [observeOn(SwingScheduler.getInstance())];
Omówmy pozostałą część kodu:
- wiersze 42–43: te wiersze służą do zachowania składni metody [subscribe]. Nigdy nie zostaną one wykonane, ponieważ proces [metier.authenticate(user)] nie zwraca żadnego wyniku;
- wiersze 35–52: po otrzymaniu wyjątku jest on wyświetlany;
- wiersze 54–61: wykonywane, gdy proces [metier.authenticate(user)] sygnalizuje zakończenie wysyłania danych;
20.3.3. Klasa [ElectionsMainForm]
![]() |
20.3.3.1. Inicjalizacja interfejsu graficznego
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;
// odwołanie do warstwy asynchronicznej [métier]
@Autowired
private IRxElectionsMetier metier;
// sesja UI
@Autowired
private UiSession uiSession;
// zalogowany użytkownik
private User user;
// szablony list JList
private DefaultListModel<String> modèleNomsVoix = null;
private DefaultListModel<String> modèleRésultats = null;
// konkurencyjne listy
private ListeElectorale[] listes;
// listy wprowadzone przez użytkownika
private final List<ListeElectorale> listesSaisies = new ArrayList<>();
private ListeElectorale[] tListesSaisies;
// inicjalizacje
@Override
protected void init() {
// generowanie komponentów przez klasę nadrzędną
super.init();
// stan formularza
Utilitaires.setEnabled(new JLabel[]{jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer}, false);
Utilitaires.setEnabled(
new JMenuItem[]{jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer}, false);
// wyśrodkowanie okna
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);
// zalogowany użytkownik
user = uiSession.getUser();
// inicjalizacje lokalne
modèleNomsVoix = new DefaultListModel<>();
jListNomsVoix.setModel(modèleNomsVoix);
modèleRésultats = new DefaultListModel<>();
jListResultats.setModel(modèleRésultats);
// wystosowano żądanie list do warstwy [métier]
metier.getListesElectorales(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// odpowiedź
listesElectorales -> {
// zapisywanie list
listes = listesElectorales;
},
// wyjątek
(th) -> showException(th),
// koniec obserwowalny
() -> {
// kolejny krok
doInitStep2();
});
}
...
- wiersz 46: metoda [init] jest wykonywana w momencie wyświetlenia powiązanego okna. Jej celem jest zainicjowanie poniższych komponentów [1-3]:
![]() |
- wiersze 71–85: asynchronicznie pobierane są listy kandydatów (komponent [1]);
- wiersz 71: obserwowanym procesem jest [metier.getListesElectorales(user)]. Jest on wykonywany w wątku wejścia/wyjścia [subscribeOn(Schedulers.io())] i obserwowany w wątku UI oraz [observeOn(SwingScheduler.getInstance()];
- wiersze 74–77: wynik zwrócony przez obserwowany proces jest zapisywany w polu [listes] w wierszu 38;
- wiersz 79: ewentualny wyjątek jest obsługiwany przez następującą metodę:
private void showException(Throwable th) {
// wyświetla się wyjątek
jTextPaneMessages.setText(getInfoForException("Les erreurs suivantes se sont produites : ", th));
jTextPaneMessages.setCaretPosition(0);
}
- wiersze 81–84: po zakończeniu obserwowanego procesu wykonywane są wiersze 81–84. Wiersze te nie są wykonywane, jeśli wystąpił wyjątek. Metoda [doInitStep2] realizuje etap 2 inicjalizacji w następujący sposób:
private void doInitStep2() {
// przypisuje się nazwy list do pola kombi jComboBoxNomsListes
for (int i = 0; i < listes.length; i++) {
jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
}
// liczba miejsc do obsadzenia
metier.getNbSiegesAPourvoir(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// odpowiedź
nbSiegesAPourvoir -> {
// inicjuje się etykietę powiązaną z tą informacją
jLabelSAP.setText(jLabelSAP.getText() + nbSiegesAPourvoir);
},
// wyjątek
(th) -> showException(th),
// koniec obserwowalny
() -> {
// kolejny etap
doInitStep3();
});
}
- wiersze 3–5: wykorzystuje się wynik poprzedniego etapu do wypełnienia listy rozwijanej nazwami list kandydatów;
- wiersze 7–20: asynchronicznie pobierana jest liczba miejsc do obsadzenia;
- wiersz 7: obserwowanym procesem jest [metier.getNbSiegesAPourvoir(user)]. Jest on wykonywany w wątku wejścia/wyjścia [subscribeOn(Schedulers.io())] i obserwowany w wątku UI oraz [observeOn(SwingScheduler.getInstance()];
- wiersze 10–13: wynik zwrócony przez proces jest wykorzystywany do aktualizacji interfejsu graficznego;
- wiersz 15: wyświetlany jest ewentualny wyjątek;
- wiersze 17–20: po otrzymaniu sygnału zakończenia obserwowalnego przechodzi się do etapu 3 procesu inicjalizacji;
Etap 3 inicjalizacji jest realizowany przez następujący kod:
private void doInitStep3() {
// próg wyborczy
metier.getSeuilElectoral(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// odpowiedź
seuilElectoral -> {
// inicjujemy etykietę powiązaną z tą informacją
jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
},
// wyjątek
(th) -> showException(th),
// koniec obserwowalny
() -> {
});
}
- wiersze 3–4: asynchroniczne żądanie progu wyborczego;
- wiersz 3: obserwowanym procesem jest [metier.getSeuilElectoral(user)]. Jest on wykonywany w wątku wejścia/wyjścia [subscribeOn(Schedulers.io())] i obserwowany w wątku UI oraz [observeOn(SwingScheduler.getInstance()];
- wiersze 6–9: wynik zwrócony przez proces służy do aktualizacji interfejsu graficznego;
- wiersz 11: wyświetlany jest ewentualny wyjątek;
- wiersze 13–14: po otrzymaniu sygnału zakończenia obserwowalnego nie podejmuje się żadnych działań: proces inicjalizacji interfejsu graficznego został zakończony;
20.3.3.2. Obliczanie liczby mandatów uzyskanych przez poszczególne listy
Metoda [doCalculer] służy do obliczania liczby mandatów zdobytych przez poszczególne listy:
@Override
protected void doCalculer() {
tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
// obliczanie liczby mandatów
String info = null;
metier.calculerSieges(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// przetwarzanie wyniku
result -> consumeResultSieges(result),
// przetwarzanie wyjątku
th -> showException(th),
// koniec obserwowalny
() -> {
}
);
}
- wiersze 6–15: asynchronicznie obliczana jest liczba mandatów zdobytych przez poszczególne listy;
- wiersz 6: obserwowanym procesem jest [metier.calculerSieges(user, tListesSaisies)]. Jest on wykonywany w wątku wejścia/wyjścia [subscribeOn(Schedulers.io())] i obserwowany w wątku UI oraz [observeOn(SwingScheduler.getInstance()];
- wiersz 9: wynik zwrócony przez proces jest wykorzystywany przez metodę [consumeResultSieges];
- wiersz 11: wyświetlany jest ewentualny wyjątek;
- wiersze 13–14: po otrzymaniu sygnału zakończenia obserwowanego obiektu nie podejmuje się żadnych działań;
W wierszu 9 metoda [consumeResultSieges] wykorzystuje wynik zwrócony przez obserwowany proces, aktualizując listy kandydatów wraz z ich polami [sieges, elimine]:
private void consumeResultSieges(ListeElectorale[] tListesSaisies) {
// zapisywanie wyniku
this.tListesSaisies = tListesSaisies;
// wyświetlanie wyników
modèleRésultats.clear();
for (int i = 0; i < tListesSaisies.length; i++) {
modèleRésultats.addElement(tListesSaisies[i].toString());
}
// aktualizacja stanu formularza
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é");
}
- wiersze 4–14: uzyskany wynik służy do aktualizacji interfejsu graficznego;
20.3.3.3. Rejestracja wyników wyborów
Rejestracja wyników wyborów odbywa się za pomocą następującej metody [doEnregistrer]:
@Override
protected void doEnregistrer() {
// wysyłanie żądania zapisu do warstwy [métier]
metier.recordResultats(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// przetwarzanie wyniku – tutaj go nie ma
(param) -> {
},
// obsługa wyjątku
(th) -> showException(th),
// koniec obserwowalny
() -> {
// aktualizacja formularza
Utilitaires.setEnabled(new JLabel[]{jLabelEnregistrer}, false);
Utilitaires.setEnabled(new JMenuItem[]{jMenuItemEnregistrer}, false);
jTextPaneMessages.setText("Enregistrement des résultats réalisé");
}
);
}
- wiersze 4–17: wyniki wyborów są zapisywane asynchronicznie;
- wiersz 4: obserwowanym procesem jest [metier.recordResultats(user, tListesSaisies)]. Jest on wykonywany w wątku wejścia/wyjścia [subscribeOn(Schedulers.io())] i obserwowany w wątku UI oraz [observeOn(SwingScheduler.getInstance()];
- wiersze 7–8: te wiersze nigdy nie zostaną wykonane, ponieważ obserwowany proces nie zwraca wyniku;
- wiersz 10: wyświetlany jest ewentualny wyjątek;
- wiersze 14–16: po otrzymaniu sygnału zakończenia obserwowalnego obiektu aktualizowany jest interfejs graficzny;
Zadanie do wykonania: sprawdź, czy aplikacja Swing działa. Następnie zmodyfikuj interfejs graficzny i kod tak, aby podczas operacji asynchronicznej z serwerem WWW / jSON pojawiał się obrazek oczekiwania oraz opcja anulowania bieżącej operacji.























