Skip to content

8. RxJava en el entorno Swing

8.1. Introduction

Aquí volveremos a la aplicación Swing presentada en el párrafo 2.

  

Para trabajar con RxJava en un entorno Swing, utilizaremos la biblioteca RxSwing, que agrega a RxJava clases e interfaces útiles en un entorno Swing. Para ello, el archivo Gradle del ejemplo de Swing es el siguiente:

  

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'
}
  • línea 15: la dependencia de RxSwing;

Solo utilizaremos un único objeto específico de RxSwing: el programador [SwingScheduler.getInstance()], que se encarga de ejecutar y observar los observables en el hilo del bucle de eventos de Swing. Lo utilizaremos exclusivamente para observar observables que se ejecutan en hilos distintos al del bucle de eventos. Recordemos la arquitectura de la aplicación de ejemplo:

Image

  • la capa de servicio asíncrona cuenta con métodos que devuelven observables. Ejecutamos estos observables en hilos distintos al del bucle de eventos. De esta manera, la interfaz gráfica no se congela. Puede reaccionar a las acciones del usuario. La más evidente es permitir que el usuario haga clic en un botón [Annuler] para interrumpir una operación asíncrona que se demore demasiado. Para que pueda hacerlo, basta con que la interfaz gráfica esté congelada (frozen);
  • la capa Swing quiere aprovechar los resultados devueltos por las operaciones asincrónicas y, a partir de ellos, actualizar la interfaz gráfica. Sin embargo, esto solo puede hacerse en el hilo del bucle de eventos. Para ello, estos resultados se observan en el programador [SwingScheduler.getInstance()];

Así, en el código de manejo de eventos de la interfaz gráfica, la interacción con la capa asíncrona [rxService] se realiza de la siguiente manera:


Observable obs=rxService.doSomething(...).subscribeOn(Schedulers.computation()).observeOn(SwingScheduler.getInstance()) ;

donde el programador [Schedulers.computation()] podrá ser reemplazado por otro programador según los casos de uso.

Se invita al lector a volver a leer el párrafo 2. Ahora cuenta con los conocimientos necesarios para comprenderlo por completo.

8.2. La estructura del código

El código implementa la siguiente arquitectura:

Image

El proyecto de IntelliJ IDEA que implementa esta arquitectura es el siguiente:

  
  • el paquete [rxswing.service] implementa las capas de servicio síncronas (IService, Service) y asíncronas (IRxService, RxService);
  • el paquete [rxswing.ui] implementa la interfaz Swing;

8.3. Ejecución del proyecto

Para ejecutar el proyecto en IntelliJ IDEA, siga estos pasos:

 

8.4. El servicio síncrono

Image

  

La capa de servicio síncrono presenta la siguiente interfaz [IService]:


package dvp.rxswing.service;

public interface IService {
  // números aleatorios en el intervalo [a,b]
  // se generan n números, siendo n en sí mismo un número aleatorio en el intervalo [minCount, maxCount]
  // los números se generan tras una espera de delay milisegundos,
  // donde [delay] es a su vez un número aleatorio en el intervalo [minDelay, maxDelay]
  public ServiceResponse getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}

El tipo [ServiceResponse] de la respuesta del servicio es el siguiente:


package dvp.rxswing.service;

import java.util.List;

public class ServiceResponse {

  // tiempo de espera del servicio
  private int delay;
  // números aleatorios
  private List<Integer> aleas;
  // hilo de ejecución
  private String executedOn;

  // constructores

  public ServiceResponse() {
      // hilo de ejecución
    executedOn = Thread.currentThread().getName();
  }

  public ServiceResponse(int delay, List<Integer> aleas) {
      // constructor local
    this();
    // otras inicializaciones
    this.delay = delay;
    this.aleas = aleas;
  }

  // getter y setter
...
}

La interfaz [IService] está implementada por la siguiente clase [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) {
    // números aleatorios en el intervalo [a,b]
    // se generan n números, siendo n en sí mismo un número aleatorio en el intervalo [minCount, maxCount]
    // los números se generan tras una espera de delay milisegundos,
    // donde [delay] es a su vez un número aleatorio en el intervalo [minDelay, maxDelay]

    // ¿algunas verificaciones
    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;
    }
    // ¿errores?
    if (!messages.isEmpty()) {
      throw new AleasException(String.join(" [---] ", messages), erreur);
    }
    // generador de números aleatorios
    Random random = new Random();
    // ¿espera?
    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);
      }
    }
    // generación del resultado
    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));
    }
    // retorno del resultado
    return new ServiceResponse(delay,nombres);
  }

}

La clase de excepción [AleasException] utilizada por el servicio es la siguiente:


package dvp.rxswing.service;

public class AleasException extends RuntimeException {

    private static final long serialVersionUID = 1L;
    // código de error
  private int code;

  // constructores
  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 y setters
...
}
  • línea 3: extiende la clase [RuntimeException]. Por lo tanto, se trata de una excepción no controlada;
  • línea 7: agrega un código de error a su clase padre (0 = sin error);

8.5. El servicio asíncrono

Image

  

La capa de servicio asíncrono presenta la siguiente interfaz [IRxService]:


package dvp.rxswing.service;

import dvp.rxswing.ui.UiResponse;
import rx.Observable;

public interface IRxService {
  // números aleatorios en el intervalo [a,b]
  // Se generan n números, siendo n en sí mismo un número aleatorio en el intervalo [minCount, maxCount]
  // los números se generan tras una espera de delay milisegundos,
  // donde [delay] es a su vez un número aleatorio en el intervalo [minDelay, maxDelay]
  public Observable<UiResponse> getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, UiResponse uiResponse);
}
  • línea 11: ahora es un observable lo que devuelve el método [getAleas] del servicio;

El método [getAleas] devuelve una respuesta de tipo [UiResponse] destinada a la capa [Ui]. Este tipo es el siguiente:


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;
  // respuesta del servicio
  private ServiceResponse serviceResponse;
  // nombre del hilo de observación
  private String observedOn;
  // hora de la solicitud
  private String requestAt;
  // hora de la respuesta
  private String responseAt;

  // constructores

  public UiResponse() {
      // hilo de observación
    observedOn = Thread.currentThread().getName();
    // hora de la solicitud
    requestAt = getTimeStamp();
  }

  // métodos privados

  private String getTimeStamp() {
    return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
  }

  // getters y setters
...
}
  • los números aleatorios se encuentran en el campo de la línea 13;
  • los demás campos sirven para especificar los hilos de ejecución y observación del observable del servicio asíncrono, así como las horas de la solicitud enviada al servicio y de la respuesta obtenida;

La interfaz asíncrona está implementada por la siguiente clase [RxService]:


package dvp.rxswing.service;

import dvp.rxswing.ui.UiResponse;
import rx.Observable;

public class RxService implements IRxService {

  // servicio síncrono
  private IService service;

  // constructor
  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) {
      // se crea un observable que emite el valor devuelto por el servicio síncrono
    return Observable.create(subscriber -> {
      try {
          // llamada síncrona
        uiResponse.setServiceResponse(service.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
        // se pasa el resultado al observador
        subscriber.onNext(uiResponse);
      } catch (Exception e) {
          // se pasa el error al observador
        subscriber.onError(e);
      } finally {
          // se le notifica al observador que las emisiones han finalizado
        subscriber.onCompleted();
      }
    });
  }
}
  • líneas 12-14: la clase [RxService] del servicio asíncrono se construye a partir de una instancia de la interfaz síncrona [IService];
  • líneas 20-33: construcción del observable, resultado del método [getAleas];
  • línea 22: se invoca el método síncrono [service.getAleas]. Su resultado, de tipo [ServiceResponse], se incluye en el objeto de tipo [UiResponse] que se proporcionará a la capa [swing]. Este objeto se pasó inicialmente en los parámetros de llamada del método (último parámetro, línea 17);
  • línea 24: la respuesta [UiResponse] se envía al observador (la capa [swing]). El objeto [UiResponse] no solo contiene la información generada por el servicio síncrono de la línea 22. También contiene otra información generada por el método que llama al método [getAleas] de la línea 17. Es por esta razón que este método llamante pasó el objeto [UiResponse] como parámetro al método [getAleas] (último parámetro, línea 17);
  • línea 30: no hay que olvidar indicar el fin de las transmisiones. Aquí tenemos un observable que solo emite un valor: el devuelto por el servicio síncrono;
  • línea 27: se notifica al observador un posible error;

8.6. La interfaz gráfica

Image

  
  • La interfaz gráfica se creó con el IDE [Netbeans], que cuenta con un buen editor gráfico. Este editor generó el archivo [AbstractJFrameAleas.form], que solo puede ser utilizado por este IDE;
  • la clase [AbstractJFrameAleas] también fue generada por el editor gráfico de NetBeans. Posteriormente, se refactorizó de la siguiente manera: los eventos de la interfaz gráfica que queríamos manejar se procesan en la clase [AbstractJFrameAleas] mediante métodos abstractos implementados en la clase hija [JFrameAleasEvents]. Al final,
    • La clase abstracta [AbstractJFrameAleas] se encarga de construir y mostrar la interfaz gráfica;
    • la clase hija [JFrameAleasEvents] se encarga de gestionar los eventos de esta;

Los componentes de la interfaz gráfica de la pestaña [Request] son los siguientes:

 
n.º
tipo
nombre
función
1
JTabbedPane
jTabbedPane1
un contenedor de pestañas. Contiene dos pestañas (JPanel) [jPanelRequest] para la solicitud, [jPanelresponse] para la respuesta;
2
JTextField
jTextFieldNbValeurs
el número de solicitudes que se deben realizar al servicio de números aleatorios. En el caso del servicio asíncrono ejecutado en el programador [Schedulers.io], estas solicitudes compartirán un procesador;
3
JTextField
jTextFieldA
límite a del intervalo [a,b]
4
JTextField
jTextFieldB
terminal b del intervalo [a,b]
5
JTextField
jTextFieldMinCount
terminal minCount del intervalo [minCount, maxCount]
6
JTextField
jTextFieldMaxCount
terminal maxCount del intervalo [minCount, maxCount]
7
JTextField
jTextFieldMinDelay
terminal minDelay del intervalo [minDelay, maxDelay]
8
JTextField
jTextFieldMaxDelay
terminal maxDelay del intervalo [minDelay, maxDelay]
9
JCheckBox
jCheckBoxRxSwing
Si la casilla está marcada, las consultas se realizan a través de la interfaz asíncrona. De lo contrario, se realizan a través de la interfaz síncrona
10
JComboBox
jComboBoxSchedulers
En el caso de las solicitudes asincrónicas, estas se ejecutarán con el programador seleccionado aquí
11
JButton
jButtonGenerate
inicia la ejecución de las consultas en el servicio síncrono o asíncrono

Los componentes de la interfaz gráfica de la pestaña [Response] son los siguientes:

 
n.º
tipo
nombre
función
1
JLabel
jLabelDuree
el tiempo total de ejecución en milisegundos de las consultas
2
JLabel
jLabelNbReponses
el número total de respuestas observadas (puede ser diferente del número de consultas, ya que cada consulta puede proporcionar varios valores a observar)
3
JList
jListNumbers
Visualización de los valores observados (recibidos)
4
JButton
jButtonAnnuler
Cancela las consultas en ejecución

8.7. Instanciación de la interfaz gráfica

  

La clase [JFrameAleasEvents] gestiona los eventos de la interfaz gráfica de usuario, en particular el clic en el botón [Générer]. Es una clase ejecutable que se inicia en el siguiente contexto:


public class JFrameAleasEvents extends AbstractJFrameAleas {

    private static final long serialVersionUID = 1L;
    // servicio de generación síncrona
    private IService service;
    // servicio de generación asíncrona
    private IRxService rxService;

    // los registros
    private int nbRequests;
    private int a;
    private int b;
    private int minDelay;
    private int maxDelay;
    private int minCount;
    private int maxCount;

    // mensajes de error
    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 ";

    // suscripciones a observables
    protected List<Subscription> subscriptions = new ArrayList<Subscription>();
    // Inicio y fin de la ejecución
    private long debut;
    // mapeador jSON
    private ObjectMapper jsonMapper;
    // modelo de respuestas
    private DefaultListModel<String> model;

    // constructor
    public JFrameAleasEvents() {
        // padre
        super();
        // local
        initJFrame();
        // servicios
        service = new Service();
        rxService = new RxService(service);
        // mapeador jSON
        jsonMapper = new ObjectMapper();
    }

    private void initJFrame() {
        // se ocultan los mensajes de error
        jLabelCountError.setText("");
        jLabelDelayError.setText("");
        jLabelIntervalError.setText("");
        jLabelNbValuesError.setText("");
        // se ocultan los textos predeterminados
        jTextFieldA.setText("100");
        jTextFieldB.setText("200");
        jTextFieldMinCount.setText("5");
        jTextFieldMaxCount.setText("10");
        jTextFieldMinDelay.setText("100");
        jTextFieldMaxDelay.setText("500");
        jTextFieldNbValeurs.setText("10");
        jLabelDuree.setText("");
        // modelo de respuestas
        model = new DefaultListModel<>();
        jListNumbers.setModel(model);
        // número de núcleos
        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);
        }

        /* Crear y mostrar el formulario */
        java.awt.EventQueue.invokeLater(() -> {
            new JFrameAleasEvents().setVisible(true);
        });
    }
  • línea 1: la clase [JFrameAleasEvents] extiende la clase [AbstractJFrameAleas], la cual a su vez extiende la clase Swing [JFrame]. Por lo tanto, la clase [JFrameAleasEvents] es una ventana Swing;
  • líneas 68-75: el método [main] que se ejecutará;
  • línea 70: establece el «look and feel» de la interfaz gráfica;
  • línea 79: se invoca al constructor de la clase [JFrameAleasEvents]: la interfaz gráfica se construirá e inicializará. Una vez hecho esto, se hace visible;
  • líneas 34-44: el constructor;
  • línea 36: la llamada al constructor padre inicializará la interfaz gráfica de usuario. En ese momento, se ve tal como la diseñó el desarrollador. Aún no es visible;
  • línea 38: se inicializan algunos componentes de la interfaz gráfica;
  • línea 40: instanciación del servicio síncrono;
  • línea 41: instanciación del servicio asíncrono;

8.8. Ejecución de consultas sincrónicas

Al hacer clic en el botón [Générer], se ejecuta el siguiente método [doGenerate]:


    @Override
    protected void doGenerate() {
        // ¿Datos válidos?
        if (!isPageValid()) {
            return;
        }
        // ¿rx o no?
        if (jCheckBoxRxSwing.isSelected()) {
            // solicitudes asincrónicas
            doGenerateWithRxService();
        } else {
            // solicitudes sincrónicas
            doGenerateWithService();
        }
}
  • líneas 4-6: se verifica que las entradas del usuario sean válidas. No comentaremos el método [isPageValid]. Es básico;
  • línea 8: se comprueba el estado de la casilla de selección RxSwing;
  • línea 13: se ejecutan las consultas de manera sincrónica;

El método [doGenerateWithService] es el siguiente:


    // generación sincrónica
    private void doGenerateWithService() {
        // Inicio de espera
        beginWaiting();
        try {
            for (int i = 0; i < nbRequests; i++) {
                // preparación de la respuesta
                UiResponse uiResponse = new UiResponse();
                // n.º de cliente
                uiResponse.setIdClient(i);
                // llamada sincrónica
                uiResponse.setServiceResponse(service.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
                // hora de respuesta
                uiResponse.setResponseAt();
                // actualización del modelo de JList con las respuestas recibidas
                model.add(0, jsonMapper.writeValueAsString(uiResponse));
                // actualización del número de respuestas
                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);
        }
        // fin de espera
        endWaiting();
}
  • línea 12: llamada sincrónica al servicio de generación de números aleatorios;
  • la ejecución del método [doGenerateWithService] se realiza íntegramente en el hilo del bucle de eventos de Swing. Mientras el método no haya finalizado, la interfaz gráfica no procesa ningún evento nuevo. Queda congelada (frozen). Así, por ejemplo, las actualizaciones de la interfaz gráfica de las líneas 16 y 18 nunca se verán. Solo serán visibles con sus valores finales, y esto ocurrirá al finalizar la ejecución de todas las consultas;

El método [beginWaiting] (línea 4) es el siguiente:


    private void beginWaiting() {
        // botones
        jButtonGenerate.setVisible(false);
        jButtonCancel.setVisible(true);
        // cursor de espera
        jTabbedPane1.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        jButtonCancel.setCursor(Cursor.getDefaultCursor());
        // borrar respuestas
        model.clear();
        // suscripciones Rx
        subscriptions.clear();
        // se muestra la vista de respuestas
        jTabbedPane1.setSelectedIndex(1);
        jLabelNbReponses.setText("0");
        jLabelDuree.setText("");
        // inicio de ejecución
        debut = new Date().getTime();
}
  • línea 3: el botón [Générer] está oculto. Esto genera un evento que tampoco podrá ejecutarse hasta que finalice la ejecución de todas las consultas. Por lo tanto, nunca se ve oculto porque el método [endWaiting] de la línea 25 del método [doGenerateWithService] lo vuelve a mostrar;
  • línea 13: se selecciona la pestaña [Response] para ver cómo llegan las respuestas. Una vez más, este evento solo se ejecutará al finalizar la ejecución de todas las consultas, momento en el que se verán todas las respuestas juntas, cuando lo que se deseaba era verlas llegar una tras otra;

La interfaz síncrona presenta claras deficiencias. Estas se superan gracias a la interfaz asíncrona.

8.9. Ejecución de consultas asincrónicas

El código para ejecutar las consultas asincrónicas es el siguiente:


private void doGenerateWithRxService() {
        // inicio de la espera
        beginWaiting();
        // vamos a obtener los números aleatorios en forma de un observable
        Observable<UiResponse> observable = Observable.empty();
        // Programador de ejecución de los distintos observables
        Scheduler[] schedulers = { Schedulers.io(), Schedulers.computation(), Schedulers.newThread(),
                Schedulers.trampoline(), Schedulers.immediate() };
        Scheduler scheduler = schedulers[jComboBoxSchedulers.getSelectedIndex()];
        // Configuración de los observables
        for (int i = 0; i < nbRequests; i++) {
            // Preparación de la respuesta
            UiResponse uiResponse = new UiResponse();
            uiResponse.setIdClient(i);
            // El observable está configurado para ejecutarse en el programador elegido por el usuario
            // luego, se suma el observable obtenido al observable general
            observable = observable.mergeWith(
                    rxService.getAleas(a, b, minCount, maxCount, minDelay, maxDelay, uiResponse).subscribeOn(scheduler));
        }
        // observador
        observable = observable.observeOn(SwingScheduler.getInstance());
        // Por el momento, solo se ha realizado la configuración
        // aún no se ha realizado ninguna solicitud al servicio síncrono de generación de números aleatorios
        // nos suscribimos al observable; esto es lo que provocará la llamada al servicio síncrono de generación de números aleatorios
        try {
            // aquí solo hay una suscripción; el resultado es una suscripción
            subscriptions.add(observable.subscribe(
                    // notificación de emisión
                    uiResponse -> {
                        // se actualiza la interfaz de usuario con la respuesta
                        // Esto es posible porque la observación se realiza en el hilo de la interfaz de usuario
                        updateUi(uiResponse);
                    } ,
                    // notificación de error
                    th -> {
                        // caso de error: se muestra
                        String message = getInfoForThrowable("L'erreur suivante s'est produite", th);
                        JOptionPane.showMessageDialog(this, message, "Informations", JOptionPane.PLAIN_MESSAGE);
                        // cancelación de solicitudes
                        doCancel();
                    } ,
                    // notificación [onCompleted]
                    // fin de la espera
                    this::endWaiting));
        } catch (Throwable th) {
            // caso de excepción + general: se muestra
            String message = getInfoForThrowable("L'erreur suivante s'est produite", th);
            JOptionPane.showMessageDialog(this, message, "Informations", JOptionPane.PLAIN_MESSAGE);
            // se cancelan las solicitudes
            doCancel();
        }
    }
  • línea 3: se modifica la interfaz gráfica para indicar que se está realizando una operación que podría tardar mucho tiempo;
  • línea 5: se crea un observable vacío. Este observable será el que observe la capa [swing];
  • línea 7: la tabla de programadores posibles;
  • línea 9: le hemos dado al usuario la posibilidad de elegir el programador en el que se ejecutarán las consultas. Recuperamos el programador que haya elegido;
  • líneas 11-19: cada una de las consultas devuelve un observable cuyos elementos se acumulan (mergeWith) (línea 17) en el observable de la línea 5;
  • líneas 13-14: se crea el objeto [UiResponse]. Recordemos que este objeto es tanto el parámetro de entrada del método [RxService.getAleas] como su resultado (líneas 17-18);
  • línea 14: cada solicitud se identifica con su número, denominado aquí [idClient]. Esto es necesario porque, en un entorno asíncrono, el orden de recepción de las respuestas puede diferir del orden de envío de las solicitudes. [idClient] permite saber a qué solicitud pertenece la respuesta;
  • líneas 17-18: se realiza la solicitud asíncrona [rxService.getAleas]. Se ejecuta en el programador elegido por el usuario. Su resultado, de tipo Observable<UiResponse>, se acumula con el observable de la línea 5. Hay que tener muy en cuenta que el método [rxService.getAleas] se ejecuta aquí y devuelve un observable. Sin embargo, esto no significa que se hayan obtenido números aleatorios. De hecho, un observable solo se ejecuta cuando uno se suscribe a él. Este aún no es el caso;
  • línea 21: esta es la instrucción importante: se solicita que la observación de los elementos emitidos por el observable de la línea 5 se realice en el hilo de la interfaz de usuario. Aquí se utiliza un programador propio de la biblioteca RxSwing;
  • líneas 25-51: nos suscribimos al observable de la línea 5. Solo ahora se solicitarán los números aleatorios al servicio síncrono de generación de dichos números. Lo esencial se encuentra en las instrucciones de las líneas 29-33. El resto se dedica principalmente a manejar los casos de error y la notificación [onCompleted] del observable;
  • líneas 28-44: hay que recordar que se solicitó observar el proceso de la línea 5 en el hilo de la interfaz de usuario. Por lo tanto, el código de las líneas 28-44 se ejecuta en el hilo de la interfaz de usuario;
  • líneas 29-33: se procesa la notificación [onNext] del observable. Se recibe un tipo [UiResponse] emitido por el proceso observado. Este es el resultado de una de las solicitudes asincrónicas. Se actualiza la interfaz gráfica con esta respuesta;
  • líneas 34-41: se procesa la notificación [onError] del observable. Se muestra un cuadro de diálogo con el error (líneas 37-38) y luego se cancelan las solicitudes (línea 40);
  • líneas 42-44: se procesa la notificación [onCompleted] del observable. Se actualiza la interfaz gráfica para indicar que el servicio solicitado ha finalizado. La línea 44 también podría haberse escrito de la siguiente manera
 ()->{endWaiting();}

Aquí se prefirió utilizar una referencia de método;

  • líneas 45-51: algunas excepciones no pasan por las líneas 34-41. Este es el caso cuando se envían demasiadas solicitudes. Una vez superado cierto límite, que depende del entorno de trabajo en el momento de la ejecución, se genera un [StackOverflowError] que es interceptado por las líneas 45-51;
  • línea 27: la suscripción genera un tipo [Subscription] que se agrega a una lista de suscripciones. Esta lista tendrá aquí un solo elemento;

Línea 32: se actualiza la interfaz gráfica con el siguiente método [updateUi]:


    private void updateUi(UiResponse uiResponse) {
        // tiempo de respuesta
        uiResponse.setResponseAt();
        // hilo de observación
        uiResponse.setObservedOn();
        // número de respuestas
        jLabelNbReponses.setText(String.valueOf(Integer.parseInt(jLabelNbReponses.getText()) + 1));
        // tiempo de ejecución
        jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
        // Agregación de la cadena jSON de la respuesta al modelo de respuestas JList
        try {
            model.add(0, jsonMapper.writeValueAsString(uiResponse));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
}

Aquí vemos que se actualizan componentes de la interfaz gráfica (líneas 7, 9, 12). Para que esto sea posible, es obligatorio estar en el hilo de la interfaz gráfica (bucle de eventos).

El método [endWaiting] es el siguiente:


    private void endWaiting() {
        // botón [Générer] visible
        jButtonGenerate.setVisible(true);
        // botón [Annuler] oculto
        jButtonCancel.setVisible(false);
        // cursor de espera oculto
        jTabbedPane1.setCursor(Cursor.getDefaultCursor());
        // pestaña de respuestas seleccionada
        jTabbedPane1.setSelectedIndex(1);
        // hora de la última actualización
        jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
}

El método [doCancel] se invoca cuando se produce un error en la ejecución de las consultas asincrónicas o cuando el usuario hace clic en el botón [Annuler]. Su código es el siguiente:


// suscripciones a los observables
    private List<Subscription> subscriptions = new ArrayList<Subscription>();
....

    @Override
    protected void doCancel() {
        // fin de la espera
        endWaiting();
        // en el caso de suscripciones
        if (jCheckBoxRxSwing.isSelected() && subscriptions != null) {
            subscriptions.forEach(Subscription::unsubscribe);
            //subscriptions.forEach(s -> s.unsubscribe());
        }
    }

  • línea 2: [subscriptions] es una lista de una suscripción;
  • línea 11: se cancelan todas las suscripciones;
  • línea 12: otra versión de la línea 11. El método [forEach] espera aquí una instancia de tipo Consumer<Subscription> (véase el párrafo 4.4);

Volvamos al código del método [doGenerateWithService]: se puede dividir en dos etapas:

  1. etapa de configuración de los observables. Esto se realiza en el hilo del llamante del método [doGenerateWithService], es decir, el hilo de la interfaz de usuario;
  2. la suscripción que provocará la ejecución de los observables;

Si los observables tienen como programador a uno de los programadores [Schedulers.computation(), Scheduler.io(), Schedulers.newThread()], entonces se ejecutarán fuera del hilo de la interfaz de usuario. Estos diferentes hilos competirán por el núcleo o los núcleos de la máquina. Dado que las consultas son operaciones que tardan mucho tiempo (varios cientos de milisegundos), el método [doGenerateWithService] ejecutado en el hilo de la interfaz de usuario terminará antes de que las consultas hayan devuelto sus respuestas. Sin embargo, este método se había ejecutado al hacer clic en el botón [Générer]. Una vez procesado este evento, el hilo de la interfaz de usuario podrá pasar a procesar los siguientes eventos. Hay varios. Así, el método [beginWaiting] había establecido varios:


    private void beginWaiting() {
        // botones
        jButtonGenerate.setVisible(false);
        jButtonCancel.setVisible(true);
        // cursor de espera
        jTabbedPane1.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        jButtonCancel.setCursor(Cursor.getDefaultCursor());
        // borrar respuestas
        model.clear();
        // suscripciones Rx
        subscriptions.clear();
        // se muestra la vista de respuestas
        jTabbedPane1.setSelectedIndex(1);
        jLabelNbReponses.setText("0");
        jLabelDuree.setText("");
        // inicio de ejecución
        debut = new Date().getTime();
}

Prácticamente todas las líneas de este código tienen un efecto en la interfaz gráfica. Esta actualización no ocurre de inmediato: los eventos se colocan en la fila de espera del bucle de eventos. Una vez que se procesa el evento de clic en el botón [Générer], estos eventos se ejecutan a su vez y el usuario puede ver cómo cambia la interfaz gráfica:

  • se muestra la pestaña [Response] (línea 13) y se le asigna un cursor de espera (línea 6)
  • se muestra su botón [Annuler] (línea 4) y el usuario podrá hacer clic en él;
  • el campo JList de las respuestas se vacía (línea 9);
  • el JLabel del número de respuestas muestra 0;
  • el JLabel de la duración de ejecución muestra una cadena vacía;

Durante todo el tiempo de ejecución de las consultas, el hilo del UI tiene acceso regular al procesador. De esta manera, puede procesar los eventos en espera. Entre estos, se encuentran los generados por el método [updateUi]:


    private void updateUi(UiResponse uiResponse) {
        // hora de respuesta
        uiResponse.setResponseAt();
        // hilo de observación
        uiResponse.setObservedOn();
        // número de respuestas
        jLabelNbReponses.setText(String.valueOf(Integer.parseInt(jLabelNbReponses.getText()) + 1));
        // tiempo de ejecución
        jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
        // agregación de la cadena jSON de la respuesta al modelo de JList de las respuestas
        try {
            model.add(0, jsonMapper.writeValueAsString(uiResponse));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
}

Cuando el hilo de la interfaz de usuario tiene el control:

  • se actualiza el valor de JLabel correspondiente al número de respuestas (línea 7);
  • se actualiza el JLabel de la duración de ejecución (línea 9);
  • se actualiza el JList de las respuestas a través de su modelo (línea 12);

De esta manera, el usuario puede ver el avance de la ejecución de las consultas. Además, puede cancelarlas mediante el botón [Annuler]. Ahí radica todo el interés de contar con servicios asíncronos frente a la capa [swing], y RxJava es la tecnología ideal para implementarlos.

Para concluir, cabe señalar que si el usuario elige uno de los programadores [Schedulers.immediate(), Schedulers.trampoline()], los observables se ejecutan en el mismo hilo que el que realiza la llamada, es decir, el hilo de la interfaz de usuario. De este modo, se vuelve a un funcionamiento síncrono.

Los resultados obtenidos con los distintos programadores se han mostrado en los párrafos 2.8.1, 2.8.2, 2.8.3 y 2.8.4.