Skip to content

8. RxJava en el entorno Swing

8.1. Introduction

Aquí volveremos a la aplicación Swing presentada en el apartado 2.

  

Para trabajar con RxJava en un entorno Swing, utilizaremos la biblioteca RxSwing, que añade a RxJava clases e interfaces útiles en un entorno Swing. Para ello, el archivo Gradle del ejemplo 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 propio de RxSwing: el programador [SwingScheduler.getInstance()], que ejecuta/observa los observables en el hilo del evento loop de Swing. Lo utilizaremos exclusivamente para observar observables que se ejecutan en hilos distintos al del evento loop. Recordemos la arquitectura de la aplicación de ejemplo:

Image

  • la capa de servicio asíncrono presenta métodos que devuelven observables. Ejecutamos estos observables en hilos diferentes al del evento loop. De este modo, la interfaz gráfica no se queda bloqueada. Puede reaccionar a las solicitudes 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 prolongue 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 asíncronas y, a partir de ellos, actualizar la interfaz gráfica. Sin embargo, esto solo puede hacerse en el hilo del evento loop. Para ello, estos resultados se observan en el programador [SwingScheduler.getInstance()];

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


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

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

Se invita al lector a volver a leer el apartado 2. Ahora dispone de los conocimientos necesarios para comprenderlo por completo.

8.2. La estructura del código

El código implementa la siguiente arquitectura:

Image

El proyecto 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, proceda de la siguiente manera:

 

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 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;
  }

  // getters y setters
...
}

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 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 comprobaciones
    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: añade 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 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 subprocesos de ejecución y observación del observable del servicio asíncrono, así como las horas de la solicitud realizada 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 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 va a 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. Por este motivo, este método llamante ha pasado el objeto [UiResponse] como parámetro al método [getAleas] (último parámetro, línea 17);
  • línea 30: no hay que olvidar señalar el final de las emisiones. 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 ha creado con IDE [Netbeans], que cuenta con un buen editor gráfico. Este editor ha generado el archivo [AbstractJFrameAleas.form], que solo puede ser utilizado por este IDE;
  • La clase [AbstractJFrameAleas] también se generó mediante el editor gráfico de Netbeans. Posteriormente se refactorizó de la siguiente manera: los eventos de la interfaz gráfica que se deseaba gestionar se tratan en la clase [AbstractJFrameAleas] mediante métodos abstractos implementados en la clase hija [JFrameAleasEvents]. Finalmente,
    • 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 solicitudes se realizan a la interfaz asíncrona. De lo contrario, se realizan a la interfaz síncrona
10
JComboBox
jComboBoxSchedulers
En el caso de las solicitudes asíncronas, 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 solicitudes 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]. Se trata de 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;

    // las entradas
    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("");
        // plantilla 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], que 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 va a ejecutar;
  • línea 70: establece el aspecto de la interfaz gráfica;
  • línea 79: se invoca al constructor de la clase [JFrameAleasEvents]: la interfaz gráfica se va a 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. En ese momento, es tal y como la ha diseñado 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 las 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 asíncronas
            doGenerateWithRxService();
        } else {
            // solicitudes sincrónicas
            doGenerateWithService();
        }
}
  • líneas 4-6: se comprueba 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 verificación RxSwing;
  • línea 13: se ejecutan las consultas de forma 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));
                // tiempo de respuesta
                uiResponse.setResponseAt();
                // actualización de la plantilla 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 la 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 evento loop 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 al final de 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 crea un evento que tampoco podrá ejecutarse hasta que hayan finalizado todas las consultas. Por lo tanto, nunca se ve oculto, ya que 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 llegar las respuestas. De nuevo, este evento solo se ejecutará al final de la ejecución de todas las consultas, momento en el que se verán todas las respuestas, cuando lo que se quería 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 asíncronas

El código de ejecución de las consultas asíncronas es el siguiente:


private void doGenerateWithRxService() {
        // inicio de la espera
        beginWaiting();
        // se obtendrán los números aleatorios en forma de observable
        Observable<UiResponse> observable = Observable.empty();
        // Programador de ejecución de los diferentes 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
            // y luego se suma el observable obtenido al observable global
            observable = observable.mergeWith(
                    rxService.getAleas(a, b, minCount, maxCount, minDelay, maxDelay, uiResponse).subscribeOn(scheduler));
        }
        // observador
        observable = observable.observeOn(SwingScheduler.getInstance());
        // por ahora, 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 el Ui con la respuesta
                        // esto es posible porque la observación tiene lugar en el hilo del Ui
                        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 puede tardar bastante tiempo;
  • línea 5: se crea un observable vacío. Este observable será el observado por la capa [swing];
  • línea 7: la tabla de programadores posibles;
  • línea 9: hemos dado al usuario la posibilidad de elegir el programador en el que ejecutar las consultas. Recuperamos el programador de su elección;
  • 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 construye el objeto [UiResponse]. Recordemos que este objeto es a la vez parámetro de entrada del método [RxService.getAleas] y su resultado (líneas 17-18);
  • línea 14: cada solicitud se identifica mediante su número, denominado aquí [idClient]. Esto es necesario porque, en un entorno asíncrono, el orden de recepción de las respuestas puede ser diferente del orden de emisión 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 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 Ui. 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 gestiona esencialmente los casos de error y la notificación [onCompleted] del observable;
  • líneas 28-44: hay que recordar que se ha solicitado observar el proceso de la línea 5 en el hilo de Ui. Por lo tanto, el código de las líneas 28-44 se ejecuta en el hilo de Ui;
  • líneas 29-33: se procesa la notificación [onNext] del observable. Se recibe un tipo [UiResponse] emitido por el proceso observado. Es el resultado de una de las solicitudes asíncronas. 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, a continuación, 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 ha preferido 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. Superado un 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 añade a una lista de suscripciones. Esta lista solo tendrá un elemento;

Línea 32: se actualiza la interfaz gráfica con el siguiente 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));
        // Añadir 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();
        }
}

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 Ui (evento loop).

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 asíncronas o cuando el usuario hace clic en el botón [Annuler]. Su código es el siguiente:


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

    @Override
    protected void doCancel() {
        // fin de la espera
        endWaiting();
        // en 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 escritura de la línea 11. El método [forEach] espera aquí una instancia de tipo Consumer<Subscription> (véase el apartado 4.4);

Volvamos al código del método [doGenerateWithService]: se puede descomponer 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 del Ui;
  2. la suscripción que provocará la ejecución de los observables;

Si los observables tienen como programador uno de los programadores [Schedulers.computation(), Scheduler.io(), Schedulers.newThread()], se ejecutarán fuera del hilo de Ui. Estos diferentes hilos competirán por el núcleo o los núcleos de la máquina. Dado que las consultas son operaciones largas (varios cientos de milisegundos), el método [doGenerateWithService] ejecutado en el hilo de Ui finalizará antes de que las consultas hayan devuelto sus respuestas. Ahora bien, 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 Ui podrá pasar al procesamiento de 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 se produce de forma inmediata: los eventos se colocan en la cola del evento loop. Una vez procesado 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 asocia 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 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 la ejecución muestra una cadena vacía;

Durante todo el tiempo de ejecución de las consultas, el subproceso del UI tiene acceso regular al procesador. De este modo, puede procesar los eventos en espera. Entre ellos se encuentran los establecidos 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));
        // duración de la ejecución
        jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
        // adició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 subproceso de Ui tiene la palabra:

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

De este modo, el usuario ve el progreso 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 una tecnología ideal para implementarlos.

Para terminar, cabe señalar que si el usuario elige uno de los programadores [Schedulers.immediate(), Schedulers.trampoline()], los observables se ejecutan entonces en el mismo hilo que el llamante, es decir, el hilo del Ui. Se vuelve entonces a un funcionamiento síncrono.

Los resultados obtenidos con los diferentes programadores se han mostrado en los apartados 2.8.1, 2.8.2, 2.8.3 y 2.8.4.