8. RxJava nell’ambiente Swing
8.1. Introduction
Torneremo ora sull’applicazione Swing presentata nel paragrafo 2.
![]() |
Per lavorare con RxJava in un ambiente Swing, utilizzeremo la libreria RxSwing che aggiunge a RxJava classi e interfacce utili in un ambiente Swing. A tal fine, il file Gradle dell’esempio Swing è il seguente:
![]() |
buildscript {
repositories {
mavenCentral()
}
}
apply plugin: 'java'
jar {
baseName = 'exemples-01'
version = '0.0.1-SNAPSHOT'
}
repositories {
mavenCentral()
}
dependencies {
compile('io.reactivex:rxswing:0.25.0')
compile('io.reactivex:rxjava:1.1.3')
compile('com.fasterxml.jackson.core:jackson-databind:2.7.3')
}
task wrapper(type: Wrapper) {
gradleVersion = '2.9'
}
- riga 15: dipendenza da RxSwing;
Utilizzeremo un unico oggetto specifico di RxSwing: lo scheduler [SwingScheduler.getInstance()] che esegue/osserva gli osservabili sul thread dell'event loop di Swing. Lo useremo esclusivamente per monitorare gli osservabili in esecuzione su thread diversi da quello dell’event loop. Ricordiamo l’architettura dell’applicazione di esempio:

- il livello di servizio asincrono presenta metodi che restituiscono osservabili. Eseguiamo questi osservabili in thread diversi da quello dell’event loop. In questo modo l’interfaccia grafica non rimane bloccata, ma può reagire alle richieste dell’utente. La più ovvia è quella di consentire all’utente di cliccare su un pulsante [Annuler] per interrompere un’operazione asincrona troppo lunga. Affinché ciò sia possibile, è sufficiente che l’interfaccia grafica sia bloccata (frozen);
- il livello Swing intende utilizzare i risultati restituiti dalle operazioni asincrone e, sulla base di essi, aggiornare l’interfaccia grafica. Tuttavia, ciò può avvenire solo nel thread dell’event loop. A tal fine, tali risultati vengono monitorati nello scheduler [SwingScheduler.getInstance()];
Pertanto, nel codice di gestione degli eventi dell’interfaccia grafica, l’interazione con il livello asincrono [rxService] avviene nella forma seguente:
Observable obs=rxService.doSomething(...).subscribeOn(Schedulers.computation()).observeOn(SwingScheduler.getInstance()) ;
dove lo scheduler [Schedulers.computation()] potrà essere sostituito da un altro scheduler a seconda dei casi d’uso.
Si invita il lettore a rileggere il paragrafo 2. Ora dispone delle conoscenze necessarie per comprenderlo appieno.
8.2. La struttura del codice
Il codice implementa la seguente architettura:

Il progetto IntelliJ IDEA che implementa questa architettura è il seguente:
![]() |
- il pacchetto [rxswing.service] implementa i livelli di servizio sincroni (IService, Service) e asincroni (IRxService, RxService);
- il pacchetto [rxswing.ui] implementa l'interfaccia Swing;
8.3. Esecuzione del progetto
Per eseguire il progetto in IntelliJ IDEA, procedere come segue:
![]() |
8.4. Il servizio sincrono

![]() |
Il livello di servizio sincrono presenta la seguente interfaccia [IService]:
package dvp.rxswing.service;
public interface IService {
// numeri casuali nell'intervallo [a,b]
// vengono generati n numeri, dove n è a sua volta un numero casuale nell'intervallo [minCount, maxCount]
// i numeri vengono generati dopo un ritardo di delay millisecondi,
// dove [delay] è a sua volta un numero casuale nell'intervallo [minDelay, maxDelay]
public ServiceResponse getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}
Il tipo [ServiceResponse] della risposta del servizio è il seguente:
package dvp.rxswing.service;
import java.util.List;
public class ServiceResponse {
// tempo di attesa del servizio
private int delay;
// numeri casuali
private List<Integer> aleas;
// thread di esecuzione
private String executedOn;
// costruttori
public ServiceResponse() {
// thread di esecuzione
executedOn = Thread.currentThread().getName();
}
public ServiceResponse(int delay, List<Integer> aleas) {
// costruttore locale
this();
// altre inizializzazioni
this.delay = delay;
this.aleas = aleas;
}
// getter e setter
...
}
L'interfaccia [IService] è implementata dalla seguente classe [Service]:
package dvp.rxswing.service;
import java.util.*;
public class Service implements IService {
@Override
public ServiceResponse getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
// numeri casuali nell'intervallo [a,b]
// vengono generati n numeri, dove n stesso è un numero casuale nell'intervallo [minCount, maxCount]
// i numeri vengono generati dopo un ritardo di delay millisecondi,
// dove [delay] è a sua volta un numero casuale nell'intervallo [minDelay, maxDelay]
// alcune verifiche
List<String> messages = new ArrayList<>();
int erreur = 0;
if (a < 0) {
messages.add("Le nombre a de l'intervalle [a,b] de génération doit être supérieur à 0");
erreur |= 2;
}
if (a >= b) {
messages.add("Dans l'intervalle [a,b] de génération, on doit avoir a< b");
erreur |= 4;
}
if (minCount < 0) {
messages.add("Le nombre min de l'intervalle [min,count] du nombre de valeurs générées doit être supérieur à 0");
erreur |= 16;
}
if (minCount > maxCount) {
messages.add("Dans l'intervalle [min,count] du nombre de valeurs générées, on doit avoir min<= max");
erreur |= 32;
}
if (minDelay < 0) {
messages.add("Le nombre min de l'intervalle [min,count] du délai d'attente doit être supérieur à 0");
erreur |= 64;
}
if (minCount > maxCount) {
messages.add("Dans l'intervalle [min,count] du délai d'attente, on doit avoir min<= max");
erreur |= 128;
}
if (maxDelay > 5000) {
messages.add("L'attente en millisecondes avant la génération des nombres doit être dans l'intervalle [0,5000]");
erreur |= 256;
}
// errori?
if (!messages.isEmpty()) {
throw new AleasException(String.join(" [---] ", messages), erreur);
}
// generatore di numeri casuali
Random random = new Random();
// in attesa?
int delay = minDelay + random.nextInt(maxDelay - minDelay + 1);
if (delay > 0) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
throw new AleasException(String.format("[%s : %s]", e.getClass().getName(), e.getMessage()), 1024);
}
}
// generazione del risultato
int count = minCount + random.nextInt(maxCount - minCount + 1);
List<Integer> nombres = new ArrayList<>();
for (int i = 0; i < count; i++) {
nombres.add(a + random.nextInt(b - a + 1));
}
// restituzione del risultato
return new ServiceResponse(delay,nombres);
}
}
La classe di eccezione [AleasException] utilizzata dal servizio è la seguente:
package dvp.rxswing.service;
public class AleasException extends RuntimeException {
private static final long serialVersionUID = 1L;
// codice di errore
private int code;
// costruttori
public AleasException() {
}
public AleasException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public AleasException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
public AleasException(String detailMessage, Throwable throwable, int code) {
super(detailMessage, throwable);
this.code = code;
}
// getter e setter
...
}
- riga 3: estende la classe [RuntimeException]. Si tratta quindi di un'eccezione non controllata;
- riga 7: aggiunge un codice di errore alla sua classe padre (0 = nessun errore);
8.5. Il servizio asincrono

![]() |
Il livello di servizio asincrono presenta la seguente interfaccia [IRxService]:
package dvp.rxswing.service;
import dvp.rxswing.ui.UiResponse;
import rx.Observable;
public interface IRxService {
// numeri casuali nell'intervallo [a,b]
// vengono generati n numeri, dove n stesso è un numero casuale nell'intervallo [minCount, maxCount]
// i numeri vengono generati dopo un ritardo di delay millisecondi,
// dove [delay] è a sua volta un numero casuale nell'intervallo [minDelay, maxDelay]
public Observable<UiResponse> getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, UiResponse uiResponse);
}
- riga 11: il metodo [getAleas] del servizio restituisce ora un osservabile;
Il metodo [getAleas] restituisce una risposta di tipo [UiResponse] destinata al livello [Ui]. Questo tipo è il seguente:
package dvp.rxswing.ui;
import dvp.rxswing.service.ServiceResponse;
import java.text.SimpleDateFormat;
import java.util.Calendar;
public class UiResponse {
// ID del cliente
private int idClient;
// risposta del servizio
private ServiceResponse serviceResponse;
// nome del thread di osservazione
private String observedOn;
// ora della richiesta
private String requestAt;
// ora della risposta
private String responseAt;
// costruttori
public UiResponse() {
// thread di osservazione
observedOn = Thread.currentThread().getName();
// ora della richiesta
requestAt = getTimeStamp();
}
// metodi privati
private String getTimeStamp() {
return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
}
// getter e setter
...
}
- i numeri casuali si trovano nel campo della riga 13;
- gli altri campi servono a specificare i thread di esecuzione e di osservazione dell’osservabile del servizio asincrono, nonché gli orari della richiesta inviata al servizio e della risposta ottenuta;
L'interfaccia asincrona è implementata dalla seguente classe [RxService]:
package dvp.rxswing.service;
import dvp.rxswing.ui.UiResponse;
import rx.Observable;
public class RxService implements IRxService {
// servizio sincrono
private IService service;
// costruttore
public RxService(IService service) {
this.service = service;
}
@Override
public Observable<UiResponse> getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, UiResponse uiResponse) {
// si crea un osservabile che emette il valore restituito dal servizio sincrono
return Observable.create(subscriber -> {
try {
// chiamata sincrona
uiResponse.setServiceResponse(service.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
// si passa il risultato all'osservatore
subscriber.onNext(uiResponse);
} catch (Exception e) {
// si passa l'errore all'osservatore
subscriber.onError(e);
} finally {
// si segnala all'osservatore che le emissioni sono terminate
subscriber.onCompleted();
}
});
}
}
- righe 12-14: la classe [RxService] del servizio asincrono viene costruita a partire da un'istanza dell'interfaccia sincrona [IService];
- righe 20-33: creazione dell’osservabile, risultato del metodo [getAleas];
- riga 22: viene chiamato il metodo sincrono [service.getAleas]. Il suo risultato di tipo [ServiceResponse] viene incluso nell'oggetto di tipo [UiResponse] da fornire al livello [swing]. Tale oggetto è stato inizialmente passato nei parametri di chiamata del metodo (ultimo parametro, riga 17);
- riga 24: la risposta [UiResponse] viene inviata all’osservatore (il livello [swing]). L’oggetto [UiResponse] non contiene solo le informazioni generate dal servizio sincrono alla riga 22. Contiene anche altre informazioni generate dal metodo chiamante del metodo [getAleas] alla riga 17. È per questo motivo che il metodo chiamante ha passato l’oggetto [UiResponse] come parametro al metodo [getAleas] (ultimo parametro, riga 17);
- riga 30: non si dimentica di segnalare la fine delle trasmissioni. Qui si ha un osservabile che emette un solo valore: quello restituito dal servizio sincrono;
- riga 27: si segnala all’osservatore un eventuale errore;
8.6. L'interfaccia grafica

![]() |
- l'interfaccia grafica è stata realizzata con IDE [Netbeans], che dispone di un buon editor grafico. Questo editor ha generato il file [AbstractJFrameAleas.form] utilizzabile solo da questo IDE;
- la classe [AbstractJFrameAleas] è stata generata anch’essa dall’editor grafico di NetBeans. È stata poi rifattorizzata nel modo seguente: gli eventi dell’interfaccia grafica che si desiderava gestire vengono trattati nella classe [AbstractJFrameAleas] tramite metodi astratti implementati nella classe figlia [JFrameAleasEvents]. Alla fine,
- la classe astratta [AbstractJFrameAleas] si occupa di costruire e visualizzare l'interfaccia grafica;
- la classe figlia [JFrameAleasEvents] si occupa della gestione degli eventi della stessa;
I componenti dell'interfaccia grafica della scheda [Request] sono i seguenti:
![]() |
n. | tipo | nome | ruolo |
1 | JTabbedPane | jTabbedPane1 | un contenitore di schede. Contiene due schede (JPanel) [jPanelRequest] per la richiesta, [jPanelresponse] per la risposta; |
2 | JTextField | jTextFieldNbValeurs | il numero di richieste da inviare al servizio dei numeri casuali. Nel caso del servizio asincrono eseguito sullo scheduler [Schedulers.io], queste richieste condivideranno un processore; |
3 | JTextField | jTextFieldA | estremità a dell'intervallo [a,b] |
4 | JTextField | jTextFieldB | terminale b dell'intervallo [a,b] |
5 | JTextField | jTextFieldMinCount | terminale minCount dell'intervallo [minCount, maxCount] |
6 | JTextField | jTextFieldMaxCount | terminale maxCount dell'intervallo [minCount, maxCount] |
7 | JTextField | jTextFieldMinDelay | terminale minDelay dell'intervallo [minDelay, maxDelay] |
8 | JTextField | jTextFieldMaxDelay | terminale maxDelay dell'intervallo [minDelay, maxDelay] |
9 | JCheckBox | jCheckBoxRxSwing | se la casella è spuntata, le richieste vengono effettuate tramite l'interfaccia asincrona. In caso contrario, vengono effettuate tramite l'interfaccia sincrona |
10 | JComboBox | jComboBoxSchedulers | nel caso di richieste asincrone, queste verranno eseguite con lo scheduler qui selezionato |
11 | JButton | jButtonGenerate | avvia l'esecuzione delle richieste al servizio sincrono o asincrono |
I componenti dell'interfaccia grafica della scheda [Response] sono i seguenti:
![]() |
n. | tipo | nome | ruolo |
1 | JLabel | jLabelDuree | il tempo totale di esecuzione delle richieste in millisecondi |
2 | JLabel | jLabelNbReponses | il numero totale di risposte osservate (può essere diverso dal numero di richieste, poiché ogni richiesta può fornire più valori da osservare) |
3 | JList | jListNumbers | visualizzazione dei valori osservati (ricevuti) |
4 | JButton | jButtonAnnuler | Annulla le richieste in esecuzione |
8.7. Inizializzazione dell'interfaccia grafica
![]() |
La classe [JFrameAleasEvents] gestisce gli eventi dell'interfaccia grafica, in particolare il clic sul pulsante [Générer]. Si tratta di una classe eseguibile che viene avviata nel seguente contesto:
public class JFrameAleasEvents extends AbstractJFrameAleas {
private static final long serialVersionUID = 1L;
// servizio di generazione sincrona
private IService service;
// servizio di generazione asincrona
private IRxService rxService;
// i dati inseriti
private int nbRequests;
private int a;
private int b;
private int minDelay;
private int maxDelay;
private int minCount;
private int maxCount;
// messaggi di errore
private final String jLabelNbValuesErrorText = "Tapez un nombre entier >=1";
private final String jLabelCountErrorText = "minCount doit être >=0 et maxCount>=minCount ";
private final String jLabelDelayErrorText = "minDelay doit être >=0 et maxDelay>=minDelay et maxDelay<=5000";
private final String jLabelIntervalErrorText = "a doit être >=0 et b>=a ";
// iscrizioni agli osservabili
protected List<Subscription> subscriptions = new ArrayList<Subscription>();
// inizio-fine dell'esecuzione
private long debut;
// mappatore jSON
private ObjectMapper jsonMapper;
// modello delle risposte
private DefaultListModel<String> model;
// costruttore
public JFrameAleasEvents() {
// genitore
super();
// locale
initJFrame();
// servizi
service = new Service();
rxService = new RxService(service);
// mappatore jSON
jsonMapper = new ObjectMapper();
}
private void initJFrame() {
// si nascondono i messaggi di errore
jLabelCountError.setText("");
jLabelDelayError.setText("");
jLabelIntervalError.setText("");
jLabelNbValuesError.setText("");
// si nascondono i testi predefiniti
jTextFieldA.setText("100");
jTextFieldB.setText("200");
jTextFieldMinCount.setText("5");
jTextFieldMaxCount.setText("10");
jTextFieldMinDelay.setText("100");
jTextFieldMaxDelay.setText("500");
jTextFieldNbValeurs.setText("10");
jLabelDuree.setText("");
// modello delle risposte
model = new DefaultListModel<>();
jListNumbers.setModel(model);
// numero di core
System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
}
public static void main(String args[]) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException
| IllegalAccessException e) {
System.out.println(e);
System.exit(0);
}
/* Crea e visualizza il modulo */
java.awt.EventQueue.invokeLater(() -> {
new JFrameAleasEvents().setVisible(true);
});
}
- riga 1: la classe [JFrameAleasEvents] estende la classe [AbstractJFrameAleas], che a sua volta estende la classe Swing [JFrame]. La classe [JFrameAleasEvents] è quindi una finestra Swing;
- righe 68-75: il metodo [main] che verrà eseguito;
- riga 70: imposta il look and feel dell’interfaccia grafica;
- riga 79: viene chiamato il costruttore della classe [JFrameAleasEvents]: l'interfaccia grafica verrà costruita e inizializzata. Una volta fatto ciò, viene resa visibile;
- righe 34-44: il costruttore;
- riga 36: la chiamata al costruttore padre inizializzerà l'interfaccia grafica. A questo punto, essa è così come l'ha progettata lo sviluppatore. Non è ancora visibile;
- riga 38: alcuni componenti dell'interfaccia grafica vengono inizializzati;
- riga 40: istanziamento del servizio sincrono;
- riga 41: istanziamento del servizio asincrono;
8.8. Esecuzione delle richieste sincrone
Facendo clic sul pulsante [Générer] viene eseguito il seguente metodo [doGenerate]:
@Override
protected void doGenerate() {
// Dati inseriti validi?
if (!isPageValid()) {
return;
}
// rx o no?
if (jCheckBoxRxSwing.isSelected()) {
// richieste asincrone
doGenerateWithRxService();
} else {
// richieste sincrone
doGenerateWithService();
}
}
- righe 4-6: si verifica che i dati inseriti dall'utente siano validi. Non commenteremo il metodo [isPageValid]. È di base;
- riga 8: si verifica lo stato della casella di controllo RxSwing;
- riga 13: si eseguono le richieste in modo sincrono;
Il metodo [doGenerateWithService] è il seguente:
// generazione sincrona
private void doGenerateWithService() {
// inizio attesa
beginWaiting();
try {
for (int i = 0; i < nbRequests; i++) {
// preparazione della risposta
UiResponse uiResponse = new UiResponse();
// n. cliente
uiResponse.setIdClient(i);
// chiamata sincrona
uiResponse.setServiceResponse(service.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
// ora della risposta
uiResponse.setResponseAt();
// aggiornamento del modello JList con le risposte ricevute
model.add(0, jsonMapper.writeValueAsString(uiResponse));
// aggiornamento del numero di risposte
jLabelNbReponses.setText(String.valueOf(Integer.parseInt(jLabelNbReponses.getText()) + 1));
}
} catch (JsonProcessingException | RuntimeException e) {
JOptionPane.showMessageDialog(this, getInfoForThrowable("L'erreur suivante s'est produite", e), "Informations",
JOptionPane.PLAIN_MESSAGE);
}
// fine attesa
endWaiting();
}
- riga 12: chiamata sincrona al servizio di generazione di numeri casuali;
- l'esecuzione del metodo [doGenerateWithService] avviene interamente nel thread dell'event loop di Swing. Finché il metodo non è terminato, l'interfaccia grafica non elabora alcun nuovo evento. Rimane bloccata (frozen). Pertanto, ad esempio, gli aggiornamenti dell'interfaccia grafica alle righe 16 e 18 non saranno mai visualizzati. Saranno visibili solo con i loro valori finali, al termine dell'esecuzione di tutte le richieste;
Il metodo [beginWaiting] (riga 4) è il seguente:
private void beginWaiting() {
// pulsanti
jButtonGenerate.setVisible(false);
jButtonCancel.setVisible(true);
// cursore di attesa
jTabbedPane1.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
jButtonCancel.setCursor(Cursor.getDefaultCursor());
// azzeramento risposte
model.clear();
// abbonamenti Rx
subscriptions.clear();
// viene visualizzata la vista delle risposte
jTabbedPane1.setSelectedIndex(1);
jLabelNbReponses.setText("0");
jLabelDuree.setText("");
// inizio esecuzione
debut = new Date().getTime();
}
- riga 3: il pulsante [Générer] è nascosto. Ciò genera un evento che, a sua volta, potrà essere eseguito solo al termine dell’esecuzione di tutte le richieste. Pertanto non lo si vede mai nascosto, poiché il metodo [endWaiting] alla riga 25 del metodo [doGenerateWithService] lo visualizza nuovamente;
- riga 13: si seleziona la scheda [Response] per visualizzare le risposte in arrivo. Anche in questo caso, l’evento verrà eseguito solo al termine dell’esecuzione di tutte le richieste, quando saranno visibili tutte le risposte contemporaneamente, mentre si desiderava vederle arrivare una dopo l’altra;
L'interfaccia sincrona presenta chiaramente delle carenze. Queste vengono superate grazie all'interfaccia asincrona.
8.9. Esecuzione delle richieste asincrone
Il codice per l’esecuzione delle richieste asincrone è il seguente:
private void doGenerateWithRxService() {
// inizio attesa
beginWaiting();
// si otterranno i numeri casuali sotto forma di un osservabile
Observable<UiResponse> observable = Observable.empty();
// Scheduler di esecuzione dei diversi osservabili
Scheduler[] schedulers = { Schedulers.io(), Schedulers.computation(), Schedulers.newThread(),
Schedulers.trampoline(), Schedulers.immediate() };
Scheduler scheduler = schedulers[jComboBoxSchedulers.getSelectedIndex()];
// configurazione degli osservabili
for (int i = 0; i < nbRequests; i++) {
// preparazione della risposta
UiResponse uiResponse = new UiResponse();
uiResponse.setIdClient(i);
// L'osservabile è configurato per essere eseguito sullo scheduler scelto dall'utente
// quindi sommatoria dell'osservabile ottenuto con l'osservabile complessivo
observable = observable.mergeWith(
rxService.getAleas(a, b, minCount, maxCount, minDelay, maxDelay, uiResponse).subscribeOn(scheduler));
}
// osservatore
observable = observable.observeOn(SwingScheduler.getInstance());
// per ora ci siamo limitati alla configurazione
// non è stata ancora effettuata alcuna richiesta al servizio sincrono di generazione di numeri casuali
// ci si abbona all'osservabile: è questo che provocherà la chiamata al servizio sincrono di generazione di numeri casuali
try {
// qui c’è solo un abbonamento: il risultato è una sottoscrizione
subscriptions.add(observable.subscribe(
// notifica di emissione
uiResponse -> {
// si aggiorna l'interfaccia utente con la risposta
// ciò è possibile poiché l'operazione avviene nel thread dell'interfaccia utente
updateUi(uiResponse);
} ,
// notifica di errore
th -> {
// caso di errore - lo si visualizza
String message = getInfoForThrowable("L'erreur suivante s'est produite", th);
JOptionPane.showMessageDialog(this, message, "Informations", JOptionPane.PLAIN_MESSAGE);
// Annullamento delle richieste
doCancel();
} ,
// notifica [onCompleted]
// fine dell'attesa
this::endWaiting));
} catch (Throwable th) {
// caso di eccezione + generale - visualizzarlo
String message = getInfoForThrowable("L'erreur suivante s'est produite", th);
JOptionPane.showMessageDialog(this, message, "Informations", JOptionPane.PLAIN_MESSAGE);
// si annullano le richieste
doCancel();
}
}
- riga 3: l’interfaccia grafica viene modificata per indicare che è in corso un’operazione potenzialmente lunga;
- riga 5: viene creato un osservabile vuoto. È questo osservabile che verrà monitorato dal livello [swing];
- riga 7: l'array dei possibili scheduler;
- riga 9: abbiamo dato all’utente la possibilità di scegliere lo scheduler su cui eseguire le richieste. Recuperiamo lo scheduler da lui scelto;
- righe 11-19: ciascuna delle query restituisce un osservabile i cui elementi vengono accumulati (mergeWith) (riga 17) nell’osservabile della riga 5;
- righe 13-14: viene costruito l’oggetto [UiResponse]. Ricordiamo che questo oggetto è sia il parametro di input del metodo [RxService.getAleas] sia il suo risultato (righe 17-18);
- riga 14: ogni richiesta è identificata dal proprio numero, qui denominato [idClient]. Ciò è necessario poiché in un ambiente asincrono l’ordine di ricezione delle risposte può differire dall’ordine di invio delle richieste. [idClient] consente di sapere a quale richiesta appartiene la risposta;
- righe 17-18: viene effettuata la richiesta asincrona [rxService.getAleas]. Viene eseguita sullo scheduler scelto dall’utente. Il suo risultato di tipo Observable<UiResponse> viene accumulato con l’observable della riga 5. È importante tenere presente che il metodo [rxService.getAleas] viene qui eseguito e restituisce un osservabile. Ciò non significa tuttavia che siano stati generati numeri casuali. Infatti, un osservabile viene eseguito solo quando ci si abbona ad esso. Questo non è ancora avvenuto;
- riga 21: questa è l’istruzione importante: si richiede che l’osservazione degli elementi emessi dall’osservabile della riga 5 avvenga sul thread dell’interfaccia utente. Qui si utilizza uno scheduler specifico della libreria RxSwing;
- righe 25-51: ci si abbona all’osservabile della riga 5. È solo ora che i numeri casuali verranno richiesti al servizio sincrono di generazione di tali numeri. L’essenziale si trova nelle istruzioni delle righe 29-33. Il resto gestisce essenzialmente i casi di errore e la notifica [onCompleted] dell’osservabile;
- righe 28-44: occorre ricordare che è stato richiesto di osservare il processo della riga 5 sul thread dell’interfaccia utente. Pertanto, il codice delle righe 28-44 viene eseguito nel thread dell’interfaccia utente;
- righe 29-33: si gestisce la notifica [onNext] dell’osservabile. Si riceve un tipo [UiResponse] emesso dal processo osservato. Si tratta del risultato di una delle richieste asincrone. Si aggiorna l’interfaccia grafica con questa risposta;
- righe 34-41: si elabora la notifica [onError] dell’osservabile. Si visualizza una finestra di dialogo che riporta l’errore (righe 37-38), quindi si annullano le richieste (riga 40);
- righe 42-44: si gestisce la notifica [onCompleted] dell'observable. Si aggiorna l'interfaccia grafica per indicare che il servizio richiesto è terminato. La riga 44 avrebbe potuto essere scritta anche nel modo seguente
In questo caso si è preferito utilizzare un riferimento al metodo;
- righe 45-51: alcune eccezioni non passano attraverso le righe 34-41. Ciò accade quando si inviano troppe richieste. Superato un certo limite, che dipende dall’ambiente di lavoro al momento dell’esecuzione, si genera un [StackOverflowError] che viene intercettato dalle righe 45-51;
- riga 27: l’abbonamento genera un tipo [Subscription] che viene aggiunto a un elenco di sottoscrizioni. In questo caso, l’elenco conterrà un solo elemento;
Riga 32: si aggiorna l’interfaccia grafica con il seguente metodo [updateUi]:
private void updateUi(UiResponse uiResponse) {
// tempo di risposta
uiResponse.setResponseAt();
// thread di monitoraggio
uiResponse.setObservedOn();
// numero di risposte
jLabelNbReponses.setText(String.valueOf(Integer.parseInt(jLabelNbReponses.getText()) + 1));
// tempo di esecuzione
jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
// aggiunta della stringa jSON della risposta al modello JList delle risposte
try {
model.add(0, jsonMapper.writeValueAsString(uiResponse));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
Si nota qui che alcuni componenti dell’interfaccia grafica vengono aggiornati (righe 7, 9, 12). Affinché ciò sia possibile, è necessario trovarsi obbligatoriamente nel thread dell’interfaccia utente (event loop).
Il metodo [endWaiting] è il seguente:
private void endWaiting() {
// pulsante [Générer] visibile
jButtonGenerate.setVisible(true);
// pulsante [Annuler] nascosto
jButtonCancel.setVisible(false);
// cursore di attesa nascosto
jTabbedPane1.setCursor(Cursor.getDefaultCursor());
// scheda delle risposte selezionata
jTabbedPane1.setSelectedIndex(1);
// data dell'ultimo aggiornamento
jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
}
Il metodo [doCancel] viene chiamato quando si verifica un errore nell’esecuzione delle richieste asincrone oppure quando l’utente fa clic sul pulsante [Annuler]. Il suo codice è il seguente:
// Abbonamenti agli osservabili
private List<Subscription> subscriptions = new ArrayList<Subscription>();
....
@Override
protected void doCancel() {
// fine attesa
endWaiting();
// in caso di sottoscrizioni
if (jCheckBoxRxSwing.isSelected() && subscriptions != null) {
subscriptions.forEach(Subscription::unsubscribe);
//subscriptions.forEach(s -> s.unsubscribe());
}
}
- riga 2: [subscriptions] è un elenco di un abbonamento;
- riga 11: tutti gli abbonamenti vengono annullati;
- riga 12: altra versione della riga 11. Il metodo [forEach] richiede qui un'istanza di tipo Consumer<Subscription> (cfr. paragrafo 4.4);
Torniamo al codice del metodo [doGenerateWithService]: esso può essere suddiviso in due fasi:
- fase di configurazione degli osservabili. Ciò avviene nel thread del chiamante del metodo [doGenerateWithService], ovvero il thread dell’interfaccia utente;
- l’abbonamento che determinerà l’esecuzione degli osservabili;
Se gli osservabili utilizzano come scheduler uno degli scheduler [Schedulers.computation(), Scheduler.io(), Schedulers.newThread()], verranno eseguiti al di fuori del thread dell'interfaccia utente. Questi diversi thread si contenderanno il o i core del sistema. Poiché le richieste sono operazioni lunghe (diverse centinaia di millisecondi), il metodo [doGenerateWithService] eseguito nel thread dell'interfaccia utente terminerà prima che le richieste abbiano restituito le loro risposte. Tuttavia, questo metodo era stato eseguito in seguito all’evento clic sul pulsante [Générer]. Una volta elaborato tale evento, il thread dell’interfaccia utente potrà passare all’elaborazione degli eventi successivi. Ce ne sono diversi. Infatti, il metodo [beginWaiting] ne aveva impostati diversi:
private void beginWaiting() {
// pulsanti
jButtonGenerate.setVisible(false);
jButtonCancel.setVisible(true);
// cursore di attesa
jTabbedPane1.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
jButtonCancel.setCursor(Cursor.getDefaultCursor());
// azzeramento risposte
model.clear();
// abbonamenti Rx
subscriptions.clear();
// viene visualizzata la vista delle risposte
jTabbedPane1.setSelectedIndex(1);
jLabelNbReponses.setText("0");
jLabelDuree.setText("");
// inizio esecuzione
debut = new Date().getTime();
}
Praticamente tutte le righe di questo codice hanno un effetto sull'interfaccia grafica. Questo aggiornamento non avviene immediatamente: gli eventi vengono inseriti nella coda dell'event loop. Una volta elaborato l'evento clic sul pulsante [Générer], questi eventi vengono eseguiti a loro volta e l'utente può vedere l'interfaccia grafica cambiare:
- viene visualizzata la scheda [Response] (riga 13) e le viene associato un cursore di attesa (riga 6)
- viene visualizzato il relativo pulsante [Annuler] (riga 4) e l'utente potrà cliccarci sopra;
- il campo JList delle risposte viene svuotato (riga 9);
- il JLabel del numero di risposte visualizza 0;
- il JLabel della durata di esecuzione visualizza una stringa vuota;
Per tutta la durata dell'esecuzione delle richieste, il thread di UI ha regolarmente accesso al processore. Può quindi elaborare gli eventi in attesa. Tra questi vi sono quelli impostati dal metodo [updateUi]:
private void updateUi(UiResponse uiResponse) {
// ora della risposta
uiResponse.setResponseAt();
// thread di osservazione
uiResponse.setObservedOn();
// numero di risposte
jLabelNbReponses.setText(String.valueOf(Integer.parseInt(jLabelNbReponses.getText()) + 1));
// durata dell'esecuzione
jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
// aggiunta della stringa jSON della risposta al modello JList delle risposte
try {
model.add(0, jsonMapper.writeValueAsString(uiResponse));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
Quando il thread dell’interfaccia utente è attivo:
- il valore JLabel relativo al numero di risposte viene aggiornato (riga 7);
- viene aggiornato il JLabel relativo alla durata di esecuzione (riga 9);
- il JList relativo alle risposte viene aggiornato tramite il suo modello (riga 12);
In questo modo l’utente può vedere lo stato di avanzamento dell’esecuzione delle richieste. Inoltre, può annullarle tramite il pulsante [Annuler]. È proprio questo il vantaggio di disporre di servizi asincroni a livello del livello [swing], e RxJava è una tecnologia di prima scelta per la loro implementazione.
Infine, va notato che se l’utente sceglie uno degli scheduler [Schedulers.immediate(), Schedulers.trampoline()], gli osservabili vengono eseguiti sullo stesso thread del chiamante, ovvero il thread dell’interfaccia utente. Si torna quindi a un funzionamento sincrono.
I risultati ottenuti con i diversi scheduler sono stati illustrati nei paragrafi 2.8.1, 2.8.2, 2.8.3 e 2.8.4.








