8. RxJava nell'ambiente Swing
8.1. Introduzione
Qui, riprenderemo l'applicazione Swing presentata nella Sezione 2.
![]() |
Per lavorare con RxJava in un ambiente Swing, useremo la libreria RxSwing, che aggiunge a RxJava classi e interfacce utili in un ambiente Swing. A tal fine, il file Gradle per l'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: la dipendenza da RxSwing;
Utilizzeremo un solo oggetto specifico di RxSwing: lo scheduler [SwingScheduler.getInstance()], che esegue/osserva gli osservabili sul thread del ciclo di eventi di Swing. Lo useremo esclusivamente per osservare gli osservabili in esecuzione su thread diversi dal ciclo di eventi. Esaminiamo l'architettura dell'applicazione di esempio:

- il livello di servizio asincrono fornisce metodi che restituiscono osservabili. Eseguiamo questi osservabili in thread diversi dal thread del ciclo di eventi. In questo modo, la GUI rimane reattiva. Può reagire agli input dell'utente. L'esempio più ovvio è consentire all'utente di fare clic su un pulsante [Annulla] per interrompere un'operazione asincrona che richiede troppo tempo. Affinché ciò funzioni, la GUI deve essere congelata;
- il livello Swing deve elaborare i risultati restituiti dalle operazioni asincrone e utilizzarli per aggiornare la GUI. Tuttavia, ciò può essere fatto solo nel thread del ciclo di eventi. Per ottenere questo risultato, tali risultati vengono osservati nello scheduler [SwingScheduler.getInstance()];
Pertanto, nel codice di gestione degli eventi della GUI, l'interazione con il livello asincrono [rxService] assume la seguente forma:
Observable obs=rxService.doSomething(...).subscribeOn(Schedulers.computation()).observeOn(SwingScheduler.getInstance()) ;
dove lo scheduler [Schedulers.computation()] può essere sostituito da un altro scheduler a seconda del caso d'uso.
Il lettore è invitato 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, segui questi passaggi:
![]() |
8.4. Il servizio sincrono

![]() |
Il livello di servizio sincrono fornisce la seguente interfaccia [IService]:
package dvp.rxswing.service;
public interface IService {
// random numbers in the [a,b] interval
// n numbers are generated with n itself a random number in the interval [minCount, maxCount]
// numbers are generated after a delay of milliseconds,
// where [delay] is itself a random number in the interval [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 {
// service waiting time
private int delay;
// random numbers
private List<Integer> aleas;
// execution thread
private String executedOn;
// manufacturers
public ServiceResponse() {
// execution thread
executedOn = Thread.currentThread().getName();
}
public ServiceResponse(int delay, List<Integer> aleas) {
// local builder
this();
// other initializations
this.delay = delay;
this.aleas = aleas;
}
// getters and setters
...
}
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) {
// random numbers in the [a,b] interval
// n numbers are generated with n itself a random number in the interval [minCount, maxCount]
// numbers are generated after a delay of milliseconds,
// where [delay] is itself a random number in the interval [minDelay, maxDelay]
// some checks
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;
}
// mistakes?
if (!messages.isEmpty()) {
throw new AleasException(String.join(" [---] ", messages), erreur);
}
// random number generator
Random random = new Random();
// waiting?
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);
}
}
// result generation
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));
}
// return result
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;
// error code
private int code;
// manufacturers
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;
}
// getters and setters
...
}
- riga 3: estende la classe [RuntimeException]. Si tratta quindi di un'eccezione non gestita;
- riga 7: aggiunge un codice di errore alla sua classe padre (0=nessun errore);
8.5. Il servizio asincrono

![]() |
Il livello di servizio asincrono fornisce la seguente interfaccia [IRxService]:
package dvp.rxswing.service;
import dvp.rxswing.ui.UiResponse;
import rx.Observable;
public interface IRxService {
// random numbers in the [a,b] interval
// n numbers are generated with n itself a random number in the interval [minCount, maxCount]
// numbers are generated after a delay of milliseconds,
// where [delay] is itself a random number in the interval [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 ora restituisce 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 {
// customer id
private int idClient;
// service response
private ServiceResponse serviceResponse;
// observation thread name
private String observedOn;
// query time
private String requestAt;
// response time
private String responseAt;
// manufacturers
public UiResponse() {
// observation thread
observedOn = Thread.currentThread().getName();
// query time
requestAt = getTimeStamp();
}
// private methods
private String getTimeStamp() {
return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
}
// getters and setters
...
}
- I numeri casuali si trovano nel campo alla riga 13;
- gli altri campi servono a specificare i thread di esecuzione e di osservazione dell'osservabile del servizio asincrono, nonché i timestamp della richiesta effettuata al servizio e della risposta ricevuta;
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 {
// synchronous service
private IService service;
// manufacturer
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) {
// we create an observable emitting the value rendered by the synchronous service
return Observable.create(subscriber -> {
try {
// synchronous call
uiResponse.setServiceResponse(service.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
// the result is passed on to the observer
subscriber.onNext(uiResponse);
} catch (Exception e) {
// we pass the error to the observer
subscriber.onError(e);
} finally {
// the observer is informed that the emissions are finished
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: costruzione dell'osservabile, il 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]. Questo oggetto è stato inizialmente passato nei parametri di chiamata del metodo (ultimo parametro, riga 17);
- riga 24: l'[UiResponse] viene inviato all'osservatore (il livello [swing]). L'oggetto [UiResponse] contiene non solo le informazioni generate dal servizio sincrono alla riga 22, ma anche altre informazioni generate dal metodo che chiama il metodo [getAleas] alla riga 17. Questo è il motivo per cui il metodo chiamante ha passato l'oggetto [UiResponse] come parametro al metodo [getAleas] (ultimo parametro, riga 17);
- riga 30: non dimentichiamo di segnalare la fine delle emissioni. Qui abbiamo un osservabile che emette un solo valore: quello restituito dal servizio sincrono;
- riga 27: notifichiamo all'osservatore eventuali errori;
8.6. L'interfaccia grafica utente

![]() |
- L'interfaccia grafica utente è stata realizzata utilizzando l'IDE [NetBeans], che dispone di un ottimo editor grafico. Questo editor ha generato il file [AbstractJFrameAleas.form], utilizzabile solo da questo IDE;
- Anche la classe [AbstractJFrameAleas] è stata generata dall'editor grafico di NetBeans. È stata poi rifattorizzata come segue: gli eventi GUI che volevamo gestire vengono elaborati nella classe [AbstractJFrameAleas] tramite metodi astratti implementati nella classe figlia [JFrameAleasEvents]. Infine,
- la classe astratta [AbstractJFrameAleas] è responsabile della creazione e della visualizzazione dell'interfaccia grafica;
- la classe figlia [JFrameAleasEvents] gestisce i suoi eventi;
I componenti dell'interfaccia grafica della scheda [Request] sono i seguenti:
![]() |
N. | tipo | nome | ruolo |
1 | JTabbedPane | jTabbedPane1 | Un contenitore a schede. Contiene due schede (JPanel): [jPanelRequest] per la richiesta e [jPanelResponse] per la risposta; |
2 | JTextField | jTextFieldNbValues | il numero di richieste da effettuare al servizio di numeri casuali. Nel caso del servizio asincrono in esecuzione sullo scheduler [Schedulers.io], queste richieste condivideranno un processore; |
3 | JTextField | jTextFieldA | punto finale a dell'intervallo [a,b] |
4 | JTextField | jTextFieldB | estremità b dell'intervallo [a,b] |
5 | JTextField | jTextFieldMinCount | minCount estremo dell'intervallo [minCount, maxCount] |
6 | JTextField | jTextFieldMaxCount | maxCount limite dell'intervallo [minCount, maxCount] |
7 | JTextField | jTextFieldMinDelay | minDelay limite dell'intervallo [minDelay, maxDelay] |
8 | JTextField | jTextFieldMaxDelay | limite maxDelay dell'intervallo [minDelay, maxDelay] |
9 | JCheckBox | jCheckBoxRxSwing | Se la casella di controllo è selezionata, le richieste vengono inviate all'interfaccia asincrona. In caso contrario, vengono inviate all'interfaccia sincrona |
10 | JComboBox | jComboBoxSchedulers | Per le richieste asincrone, queste verranno eseguite utilizzando lo scheduler selezionato qui |
11 | JButton | jButtonGenerate | avvia l'esecuzione delle richieste al servizio sincrono o asincrono |
I componenti GUI della scheda [Response] sono i seguenti:
![]() |
N. | tipo | nome | ruolo |
1 | JLabel | jLabelDuration | il tempo di esecuzione totale in millisecondi delle richieste |
2 | JLabel | jLabelNbResponses | il numero totale di risposte osservate (può differire dal numero di richieste, poiché ogni richiesta può restituire più valori da osservare) |
3 | JList | jListNumbers | visualizzazione dei valori osservati (ricevuti) |
4 | JButton | jButtonCancel | annulla le richieste attualmente in esecuzione |
8.7. Istanziamento dell'interfaccia utente grafica
![]() |
La classe [JFrameAleasEvents] gestisce gli eventi della GUI, compresi i clic sul pulsante [Generate]. Si tratta di una classe eseguibile che viene eseguita nel seguente contesto:
public class JFrameAleasEvents extends AbstractJFrameAleas {
private static final long serialVersionUID = 1L;
// synchronous generation service
private IService service;
// asynchronous generation service
private IRxService rxService;
// seizures
private int nbRequests;
private int a;
private int b;
private int minDelay;
private int maxDelay;
private int minCount;
private int maxCount;
// error messages
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 ";
// subscriptions to observables
protected List<Subscription> subscriptions = new ArrayList<Subscription>();
// start-end of execution
private long debut;
// mapper jSON
private ObjectMapper jsonMapper;
// answer model
private DefaultListModel<String> model;
// manufacturer
public JFrameAleasEvents() {
// parent
super();
// local
initJFrame();
// services
service = new Service();
rxService = new RxService(service);
// mapper jSON
jsonMapper = new ObjectMapper();
}
private void initJFrame() {
// hide error messages
jLabelCountError.setText("");
jLabelDelayError.setText("");
jLabelIntervalError.setText("");
jLabelNbValuesError.setText("");
// hide texts by default
jTextFieldA.setText("100");
jTextFieldB.setText("200");
jTextFieldMinCount.setText("5");
jTextFieldMaxCount.setText("10");
jTextFieldMinDelay.setText("100");
jTextFieldMaxDelay.setText("500");
jTextFieldNbValeurs.setText("10");
jLabelDuree.setText("");
// answer model
model = new DefaultListModel<>();
jListNumbers.setModel(model);
// number of hearts
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);
}
/* Create and display the form */
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 l'aspetto della GUI;
- 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à la GUI. A questo punto, appare esattamente come l'ha progettata lo sviluppatore. Non è ancora visibile;
- riga 38: vengono inizializzati alcuni componenti della GUI;
- riga 40: istanziazione del servizio sincrono;
- riga 41: istanziazione del servizio asincrono;
8.8. Esecuzione delle richieste sincrone
Facendo clic sul pulsante [Generate] si avvia l'esecuzione del seguente metodo [doGenerate]:
@Override
protected void doGenerate() {
// saisies valides ?
if (!isPageValid()) {
return;
}
// rx ou pas ?
if (jCheckBoxRxSwing.isSelected()) {
// requêtes asynchrones
doGenerateWithRxService();
} else {
// requêtes synchrones
doGenerateWithService();
}
}
- righe 4–6: verifichiamo che l'input dell'utente sia valido. Non commenteremo il metodo [isPageValid]. È elementare;
- riga 8: controlliamo lo stato della casella di controllo RxSwing;
- riga 13: eseguiamo le richieste in modo sincrono;
Il metodo [doGenerateWithService] è il seguente:
// synchronous generation
private void doGenerateWithService() {
// start waiting
beginWaiting();
try {
for (int i = 0; i < nbRequests; i++) {
// response preparation
UiResponse uiResponse = new UiResponse();
// customer no
uiResponse.setIdClient(i);
// synchronous call
uiResponse.setServiceResponse(service.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
// response time
uiResponse.setResponseAt();
// update the JList model with the responses received
model.add(0, jsonMapper.writeValueAsString(uiResponse));
// update number of responses
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);
}
// end waiting
endWaiting();
}
- riga 12: chiamata sincrona al servizio di generazione di numeri casuali;
- il metodo [doGenerateWithService] viene eseguito interamente all'interno del thread del ciclo di eventi Swing. Finché il metodo non è terminato, la GUI non elabora alcun nuovo evento. È bloccata. Pertanto, ad esempio, gli aggiornamenti della GUI alle righe 16 e 18 non saranno mai visibili. Saranno visibili solo con i loro valori finali, e ciò avverrà al termine dell'esecuzione di tutte le richieste;
Il metodo [beginWaiting] (riga 4) è il seguente:
private void beginWaiting() {
// buttons
jButtonGenerate.setVisible(false);
jButtonCancel.setVisible(true);
// wait slider
jTabbedPane1.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
jButtonCancel.setCursor(Cursor.getDefaultCursor());
// raz answers
model.clear();
// rx subscriptions
subscriptions.clear();
// the response view is displayed
jTabbedPane1.setSelectedIndex(1);
jLabelNbReponses.setText("0");
jLabelDuree.setText("");
// start of execution
debut = new Date().getTime();
}
- riga 3: il pulsante [Generate] viene nascosto. Questo crea un evento che può essere eseguito solo dopo che tutte le richieste sono state completate. Tuttavia, non lo vediamo mai nascosto, perché il metodo [endWaiting] alla riga 25 del metodo [doGenerateWithService] lo visualizza nuovamente;
- riga 13: selezioniamo la scheda [Response] per vedere le risposte in arrivo. Anche in questo caso, l'evento verrà eseguito solo dopo che tutte le richieste saranno terminate, a quel punto vedremo tutte le risposte in una volta sola, mentre volevamo vederle arrivare una dopo l'altra;
L'interfaccia sincrona presenta chiaramente dei limiti. Questi vengono superati dall'interfaccia asincrona.
8.9. Esecuzione di richieste asincrone
Il codice per l'esecuzione delle richieste asincrone è il seguente:
private void doGenerateWithRxService() {
// début attente
beginWaiting();
// on va obtenir les nombres aléatoires sous la forme d'un observable
Observable<UiResponse> observable = Observable.empty();
// Schéduler d'exécution des différents observables
Scheduler[] schedulers = { Schedulers.io(), Schedulers.computation(), Schedulers.newThread(),
Schedulers.trampoline(), Schedulers.immediate() };
Scheduler scheduler = schedulers[jComboBoxSchedulers.getSelectedIndex()];
// configuration des observables
for (int i = 0; i < nbRequests; i++) {
// préparation réponse
UiResponse uiResponse = new UiResponse();
uiResponse.setIdClient(i);
// l'observable est configuré pour s'exécuter sur le schéduler choisi par l'utilisateur
// puis cumul de l'observable obtenu à l'observable du tout
observable = observable.mergeWith(
rxService.getAleas(a, b, minCount, maxCount, minDelay, maxDelay, uiResponse).subscribeOn(scheduler));
}
// observateur
observable = observable.observeOn(SwingScheduler.getInstance());
// pour l'instant, on a juste fait de la configuration
// aucune requête n'a encore été faite au service synchrone de génération des nombres aléatoires
// on s'abonne à l'observable - c'est ce qui va provoquer l'appel au service synchrone de génération des nombres aléatoires
try {
// il n'y a ici qu'un abonnement - le résultat est une souscription
subscriptions.add(observable.subscribe(
// notification d'émission
uiResponse -> {
// on met à jour l'Ui avec la réponse
// ceci est possible car l'observation a lieu dans le thread de l'Ui
updateUi(uiResponse);
} ,
// notification d'erreur
th -> {
// cas d'erreur - on l'affiche
String message = getInfoForThrowable("L'erreur suivante s'est produite", th);
JOptionPane.showMessageDialog(this, message, "Informations", JOptionPane.PLAIN_MESSAGE);
// annulation requêtes
doCancel();
} ,
// notification [onCompleted]
// fin de l'attente
this::endWaiting));
} catch (Throwable th) {
// cas d'exception + générale - on l'affiche
String message = getInfoForThrowable("L'erreur suivante s'est produite", th);
JOptionPane.showMessageDialog(this, message, "Informations", JOptionPane.PLAIN_MESSAGE);
// on annule les requêtes
doCancel();
}
}
- riga 3: l'interfaccia grafica viene aggiornata per indicare che è in corso un'operazione potenzialmente di lunga durata;
- riga 5: viene creato un osservabile vuoto. Questo osservabile sarà 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 query. Recuperiamo lo scheduler di sua scelta;
- righe 11–19: ogni richiesta restituisce un osservabile i cui elementi vengono uniti (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 da un numero, qui indicato come [idClient]. Ciò è necessario perché, in un ambiente asincrono, l'ordine in cui vengono ricevute le risposte può differire dall'ordine in cui sono state inviate le richieste. [idClient] ci permette di determinare a quale richiesta appartiene la risposta;
- Righe 17–18: La richiesta asincrona viene effettuata [rxService.getRandom]. Viene eseguita sullo scheduler scelto dall'utente. Il suo risultato, di tipo Observable<UiResponse>, viene combinato con l'osservabile della riga 5. È importante notare che il metodo [rxService.getAleas] viene eseguito qui e restituisce un osservabile. Tuttavia, ciò non significa che siano stati generati numeri casuali. Infatti, un osservabile viene eseguito solo quando si è sottoscritti. Questo non è ancora il caso;
- riga 21: questa è l'istruzione chiave: specifichiamo che gli elementi emessi dall'osservabile alla riga 5 debbano essere osservati sul thread dell'interfaccia utente. Qui utilizziamo uno scheduler specifico della libreria RxSwing;
- righe 25–51: ci iscriviamo all'osservabile della riga 5. Solo ora i numeri casuali saranno richiesti al servizio sincrono che li genera. La parte fondamentale è nelle istruzioni alle righe 29–33. Il resto gestisce principalmente i casi di errore e la notifica [onCompleted] dall'osservabile;
- righe 28–44: Ricordiamo che abbiamo richiesto di osservare il processo della riga 5 sul thread dell'interfaccia utente. Pertanto, il codice nelle righe 28–44 viene eseguito sul thread dell'interfaccia utente;
- righe 29–33: Gestiamo la notifica [onNext] dell'observable. Riceviamo un tipo [UiResponse] emesso dal processo osservato. Questo è il risultato di una delle richieste asincrone. Aggiorniamo l'interfaccia utente con questa risposta;
- righe 34–41: gestiamo la notifica [onError] dell'observable. Visualizziamo una finestra di dialogo che mostra l'errore (righe 37–38) e poi annulliamo le richieste (riga 40);
- righe 42–44: gestiamo la notifica [onCompleted] dell'observable. Aggiorniamo la GUI per mostrare che il servizio richiesto è stato completato. La riga 44 avrebbe potuto essere scritta anche come segue
Qui abbiamo scelto di utilizzare un riferimento al metodo;
- righe 45–51: alcune eccezioni non passano attraverso le righe 34–41. Ciò accade quando vengono effettuate troppe richieste. Una volta superato un certo limite, che dipende dall'ambiente di runtime, si verifica un [StackOverflowError], che viene gestito dalle righe 45–51;
- Riga 27: L'abbonamento produce un tipo [Subscription] che viene aggiunto a un elenco di abbonamenti. In questo caso, l'elenco conterrà un solo elemento;
Riga 32: aggiorniamo l'interfaccia utente utilizzando il seguente metodo [updateUi]:
private void updateUi(UiResponse uiResponse) {
// response time
uiResponse.setResponseAt();
// observation thread
uiResponse.setObservedOn();
// number of responses
jLabelNbReponses.setText(String.valueOf(Integer.parseInt(jLabelNbReponses.getText()) + 1));
// running time
jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
// add the jSON response string to the JList response template
try {
model.add(0, jsonMapper.writeValueAsString(uiResponse));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
Qui vediamo che i componenti dell'interfaccia utente grafica vengono aggiornati (righe 7, 9, 12). Affinché ciò sia possibile, è necessario trovarsi nel thread dell'interfaccia utente (ciclo di eventi).
Il metodo [endWaiting] è il seguente:
private void endWaiting() {
// generate] button visible
jButtonGenerate.setVisible(true);
// hidden [Cancel] button
jButtonCancel.setVisible(false);
// hidden wait cursor
jTabbedPane1.setCursor(Cursor.getDefaultCursor());
// selected answers tab
jTabbedPane1.setSelectedIndex(1);
// duration updated one last time
jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
}
Il metodo [doCancel] viene chiamato quando si verifica un errore durante l'esecuzione di richieste asincrone o quando l'utente fa clic sul pulsante [Annulla]. Il suo codice è il seguente:
// les souscriptions aux observables
private List<Subscription> subscriptions = new ArrayList<Subscription>();
....
@Override
protected void doCancel() {
// fin attente
endWaiting();
// dans le cas de souscriptions
if (jCheckBoxRxSwing.isSelected() && subscriptions != null) {
subscriptions.forEach(Subscription::unsubscribe);
//subscriptions.forEach(s -> s.unsubscribe());
}
}
- riga 2: [subscriptions] è un elenco di abbonamenti;
- riga 11: tutte le sottoscrizioni vengono annullate;
- riga 12: un altro modo di scrivere la riga 11. Il metodo [forEach] si aspetta qui un'istanza di tipo Consumer<Subscription> (vedi sezione 4.4);
Torniamo al codice del metodo [doGenerateWithService]: può essere suddiviso in due passaggi:
- la fase di configurazione degli osservabili. Questa viene eseguita nel thread del chiamante del metodo [doGenerateWithService], ovvero il thread dell'interfaccia utente;
- l'abbonamento che attiverà l'esecuzione degli osservabili;
Se gli osservabili utilizzano uno degli scheduler [Schedulers.computation(), Scheduler.io(), Schedulers.newThread()], allora verranno eseguiti al di fuori del thread dell'interfaccia utente. Questi diversi thread entreranno in competizione per il/i core della macchina. Poiché le richieste sono operazioni di lunga durata (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 all'evento di clic sul pulsante [Generate]. Una volta elaborato questo evento, il thread dell'interfaccia utente potrà passare all'elaborazione degli eventi successivi. Ce ne sono diversi. Pertanto, il metodo [beginWaiting] ne aveva impostati diversi:
private void beginWaiting() {
// buttons
jButtonGenerate.setVisible(false);
jButtonCancel.setVisible(true);
// waiting cursor
jTabbedPane1.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
jButtonCancel.setCursor(Cursor.getDefaultCursor());
// raz answers
model.clear();
// rx subscriptions
subscriptions.clear();
// the response view is displayed
jTabbedPane1.setSelectedIndex(1);
jLabelNbReponses.setText("0");
jLabelDuree.setText("");
// start of execution
debut = new Date().getTime();
}
Praticamente ogni riga di questo codice influisce sull'interfaccia grafica. Questo aggiornamento non avviene immediatamente: gli eventi vengono inseriti nella coda del ciclo di eventi. Una volta elaborato l'evento clic sul pulsante [Generate], questi eventi vengono eseguiti in sequenza e l'utente può vedere il cambiamento dell'interfaccia grafica:
- viene visualizzata la scheda [Response] (riga 13) e ad essa viene associato un indicatore di caricamento (riga 6)
- viene visualizzato il pulsante [Cancel] (riga 4) e l'utente può cliccarci sopra;
- la JList delle risposte viene svuotata (riga 9);
- il JLabel relativo al numero di risposte visualizza 0;
- il JLabel relativo al tempo di esecuzione visualizza una stringa vuota;
Durante l'esecuzione delle query, il thread dell'interfaccia utente ha accesso regolare al processore. Può quindi elaborare gli eventi in sospeso. Tra questi vi sono quelli impostati dal metodo [updateUi]:
private void updateUi(UiResponse uiResponse) {
// response time
uiResponse.setResponseAt();
// observation thread
uiResponse.setObservedOn();
// number of responses
jLabelNbReponses.setText(String.valueOf(Integer.parseInt(jLabelNbReponses.getText()) + 1));
// running time
jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
// add the jSON response string to the JList response template
try {
model.add(0, jsonMapper.writeValueAsString(uiResponse));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
Quando il thread dell'interfaccia utente è attivo:
- il JLabel che visualizza il numero di risposte viene aggiornato (riga 7);
- il JLabel relativo al tempo di esecuzione viene aggiornato (riga 9);
- la JList delle risposte viene aggiornata tramite il suo modello (riga 12);
Ciò consente all'utente di vedere lo stato di avanzamento dell'esecuzione della query. Inoltre, è possibile annullarle tramite il pulsante [Annulla]. Questo è il vero scopo di avere servizi asincroni davanti al livello [Swing], e RxJava è la tecnologia scelta per implementarli.
Infine, si noti che se l'utente sceglie uno degli scheduler [Schedulers.immediate(), Schedulers.trampoline()], gli osservabili vengono quindi eseguiti sullo stesso thread del chiamante, ovvero il thread dell'interfaccia utente. Ciò comporta un comportamento sincrono.
I risultati ottenuti con i diversi scheduler sono stati mostrati nelle sezioni 2.8.1, 2.8.2, 2.8.3 e 2.8.4.








