Skip to content

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:

Image

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

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, segui questi passaggi:

 

8.4. Il servizio sincrono

Image

  

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

Image

  

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

Image

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

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:

  1. la fase di configurazione degli osservabili. Questa viene eseguita nel thread del chiamante del metodo [doGenerateWithService], ovvero il thread dell'interfaccia utente;
  2. 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.