Skip to content

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:

Image

  • 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:

Image

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

Image

  

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

Image

  

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

Image

  
  • 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
 ()->{endWaiting();}

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:

  1. fase di configurazione degli osservabili. Ciò avviene nel thread del chiamante del metodo [doGenerateWithService], ovvero il thread dell’interfaccia utente;
  2. 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.