20. Programmazione asincrona con RxJava
Documento da leggere: [Introduction à RxJava. Application aux environnements Swing et Android.]
In questo capitolo torniamo al capitolo 17.6, in cui avevamo realizzato un’applicazione client/server con la seguente architettura:
![]() |
Alcune azioni dell’utente sull’interfaccia Swing in [1] innescano azioni che arrivano fino al database in [3] attraverso una rete HTTP [2]. A causa di ciò, la risposta all’azione dell’utente può richiedere più o meno tempo. Sarebbe utile poter inserire un indicatore di attesa sull’interfaccia utente con un’opzione per annullare l’operazione avviata qualora questa dovesse protrarsi troppo a lungo. Nel capitolo 17.6, ogni azione dell'utente che richiede lo scambio di informazioni con il server è sincrona. Il gestore di eventi eseguito dal codice termina solo quando viene ricevuta la risposta. Per tutto questo tempo, l'interfaccia grafica rimane bloccata: non risponde alle nuove azioni dell'utente. Queste vengono semplicemente messe in coda per essere elaborate una volta terminato il gestore di eventi attualmente in esecuzione. Pertanto, se venisse visualizzato un pulsante di annullamento, l’utente potrebbe cliccarci sopra ma non accadrebbe nulla finché l’operazione in corso non fosse terminata. Il pulsante di annullamento non avrebbe quindi alcuna utilità.
Affinché il clic sul pulsante di annullamento abbia effetto, è necessario che l’operazione in corso sia terminata. A tal fine, deve avviare l’operazione potenzialmente lunga in modo asincrono:
- il gestore di eventi avvia l’operazione lunga ma non ne attende il risultato e restituisce il controllo al thread di UI che gestisce gli eventi dell’interfaccia grafica. L’operazione lunga viene avviata su un thread diverso da quello di UI, il che non blocca quest’ultimo;
- se l’utente fa clic sul pulsante di annullamento prima del termine dell’operazione lunga, il thread di UI, che è libero, può gestire tale evento. È quindi possibile interrompere l’operazione lunga ignorandone il risultato;
- se l’operazione lunga non è stata annullata, l’arrivo della risposta provocherà un evento nel thread di UI. Quest’ultimo, se libero, eseguirà quindi il codice associato a tale evento, che elaborerà la risposta;
L'interfaccia utente funzionerà come in precedenza. Se i tempi di risposta del server sono rapidi, l'utente non noterà alcuna differenza. Se invece sono percepibili, all'utente apparirà un pulsante di annullamento e avrà la possibilità di interrompere l'operazione in corso.
La libreria [Rx] consente di effettuare la programmazione asincrona. Il suo grande vantaggio risiede nel fatto che è stata portata in numerosi ambienti (Java, .NET, JS, ...) e che la sua padronanza in un ambiente può essere facilmente trasferita in un altro. Ci baseremo qui sul capitolo 2 del documento [Introduction à RxJava. Application aux environnements Swing et Android]. Il lettore è invitato a leggerlo. Di seguito, riprendiamo il codice tratto dagli esempi di questo capitolo.
Modificheremo l’architettura dell’applicazione nel modo seguente:
![]() |
- in [1], inseriamo un livello [RxJava] tra il livello [swing] e il livello [métier]. I metodi di quest’ultimo saranno d’ora in poi chiamati in modo asincrono;
Procederemo in più fasi:
- fase 1: il livello [metier, DAO] presenta attualmente un'interfaccia sincrona con il livello [ui]. Lo trasformeremo in un livello asincrono [RxJava, metier, DAO];
- fase 2: convertiremo l’applicazione console sincrona in un’applicazione sempre sincrona ma che utilizza l’interfaccia asincrona [RxJava, metier, DAO];
- fase 3: convertiremo l’applicazione Swing sincrona in un’applicazione Swing asincrona;
20.1. fase 1
Trasformiamo l'attuale livello sincrono [metier, DAO] in un livello asincrono [RxJava, metier, DAO].
20.1.1. Creazione
Partiamo dal progetto Maven del capitolo 17.4, che apriamo con NetBeans:
![]() | ![]() |
Duplichiamo questo progetto [1] (copia/incolla) in un nuovo progetto [elections-rxjava-metier-dao-security-webjson] [2].
20.1.2. Configurazione Maven
Modifichiamo il file [pom.xml] del nuovo progetto per aggiungere la dipendenza dalla libreria [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>
<!-- libreria jSON utilizzata da 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>
<!-- componente utilizzato da 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>
<!-- libreria di log -->
<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>
- righe 65-70: abbiamo aggiunto la dipendenza dalla libreria RxJava;
20.1.3. Implementazione asincrona del livello [métier]
![]() |
Per implementare il livello [RxJava, métier], aggiungiamo al progetto un'interfaccia asincrona [IRxElectionsMetier] [1] e la relativa implementazione [RxElectionsMetier] [2]:
![]() |
L’interfaccia [IRxElectionsMetier] è l’interfaccia asincrona del livello [RxJava, métier]. Il suo codice è il seguente:
package elections.security.client.metier;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import rx.Observable;
public interface IRxElectionsMetier {
// autenticazione
Observable<Void> authenticate(User user);
// ottenere le liste in lizza
Observable<ListeElectorale[]> getListesElectorales(User user);
// il numero di seggi da assegnare
Observable<Integer> getNbSiegesAPourvoir(User user);
// soglia elettorale
Observable<Double> getSeuilElectoral(User user);
// registrazione dei risultati
Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales);
// il calcolo dei seggi
Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales);
}
L'interfaccia [IRxElectionsMetier] riprende i metodi dell'interfaccia [IElectionsMetier], ma laddove un metodo M dell'interfaccia [IElectionsMetier] restituiva un risultato di tipo T, il metodo M dell’interfaccia [IRxElectionsMetier] restituisce un risultato di tipo Observable<T>. Il tipo [Observable] è fornito dalla libreria RxJava. Un tipo Observable<T> fornisce il metodo [subscribe] che recupera il tipo T in modo asincrono. A questo metodo sono associati tre eventi:
- onSuccess(T result), che segnala che è disponibile un risultato di tipo T. L'operazione asincrona può fornire più risultati;
- onError(Throwable th), che segnala che l’operazione asincrona ha riscontrato un errore;
- onCompleted(), che segnala che l’operazione asincrona è terminata;
Finché il metodo [Observable.subscribe] non viene chiamato, l’operazione asincrona associata all’observable non viene avviata. Il codice che chiama un metodo M dell’interfaccia [IRxElectionsMetier] non ottiene il risultato T atteso, ma un tipo Observable<T> che gli consentirà in seguito di ottenere il risultato T chiamando il metodo [Observable.subscribe].
L’implementazione [RxElectionsMetier] dell’interfaccia [IRxElectionsMetier] è la seguente:
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 {
// richiesta con metodo sincrono e successiva risposta al sottoscrittore
subscriber.onNext(metier.getListesElectorales(user));
// si segnala la fine dell'osservabile
subscriber.onCompleted();
} catch (Exception e) {
// si inoltra l'eccezione
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) {
...
}
}
- righe 12-13: iniezione Spring del livello business sincrono;
- righe 20-34: commenteremo il metodo [getListesElectorales] che, invece di restituire un tipo [ListeElectorale[]], restituisce un tipo [Observable<ListeElectorale[]>];
- righe 22-32: il metodo statico [Observable.create] consente di creare un Observable a partire da un tipo [Subscriber]. Il tipo [Subscriber] rappresenta un sottoscrittore dei flussi di risultati generati dal processo osservato (l'Observable). Fornisce tre metodi:
- [Subscriber.onNext] (riga 25) per ricevere un risultato dal processo osservato;
- [Subscriber.onError] (riga 30) per ricevere un'eccezione dal processo osservato. Dopo un'eccezione, il tipo [Observable] non emette più risultati;
- [Subscriber.onCompleted] (riga 27) per ricevere il segnale di fine emissione del processo osservato. In questo caso, il processo osservato emette un solo elemento. Si noti che questo segnale non viene emesso se si verifica un'eccezione. Questo è il comportamento predefinito degli Observable: l'emissione di un'eccezione segnala anche la fine delle emissioni. Gli abbonati ne sono a conoscenza;
- righe 22-34: il metodo [Observable.create] accetta come parametro un tipo [Observable.OnSubscribe]. Questo tipo è un’interfaccia funzionale. Questo concetto è stato introdotto con Java 8 e indica un’interfaccia con un unico metodo. In questo caso, l’unico metodo dell’interfaccia [Observable.OnSubscribe] è il seguente:
Per implementare un'interfaccia funzionale con un unico metodo m(param1, param2, ..., paramn), è possibile utilizzare la seguente sintassi semplificata:
È quanto avviene alle righe 22-34:
- [subscriber] è il parametro del metodo [Observable.OnSubscribe.call];
- righe 23-32: il codice che si desidera assegnare al metodo [call];
- riga 25: si richiedono le liste elettorali in modo sincrono al livello [métier] iniettato alla riga 13. Si dovrà quindi attendere il risultato. Una volta ricevuto, questo viene passato al metodo [onNext] del sottoscrittore;
- riga 28: in caso di errore, l’eccezione viene passata al metodo [onError] del sottoscrittore;
- riga 31: si attende un solo risultato. Una volta ottenuto (le liste elettorali o un'eccezione), si comunica al sottoscrittore che il processo osservato ha terminato di emettere risultati;
Si ricordi che il metodo [RxElectionsMetier] restituisce un tipo Observable<ListeElectorale[]> e non il tipo ListeElectorale[] stesso. Il codice chiamante dovrà richiamare il metodo Observable<ListeElectorale[]>.subscribe affinché il codice delle righe 23-33 venga eseguito e restituisca le liste elettorali tramite la riga 25.
Il codice degli altri metodi è analogo:
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 {
// chiamata metodo sincrono
metier.authenticate(user);
// si segnala la fine dell'osservabile
subscriber.onCompleted();
} catch (Exception e) {
// si inoltra l'eccezione
subscriber.onError(e);
}
});
}
@Override
public Observable<ListeElectorale[]> getListesElectorales(User user) {
return Observable.create(subscriber -> {
try {
// chiamata del metodo sincrono e successiva risposta all'abbonato
subscriber.onNext(metier.getListesElectorales(user));
// viene segnalata la fine dell'osservabile
subscriber.onCompleted();
} catch (Exception e) {
// si inoltra l'eccezione
subscriber.onError(e);
}
});
}
@Override
public Observable<Integer> getNbSiegesAPourvoir(User user) {
return Observable.create(subscriber -> {
try {
// chiamata del metodo sincrono, quindi risposta al sottoscrittore
subscriber.onNext(metier.getNbSiegesAPourvoir(user));
// viene segnalata la fine dell'osservabile
subscriber.onCompleted();
} catch (Exception e) {
// si inoltra l'eccezione
subscriber.onError(e);
}
});
}
@Override
public Observable<Double> getSeuilElectoral(User user) {
return Observable.create(subscriber -> {
try {
// chiamata del metodo sincrono, quindi risposta al sottoscrittore
subscriber.onNext(metier.getSeuilElectoral(user));
// viene segnalata la fine dell'osservabile
subscriber.onCompleted();
} catch (Exception e) {
// si inoltra l'eccezione
subscriber.onError(e);
}
});
}
@Override
public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
return Observable.create(subscriber -> {
try {
// chiamata del metodo sincrono
metier.recordResultats(user, listesElectorales);
// si segnala la fine dell'osservabile
subscriber.onCompleted();
} catch (Exception e) {
// si inoltra l'eccezione
subscriber.onError(e);
}
});
}
@Override
public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
return Observable.create(subscriber -> {
try {
// chiamata metodo sincrono e successiva risposta all'abbonato
subscriber.onNext(metier.calculerSieges(user, listesElectorales));
// si segnala la fine dell'osservabile
subscriber.onCompleted();
} catch (Exception e) {
// si inoltra l'eccezione
subscriber.onError(e);
}
});
}
}
- righe 20 e 81: il metodo [onNext] del sottoscrittore non viene chiamato perché quest’ultimo non si aspetta alcun risultato;
20.1.4. I test JUnit del livello [métier]
![]() |
20.1.4.1. Test01
Riprendiamo il test unitario [Test01] esaminato al paragrafo 17.4.4. È stato progettato per effettuare chiamate sincrone all'interfaccia [IElectionsMetier]. Lo modifichiamo in modo che effettui chiamate sincrone alla nuova interfaccia [IRxElectionsMetier]. È infatti possibile effettuare chiamate sincrone a un'interfaccia asincrona RxJava. Il codice diventa il seguente:
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 {
// livello [electionsMetier]
@Autowired
private IRxElectionsMetier electionsMetier;
// mappatore jSON
private final ObjectMapper mapper = new ObjectMapper();
// utenti
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() {
// si crea la tabella delle 7 liste candidate
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);
// si calcolano i seggi di ciascuna lista
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// si verificano i risultati
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() {
// si crea la tabella delle 7 liste candidate
ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// si fissano in modo definitivo i voti
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);
// si calcolano i seggi ottenuti da ciascuna delle liste
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// si verificano i risultati
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() {
// si crea una tabella composta da 24 liste candidate, ciascuna con 1 voto
ListeElectorale[] listes = new ListeElectorale[25];
// le 25 liste avranno lo stesso numero di voti (4%)
for (int i = 0; i < listes.length; i++) {
listes[i] = new ListeElectorale("Liste" + (i + 1), 1, 0, false);
}
// calcolo dei seggi - normalmente si dovrebbe ottenere un ElectionsException
// con una soglia elettorale del 5%
BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
}
/**
* enregistrement des résultats de l'élection
*
* @throws JsonProcessingException
*/
@Test
public void ecritureResultatsElections() throws JsonProcessingException {
// si crea la tabella delle 7 liste candidate
ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// si fissano in modo definitivo i voti
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);
// si calcolano i seggi ottenuti da ciascuna lista
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// si visualizzano i risultati
for (int i = 0; i < listes.length; i++) {
System.out.println(mapper.writeValueAsString(listes[i]));
}
// si salvano i risultati nel database
BlockingObservable.from(electionsMetier.recordResultats(admin, listes)).firstOrDefault(null);
// si verificano i risultati
listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// si visualizzano i risultati
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());
}
}
Esaminiamo le modifiche:
- riga 48: il metodo statico [BlockingObservable.from(Observable).first]:
- si abbona al parametro osservabile di [from];
- avvia l'esecuzione del codice associato all'osservabile;
- attende di ricevere il primo risultato. Si tratta quindi di un'operazione sincrona;
Qui utilizziamo il metodo [firstOrDefault(null)] perché l’osservabile [metier.authenticate] non restituisce alcun risultato quando viene eseguito. Il risultato del metodo [firstOrDefault(null)] sarà quindi null, valore qui non utilizzato;
Riprendiamo questo schema nel resto del codice ogni volta che vogliamo richiamare il livello [métier].
Il test unitario [Test01] deve superare il test:
![]() |
Lavoro da svolgere: verificare che il test [Test01] superi il test.
20.1.4.2. Test02
Modifichiamo il test [Test01] in modo da testare ora l'interfaccia asincrona [IRxElectionsMetier] effettuando chiamate asincrone ai suoi metodi.
Esaminiamo un primo test:
// semaforo di sincronizzazione dei thread
private CountDownLatch latch;
// -----------------------------------
private ElectionsException checkUserUserException;
@Test()
public void checkUserUser() throws InterruptedException {
// semaforo a 1
latch = new CountDownLatch((1));
// operazione asincrona
electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
.subscribe((result) -> {
},
(th) -> {
checkUserUserException = (ElectionsException) th;
latch.countDown();
},
() -> {
latch.countDown();
});
// attesa semaforo
latch.await();
// verifica dei risultati
Assert.assertNotNull(checkUserUserException);
Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
- riga 2: un semaforo è uno strumento utilizzato per sincronizzare i thread tra loro. I thread sono flussi di esecuzione che si eseguono in parallelo. Per eseguire un’attività T1, il thread [Thread1] potrebbe aver bisogno che un’attività T2, eseguita da un thread [Thread2], sia terminata. Attende quindi che il thread [Thread2] gli invii un segnale che indichi che il task T2 è terminato. Esistono diversi modi per gestire questa sincronizzazione tra due thread. Il metodo utilizzato in questo caso è il seguente:
- riga 10: il thread [Thread1] crea un semaforo con valore 1;
- riga 12: il thread [Thread1] crea e avvia un thread [Thread2]. Ciò si ottiene con la sintassi:
electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
Il metodo [Observable.subscribeOn] imposta il thread su cui verrà eseguito il processo osservato. Il parametro di [subscribeOn] è un pool di thread. La libreria RxJava ne fornisce diversi, adatti a diverse situazioni. Il pool [Schedulers.io()] è quello consigliato per le operazioni di rete;
- (continua)
- righe 12-13: l'operazione
electionsMetier.authenticate(user).subscribeOn(Schedulers.io()).subscribe(...)
esegue l'operazione sincrona incapsulata nell'osservabile [authenticate(user)]. Tuttavia, poiché questa operazione sincrona viene avviata su un thread diverso dal thread [Thread1], quest’ultimo non attende la risposta del metodo [subscribe] e passa all’istruzione successiva;
- (continua)
- riga 23: il thread [Thread1] si arresta e attende che il semaforo passi a 0 (al momento è a 1);
- righe 13-21: il metodo [subscribe] accetta come parametri tre funzioni lambda:
- la prima, [(result)->{...}], viene chiamata ogni volta che l’osservabile [authenticate(user)] emette un risultato [result]. In questo caso abbiamo un osservabile [authenticate(user)] che esegue un’operazione ma non emette alcun risultato. La funzione lambda [(result)->{}] non verrà quindi mai chiamata. Ecco perché il suo codice è qui vuoto ([{}]);
- il secondo, [(th)->{...}], riceve come parametro un tipo [Throwable]. Viene chiamato quando l’esecuzione dell’osservabile incontra un’eccezione. Qui, trattiamo il parametro [Throwable th] nel modo seguente:
- riga 16: lo memorizziamo in un campo della classe di test di tipo [ElectionsException] poiché l’osservabile in esecuzione genera solo questo tipo di eccezione;
- riga 17: impostiamo il semaforo a 0 per indicare che il thread [Thread2] ha terminato il proprio lavoro;
- il terzo [()->{...}] viene chiamato quando l'osservabile non ha più elementi da emettere. Gestiamo questo evento nel modo seguente:
- riga 20: impostiamo il semaforo a 0 per indicare che il thread [Thread2] ha terminato il proprio lavoro;
Va notato che la terza funzione lambda non viene chiamata se si verifica un'eccezione. Per questo motivo, è stato necessario impostare il semaforo a 0 anche alla riga 17;
- riga 25: quando si arriva a questa riga, l’observable ha terminato il proprio lavoro. È quindi possibile effettuare le stesse verifiche del test [Test01];
Esaminiamo un altro test:
// -----------------------------------
private ElectionsException calculSieges1Exception;
private ListeElectorale[] listesCalculSieges1;
@Test
public void calculSieges1() throws InterruptedException {
// si crea la tabella delle 7 liste candidate
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);
// semaforo a 1
latch = new CountDownLatch((1));
// operazione asincrona
// si calcolano i seggi di ciascuna lista
electionsMetier.calculerSieges(admin, listes).subscribeOn(Schedulers.io())
.subscribe((result) -> {
listesCalculSieges1 = result;
},
(th) -> {
calculSieges1Exception = (ElectionsException) th;
latch.countDown();
},
() -> {
latch.countDown();
});
// attesa semaforo
latch.await();
// si verificano i risultati
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());
}
- righe 20-30: esecuzione asincrona dell’osservabile [electionsMetier.calculerSieges(admin, listes)];
- righe 21-23: l’esecuzione dell’osservabile restituisce un tipo [ListeElectorale[]] che viene memorizzato in un campo della classe di test, riga 3;
- righe 34-48: queste verifiche corrispondono a quelle del test [Test01], alle quali è stata aggiunta la verifica della riga 34 che assicura che non si sia verificata alcuna eccezione;
L'intero test [Test02] è disponibile nel materiale didattico.
Compito da svolgere: eseguire il test [Test02] e verificare che venga superato.
20.1.4.3. Test03
Il test [Test03] fa la stessa cosa del test [Test01]: verifica l’interfaccia [IRxElectionsMetier] tramite chiamate sincrone a tale interfaccia. Si tratta di una copia del test [Test02] con due differenze:
- gli osservabili non vengono più eseguiti in un thread diverso da quello che esegue i test. Quando il thread [Thread1] esegue il metodo [subscribe] di un osservabile, quest’ultimo avvia un’operazione HTTP verso il server, anch’essa sul thread [Thread1]. L’intero metodo [subscribe] diventa quindi sincrono;
- poiché rimane un solo thread, la sincronizzazione dei thread diventa superflua e il semaforo scompare;
Ecco due esempi di test:
// -----------------------------------
private ElectionsException checkUserUserException;
@Test()
public void checkUserUser() throws InterruptedException {
// operazione sincrona
electionsMetier.authenticate(user)
.subscribe((result) -> {
},
(th) -> {
checkUserUserException = (ElectionsException) th;
},
() -> {
});
// verifica dei risultati
Assert.assertNotNull(checkUserUserException);
Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
- riga 7: per impostazione predefinita, il metodo [electionsMetier.authenticate(user).subscribe] viene eseguito nel thread del codice chiamante. Si ha quindi un'operazione sincrona;
// -----------------------------------
private ElectionsException calculSieges1Exception;
private ListeElectorale[] listesCalculSieges1;
@Test
public void calculSieges1() throws InterruptedException {
// si crea la tabella delle 7 liste candidate
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);
// operazione sincrona
// si calcolano i seggi di ciascuna lista
electionsMetier.calculerSieges(admin, listes)
.subscribe((result) -> {
listesCalculSieges1 = result;
},
(th) -> {
calculSieges1Exception = (ElectionsException) th;
},
() -> {
});
// si verificano i risultati
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());
}
Compito da svolgere: eseguire il test [Test03] e verificare che superi il test.
20.2. Fase 2
Ora convertiamo l’applicazione console sincrona del capitolo 17.5 in un’applicazione sempre sincrona ma che utilizza l’interfaccia asincrona [RxJava, metier, DAO];
![]() |
Partiamo dal progetto [elections-console-metier-dao-security-webjson] [1] del capitolo 17.5, che duplichiamo in un nuovo progetto [elections-console-rxjava- metier-dao-security-webjson] [2]:
![]() | ![]() |
- in [3-4]; nel nuovo progetto eliminiamo la dipendenza dal vecchio livello sincrono [métier];
![]() | ![]() |
- in [5-9], si aggiunge una dipendenza dal nuovo livello asincrono [métier];
![]() | ![]() |
- in [10-14], si rinomina la classe [ElectionsConsole] in [ElectionsConsole01];
Allo stesso modo, si rinomina la classe [BootElectionsConsole] in [BootElectionsConsole01]:
![]() |
Il codice attuale della classe [BootElectionsConsole01] è il seguente:
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);
}
}
- riga 13: poiché il nome della classe [ElectionsConsole] è stato modificato in [ElectionsConsole01], ora occorre scrivere:
return ctx.getBean("electionsConsole01",IElectionsUI.class);
Torniamo al codice della classe [ElectionsConsole01]:
@Component
public class ElectionsConsole01 implements IElectionsUI {
@Autowired
private IElectionsMetier electionsMetier;
@Autowired
private User admin;
@Override
public void run() {
// le liste in lizza
ListeElectorale[] listes;
// inserimento dei dati
try (Scanner clavier = new Scanner(System.in)) {
// si richiedono le liste in competizione al livello [metier]
listes = electionsMetier.getListesElectorales(admin);
...
// si effettua il calcolo dei seggi
listes=electionsMetier.calculerSieges(admin,listes);
// si registrano i risultati
electionsMetier.recordResultats(admin,listes);
...
}
Se si segue l’esempio del test [Test01] del paragrafo 20.1.4.1, le righe 5, 17, 20 e 22 subiranno le seguenti modifiche:
@Component
public class ElectionsConsole01 implements IElectionsUI {
@Autowired
private IRxElectionsMetier electionsMetier;
@Autowired
private User admin;
@Override
public void run() {
// le liste in lizza
ListeElectorale[] listes;
// inserimento dei dati
try (Scanner clavier = new Scanner(System.in)) {
// si richiedono le liste in lizza dal livello [metier]
listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
...
// si effettua il calcolo dei seggi
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// si registrano i risultati
BlockingObservable.from(electionsMetier.recordResultats(admin, listes));
...
}
Compito da svolgere: configurare il progetto per eseguire la classe [BootElectionsConsole01] con i tre parametri [SS, Heures travaillées, Jours travaillés] e verificare che l'esecuzione del progetto così configurato dia i risultati attesi.
Compito da svolgere: configurare il progetto per eseguire la coppia [BootElectionsConsole02, ElectionsConsole02], dove la classe [ElectionsConsole02] sarà stata scritta seguendo il modello del test [Test02] del paragrafo 20.1.4.2.
Compito da svolgere: configurare il progetto per eseguire la coppia [BootElectionsConsole03, ElectionsConsole03], dove la classe [ElectionsConsole03] sarà stata scritta seguendo il modello del test [Test03] del paragrafo 20.1.4.3.
20.3. Fase 3
Passiamo ora al porting dell’applicazione Swing in un ambiente asincrono.
![]() |
Iniziamo duplicando il progetto [elections-swing-metier-dao-security-webjson] [1] del capitolo 17.6 in un nuovo progetto [elections-swing-rxjava-metier-dao-security-webjson] [2]:
![]() | ![]() |
- in [3, 4], eliminiamo la dipendenza dal livello sincrono [console];
![]() | ![]() |
- in [5-9], aggiungiamo una dipendenza dal livello console asincrono;
Il livello [swing] effettuerà vere e proprie chiamate asincrone al livello [métier]. Quando si chiama un metodo di quest'ultimo, ci saranno due thread:
- il thread di UI, che gestisce gli eventi;
- un thread di I/O che eseguirà la chiamata HTTP al server;
Per tutta la durata della chiamata asincrona, dovremmo visualizzare un'immagine di attesa e un pulsante di annullamento. In questa sede non lo faremo, ma vi verrà proposto come miglioramento dell’applicazione. Le modifiche riguardano le due classi che effettuano le chiamate al livello [métier]:
![]() |
20.3.1. Configurazione Maven
In questo caso utilizzeremo la libreria [RxSwing], che aggiunge alla libreria [RxJava] funzionalità disponibili solo in un ambiente Swing. A tal fine, modifichiamo il file [pom.xml] come segue:
<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>
<!-- livelli inferiori -->
<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. La classe [ElectionsConnectForm]
In un funzionamento asincrono, la classe [ElectionsConnectForm] diventa la seguente:
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;
// riferimento al livello asincrono [métier]
@Autowired
private IRxElectionsMetier metier;
// utente connesso
private User user;
// modulo principale
@Autowired
private ElectionsMainForm electionsMainForm;
// sessione UI
@Autowired
private UiSession uiSession;
@Override
protected void doConnect() {
if (isPageValid()) {
// autenticazione dell'utente
metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(
// nessuna risposta
(result) -> {
},
// gestione dell'eccezione
(th) -> {
// si registra l'errore
String info = getInfoForException("Les erreurs suivantes se sont produites :", th);
// viene visualizzata l'informazione
jTextPaneErreurs.setText(info);
jTextPaneErreurs.setCaretPosition(0);
},
// l'autenticazione è terminata
() -> {
// l'utente viene memorizzato nella sessione
uiSession.setUser(user);
// la schermata di accesso viene nascosta
setVisible(false);
// viene visualizzata la pagina principale
electionsMainForm.run();
});
}
}
// inizializzazioni
@Override
protected void init() {
...
}
@Override
public void run() {
// viene visualizzata l'interfaccia grafica
SwingUtilities.invokeLater(new Runnable() {
public void run() {
init();
setVisible(true);
}
});
}
private boolean isPageValid() {
...
}
private String getInfoForException(String message, Throwable ex) {
...
}
}
- righe 36-63: il metodo [doConnect] viene eseguito quando l'utente seleziona l'opzione di menu [Connexion]:
![]() |
Tutto sta nella riga 40:
metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(...)
- il processo osservato è [metier.authenticate(user)];
- verrà eseguito su un thread di I/O prelevato dal pool [Schedulers.io()];
- sarà monitorato nel thread UI, quello che gestisce gli eventi dell’interfaccia Swing [observeOn(SwingScheduler.getInstance())]. Questo thread viene ottenuto tramite il metodo [SwingScheduler.getInstance()], dove [SwingScheduler] è una classe fornita dalla libreria [RxSwing]. Ciò è obbligatorio. Una volta ottenuto il risultato dell'operazione asincrona, questo viene spesso utilizzato per modificare elementi dell'interfaccia Swing. Tuttavia, quest'ultima può essere modificata solo nel thread di UI, altrimenti si verifica un'eccezione. È quindi necessario che le righe 41-61 vengano eseguite nel thread di UI. Ciò è garantito qui dal metodo [observeOn(SwingScheduler.getInstance())];
Commentiamo il resto del codice:
- righe 42-43: queste righe servono a rispettare la sintassi del metodo [subscribe]. Non verranno mai eseguite poiché il processo [metier.authenticate(user)] non restituisce alcun risultato;
- righe 35-52: al ricevimento di un'eccezione, questa viene visualizzata;
- righe 54-61: vengono eseguite quando il processo [metier.authenticate(user)] segnala la fine delle sue emissioni;
20.3.3. La classe [ElectionsMainForm]
![]() |
20.3.3.1. Inizializzazione dell'interfaccia grafica
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;
// riferimento al livello asincrono [métier]
@Autowired
private IRxElectionsMetier metier;
// sessione UI
@Autowired
private UiSession uiSession;
// utente connesso
private User user;
// modelli delle liste JList
private DefaultListModel<String> modèleNomsVoix = null;
private DefaultListModel<String> modèleRésultats = null;
// elenchi in competizione
private ListeElectorale[] listes;
// elenchi inseriti dall'utente
private final List<ListeElectorale> listesSaisies = new ArrayList<>();
private ListeElectorale[] tListesSaisies;
// inizializzazioni
@Override
protected void init() {
// generazione dei componenti da parte della classe padre
super.init();
// stato del modulo
Utilitaires.setEnabled(new JLabel[]{jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer}, false);
Utilitaires.setEnabled(
new JMenuItem[]{jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer}, false);
// centrare la finestra
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);
// utente connesso
user = uiSession.getUser();
// inizializzazioni locali
modèleNomsVoix = new DefaultListModel<>();
jListNomsVoix.setModel(modèleNomsVoix);
modèleRésultats = new DefaultListModel<>();
jListResultats.setModel(modèleRésultats);
// richiesta degli elenchi al livello [métier]
metier.getListesElectorales(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// risposta
listesElectorales -> {
// si memorizzano gli elenchi
listes = listesElectorales;
},
// eccezione
(th) -> showException(th),
// fine osservabile
() -> {
// fase successiva
doInitStep2();
});
}
...
- riga 46: il metodo [init] viene eseguito quando sta per essere visualizzata la finestra associata. Ha lo scopo di inizializzare i componenti [1-3] riportati di seguito:
![]() |
- righe 71-85: vengono richieste in modo asincrono le liste dei candidati (componente [1]);
- riga 71: il processo osservato è [metier.getListesElectorales(user)]. Viene eseguito su un thread di I/O [subscribeOn(Schedulers.io())] e osservato sul thread di UI [observeOn(SwingScheduler.getInstance()];
- righe 74-77: il risultato restituito dal processo osservato viene memorizzato nel campo [listes] della riga 38;
- riga 79: l'eventuale eccezione viene gestita dal seguente metodo:
private void showException(Throwable th) {
// visualizzazione dell'eccezione
jTextPaneMessages.setText(getInfoForException("Les erreurs suivantes se sont produites : ", th));
jTextPaneMessages.setCaretPosition(0);
}
- righe 81-84: al termine del processo osservato, vengono eseguite le righe 81-84. Queste righe non vengono eseguite se si è verificata un'eccezione. Il metodo [doInitStep2] garantisce la fase 2 dell'inizializzazione nel modo seguente:
private void doInitStep2() {
// si associano i nomi delle liste al menu a tendina jComboBoxNomsListes
for (int i = 0; i < listes.length; i++) {
jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
}
// numero di posti da assegnare
metier.getNbSiegesAPourvoir(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// risposta
nbSiegesAPourvoir -> {
// si inizializza l'etichetta associata a questa informazione
jLabelSAP.setText(jLabelSAP.getText() + nbSiegesAPourvoir);
},
// eccezione
(th) -> showException(th),
// fine osservabile
() -> {
// fase successiva
doInitStep3();
});
}
- righe 3-5: si utilizza il risultato della fase precedente per compilare l'elenco a discesa con i nomi delle liste dei candidati;
- righe 7-20: si richiede il numero di seggi da assegnare in modo asincrono;
- riga 7: il processo osservato è [metier.getNbSiegesAPourvoir(user)]. Viene eseguito su un thread di I/O [subscribeOn(Schedulers.io())] e osservato sul thread di UI [observeOn(SwingScheduler.getInstance()];
- righe 10-13: il risultato restituito dal processo viene utilizzato per aggiornare l'interfaccia grafica;
- riga 15: viene visualizzata l'eventuale eccezione;
- righe 17-20: alla ricezione del segnale di fine dell'osservabile, si passa alla fase 3 del processo di inizializzazione;
La fase 3 dell'inizializzazione è gestita dal seguente codice:
private void doInitStep3() {
// soglia elettorale
metier.getSeuilElectoral(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// risposta
seuilElectoral -> {
// si inizializza l'etichetta associata a questa informazione
jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
},
// eccezione
(th) -> showException(th),
// fine osservabile
() -> {
});
}
- righe 3-4: si richiede la soglia elettorale in modo asincrono;
- riga 3: il processo osservato è [metier.getSeuilElectoral(user)]. Viene eseguito su un thread di I/O [subscribeOn(Schedulers.io())] e osservato sul thread UI [observeOn(SwingScheduler.getInstance()];
- righe 6-9: il risultato restituito dal processo viene utilizzato per aggiornare l'interfaccia grafica;
- riga 11: viene visualizzata l'eventuale eccezione;
- righe 13-14: alla ricezione del segnale di fine dell'osservabile, non viene eseguita alcuna operazione: il processo di inizializzazione dell'interfaccia grafica è terminato;
20.3.3.2. Calcolo dei seggi ottenuti dalle diverse liste
Il metodo [doCalculer] ha la funzione di calcolare il numero di seggi ottenuti dalle diverse liste:
@Override
protected void doCalculer() {
tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
// calcolo dei seggi
String info = null;
metier.calculerSieges(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// elaborazione del risultato
result -> consumeResultSieges(result),
// elaborazione dell'eccezione
th -> showException(th),
// fine osservabile
() -> {
}
);
}
- righe 6-15: si calcolano in modo asincrono i seggi ottenuti dalle diverse liste;
- riga 6: il processo osservato è [metier.calculerSieges(user, tListesSaisies)]. Viene eseguito su un thread di I/O [subscribeOn(Schedulers.io())] e osservato sul thread di UI [observeOn(SwingScheduler.getInstance()];
- riga 9: il risultato restituito dal processo viene utilizzato dal metodo [consumeResultSieges];
- riga 11: viene visualizzata l'eventuale eccezione;
- righe 13-14: alla ricezione del segnale di fine dell’osservabile, non viene eseguita alcuna operazione;
Riga 9: il metodo [consumeResultSieges] utilizza il risultato restituito dal processo osservato, gli elenchi dei candidati con i relativi campi [sieges, elimine] aggiornati:
private void consumeResultSieges(ListeElectorale[] tListesSaisies) {
// salvataggio del risultato
this.tListesSaisies = tListesSaisies;
// visualizzazione dei risultati
modèleRésultats.clear();
for (int i = 0; i < tListesSaisies.length; i++) {
modèleRésultats.addElement(tListesSaisies[i].toString());
}
// aggiornamento stato modulo
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é");
}
- righe 4-14: il risultato ottenuto viene utilizzato per aggiornare l’interfaccia grafica;
20.3.3.3. Registrazione dei risultati elettorali
La registrazione dei risultati elettorali avviene tramite il seguente metodo [doEnregistrer]:
@Override
protected void doEnregistrer() {
// richiesta di registrazione al livello [métier]
metier.recordResultats(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// elaborazione del risultato - qui non ce n'è
(param) -> {
},
// elaborazione dell'eccezione
(th) -> showException(th),
// fine osservabile
() -> {
// aggiornamento del modulo
Utilitaires.setEnabled(new JLabel[]{jLabelEnregistrer}, false);
Utilitaires.setEnabled(new JMenuItem[]{jMenuItemEnregistrer}, false);
jTextPaneMessages.setText("Enregistrement des résultats réalisé");
}
);
}
- righe 4-17: i risultati delle elezioni vengono registrati in modo asincrono;
- riga 4: il processo osservato è [metier.recordResultats(user, tListesSaisies)]. Viene eseguito su un thread di I/O [subscribeOn(Schedulers.io())] e osservato sul thread di UI [observeOn(SwingScheduler.getInstance()];
- righe 7-8: queste righe non verranno mai eseguite poiché il processo osservato non restituisce alcun risultato;
- riga 10: viene visualizzata l'eventuale eccezione;
- righe 14-16: alla ricezione del segnale di fine dell’osservabile, si aggiorna l’interfaccia grafica;
Compito da svolgere: verificare che l’applicazione Swing funzioni. Successivamente, modificare l’interfaccia grafica e il codice in modo che, durante un’operazione asincrona con il server web / jSON, compaia un’immagine di attesa e un’opzione per annullare l’operazione in corso.























