20. Programación asíncrona con RxJava
Documento de lectura: [Introduction à RxJava. Application aux environnements Swing et Android.]
En este capítulo, volvemos al capítulo 17.6, donde creamos una aplicación cliente/servidor con la siguiente arquitectura:
![]() |
Algunas acciones del usuario en la interfaz Swing de [1] desencadenan acciones que llegan hasta la base de datos de [3] a través de una red HTTP [2]. Debido a ello, la respuesta a la acción del usuario puede tardar más o menos tiempo en llegar. Sería conveniente poder incluir un indicador de espera en la interfaz de usuario con una opción para cancelar la operación iniciada si esta se alargara demasiado. En el capítulo 17.6, cada acción del usuario que requiera intercambiar información con el servidor es sincrónica. El gestor de eventos ejecutado por el código no finaliza hasta que se recibe la respuesta. Durante todo ese tiempo, la interfaz gráfica queda bloqueada: no responde a nuevas acciones del usuario. Estas acciones simplemente se ponen en cola para ser procesadas cuando el gestor de eventos que se está ejecutando en ese momento haya finalizado. Así, si se mostrara un botón de cancelación, el usuario podría hacer clic en él, pero no pasaría nada hasta que la operación en curso hubiera finalizado. El botón de cancelación no tendría entonces ninguna utilidad.
Para que al hacer clic en el botón de cancelación se produzca el efecto deseado, es necesario que la operación en curso haya finalizado. Para ello, debe iniciar la operación, que puede ser larga, de forma asíncrona:
- el gestor de eventos inicia la operación prolongada, pero no espera su resultado y devuelve el control al hilo de UI, que gestiona los eventos de la interfaz gráfica. La operación prolongada se inicia en un hilo diferente al de UI, lo que evita que este último se bloquee;
- si el usuario hace clic en el botón de cancelación antes de que finalice la operación prolongada, el hilo de UI, que está libre, puede gestionar este evento. De este modo, se puede abandonar la operación prolongada ignorando su resultado;
- si la operación prolongada no se ha cancelado, la llegada de la respuesta provocará un evento en el hilo de UI. Este, si está libre, ejecutará entonces el código asociado a dicho evento, que procesará la respuesta;
La interfaz de usuario funcionará como hasta ahora. Si los tiempos de respuesta del servidor son rápidos, el usuario no notará la diferencia. Si son perceptibles, al usuario le aparecerá un botón de cancelación y tendrá la posibilidad de interrumpir la operación en curso.
La biblioteca [Rx] permite realizar programación asíncrona. Su gran interés radica en que se ha adaptado a numerosos entornos (Java, .NET, JS, ...) y que los conocimientos adquiridos en un entorno pueden trasladarse fácilmente a otro. Nos basaremos aquí en el capítulo 2 del documento [Introduction à RxJava. Application aux environnements Swing et Android]. Se recomienda al lector que lo lea. A continuación, retomamos código procedente de los ejemplos de dicho capítulo.
Vamos a desarrollar la arquitectura de la aplicación de la siguiente manera:
![]() |
- en [1], intercalamos una capa [RxJava] entre la capa [swing] y la capa [métier]. A partir de ahora, los métodos de esta última se llamarán de forma asíncrona;
Procederemos en varias etapas:
- paso 1: la capa [metier, DAO] presenta, por el momento, una interfaz síncrona con la capa [ui]. La transformaremos en una capa asíncrona [RxJava, metier, DAO];
- paso 2: adaptaremos la aplicación de consola síncrona para que siga siendo síncrona, pero utilizando la interfaz asíncrona [RxJava, metier, DAO];
- paso 3: convertiremos la aplicación Swing síncrona en una aplicación Swing asíncrona;
20.1. paso 1
Transformamos la capa sincrónica actual [metier, DAO] en una capa asíncrona [RxJava, metier, DAO].
20.1.1. Creación
Partimos del proyecto Maven del capítulo 17.4, que abrimos con NetBeans:
![]() | ![]() |
Duplicamos este proyecto [1] (copiar/pegar) en un nuevo proyecto [elections-rxjava-metier-dao-security-webjson] [2].
20.1.2. Configuración de Maven
Modificamos el archivo [pom.xml] del nuevo proyecto para añadir la dependencia de la biblioteca [RxJava]:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.elections</groupId>
<artifactId>elections-metier-dao-security-rxjava-webjson</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>Client jUnit du serveur web / jSON</description>
<name>elections-metier-dao-security-rxjava-webjson</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- biblioteca jSON utilizada por Spring -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- componente utilizado por Spring RestTemplate -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
<scope>test</scope>
</dependency>
<!-- biblioteca de registros -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.reactivex/rxjava -->
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- líneas 65-70: hemos añadido la dependencia de la biblioteca RxJava;
20.1.3. Implementación asíncrona de la capa [métier]
![]() |
Para implementar la capa [RxJava, métier], añadimos una interfaz asíncrona [IRxElectionsMetier] [1] y su implementación [RxElectionsMetier] [2] al proyecto:
![]() |
La interfaz [IRxElectionsMetier] es la interfaz asíncrona de la capa [RxJava, métier]. Su código es el siguiente:
package elections.security.client.metier;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import rx.Observable;
public interface IRxElectionsMetier {
// autenticación
Observable<Void> authenticate(User user);
// obtener las listas de candidatos
Observable<ListeElectorale[]> getListesElectorales(User user);
// el número de escaños por cubrir
Observable<Integer> getNbSiegesAPourvoir(User user);
// el umbral electoral
Observable<Double> getSeuilElectoral(User user);
// el registro de los resultados
Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales);
// cálculo de escaños
Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales);
}
La interfaz [IRxElectionsMetier] recoge los métodos de la interfaz [IElectionsMetier], pero mientras que un método M de la interfaz [IElectionsMetier] devolvía un resultado de tipo T, el método M de la interfaz [IRxElectionsMetier] devuelve un resultado de tipo Observable<T>. El tipo [Observable] lo proporciona la biblioteca RxJava. Un tipo Observable<T> proporciona el método [subscribe], que obtendrá el tipo T de forma asíncrona. A este método se le asocian tres eventos:
- onSuccess(T result), que avisa de que hay un resultado de tipo T disponible. La operación asíncrona puede proporcionar varios resultados;
- onError(Throwable th), que avisa de que la operación asíncrona ha detectado un error;
- onCompleted(), que notifica que la operación asíncrona ha finalizado;
Mientras no se llame al método [Observable.subscribe], la operación asíncrona vinculada al observable no se inicia. El código que invoca un método M de la interfaz [IRxElectionsMetier] no obtiene el resultado T esperado, sino un tipo Observable<T> que le permitirá posteriormente obtener el resultado T al invocar el método [Observable.subscribe].
La implementación [RxElectionsMetier] de la interfaz [IRxElectionsMetier] es la siguiente:
package elections.security.client.metier;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.Observable;
@Component
public class RxElectionsMetier implements IRxElectionsMetier {
@Autowired
private IElectionsMetier metier;
@Override
public Observable<Void> authenticate(User user) {
...
}
@Override
public Observable<ListeElectorale[]> getListesElectorales(User user) {
return Observable.create(subscriber -> {
try {
// llamada al método síncrono y posterior respuesta al suscriptor
subscriber.onNext(metier.getListesElectorales(user));
// se notifica el fin de la observación
subscriber.onCompleted();
} catch (Exception e) {
// se reenvía la excepción
subscriber.onError(e);
}
});
}
@Override
public Observable<Integer> getNbSiegesAPourvoir(User user) {
...
}
@Override
public Observable<Double> getSeuilElectoral(User user) {
...
}
@Override
public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
...
}
@Override
public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
...
}
}
- líneas 12-13: inyección Spring de la capa de negocio síncrona;
- líneas 20-34: vamos a comentar el método [getListesElectorales], que en lugar de devolver un tipo [ListeElectorale[]] devuelve un tipo [Observable<ListeElectorale[]>];
- líneas 22-32: el método estático [Observable.create] permite crear un Observable a partir de un tipo [Subscriber]. El tipo [Subscriber] representa un suscriptor de los flujos de resultados generados por el proceso observado (el Observable). Proporciona tres métodos:
- [Subscriber.onNext] (línea 25) para recibir un resultado del proceso observado;
- [Subscriber.onError] (línea 30) para recibir una excepción del proceso observado. Tras una excepción, el tipo [Observable] deja de emitir resultados;
- [Subscriber.onCompleted] (línea 27) para recibir la señal de fin de emisión del proceso observado. En este caso, el proceso observado solo emite un elemento. Cabe señalar que esta señal no se emite si se produce una excepción. Este es el comportamiento por defecto de los Observables: la emisión de una excepción también indica el fin de las emisiones. Los suscriptores lo saben;
- líneas 22-34: el método [Observable.create] admite como parámetro un tipo [Observable.OnSubscribe]. Este tipo es una interfaz funcional. Este concepto se introdujo con Java 8 y hace referencia a una interfaz que tiene un único método. En este caso, el único método de la interfaz [Observable.OnSubscribe] es el siguiente:
Para implementar una interfaz funcional con un único método m(param1, param2, ..., paramn), se puede utilizar la siguiente sintaxis simplificada:
Esto es lo que se hace en las líneas 22-34:
- [subscriber] es el parámetro del método [Observable.OnSubscribe.call];
- líneas 23-32: el código que se quiere asignar al método [call];
- línea 25: se solicitan las listas electorales de forma sincrónica a la capa [métier] inyectada en la línea 13. Por lo tanto, habrá que esperar a recibir el resultado. Cuando se reciba, se pasa al método [onNext] del suscriptor;
- línea 28: en caso de error, la excepción se pasa al método [onError] del suscriptor;
- línea 31: solo se espera un resultado. Cuando se haya obtenido (las listas electorales o una excepción), se indica al suscriptor que el proceso observado ha terminado de emitir resultados;
Hay que tener muy presente que el método [RxElectionsMetier] devuelve un tipo Observable<ListeElectorale[]> y no el tipo ListeElectorale[] en sí mismo. El código que realiza la llamada deberá invocar el método Observable<ListeElectorale[]>.subscribe para que se ejecute el código de las líneas 23-33 y se devuelvan las listas electorales mediante la línea 25.
El código de los demás métodos es similar:
package elections.security.client.metier;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.Observable;
@Component
public class RxElectionsMetier implements IRxElectionsMetier {
@Autowired
private IElectionsMetier metier;
@Override
public Observable<Void> authenticate(User user) {
return Observable.create(subscriber -> {
try {
// Llamada al método síncrono
metier.authenticate(user);
// se notifica el fin del observable
subscriber.onCompleted();
} catch (Exception e) {
// se reenvía la excepción
subscriber.onError(e);
}
});
}
@Override
public Observable<ListeElectorale[]> getListesElectorales(User user) {
return Observable.create(subscriber -> {
try {
// se llama al método síncrono y, a continuación, se responde al suscriptor
subscriber.onNext(metier.getListesElectorales(user));
// se notifica el fin del observable
subscriber.onCompleted();
} catch (Exception e) {
// se reenvía la excepción
subscriber.onError(e);
}
});
}
@Override
public Observable<Integer> getNbSiegesAPourvoir(User user) {
return Observable.create(subscriber -> {
try {
// se llama al método síncrono y, a continuación, se responde al suscriptor
subscriber.onNext(metier.getNbSiegesAPourvoir(user));
// se notifica el fin del observable
subscriber.onCompleted();
} catch (Exception e) {
// se reenvía la excepción
subscriber.onError(e);
}
});
}
@Override
public Observable<Double> getSeuilElectoral(User user) {
return Observable.create(subscriber -> {
try {
// se llama al método síncrono y, a continuación, se responde al suscriptor
subscriber.onNext(metier.getSeuilElectoral(user));
// se notifica el fin del observable
subscriber.onCompleted();
} catch (Exception e) {
// se reenvía la excepción
subscriber.onError(e);
}
});
}
@Override
public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
return Observable.create(subscriber -> {
try {
// llamada al método síncrono
metier.recordResultats(user, listesElectorales);
// se notifica el fin del observable
subscriber.onCompleted();
} catch (Exception e) {
// se reenvía la excepción
subscriber.onError(e);
}
});
}
@Override
public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
return Observable.create(subscriber -> {
try {
// se llama al método síncrono y, a continuación, se responde al suscriptor
subscriber.onNext(metier.calculerSieges(user, listesElectorales));
// se notifica el fin del observable
subscriber.onCompleted();
} catch (Exception e) {
// se reenvía la excepción
subscriber.onError(e);
}
});
}
}
- líneas 20 y 81: el método [onNext] del suscriptor no se invoca porque este no espera resultados;
20.1.4. Las pruebas JUnit de la capa [métier]
![]() |
20.1.4.1. Test01
Retomamos la prueba unitaria [Test01] analizada en el apartado 17.4.4. Se diseñó para realizar llamadas síncronas a la interfaz [IElectionsMetier]. Lo modificamos para que realice llamadas sincrónicas a la nueva interfaz [IRxElectionsMetier]. De hecho, es posible realizar llamadas sincrónicas a una interfaz asíncrona RxJava. El código queda así:
package elections.security.client.metier.junit;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import elections.security.client.config.MetierConfig;
import elections.security.client.entities.ElectionsException;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import elections.security.client.metier.IRxElectionsMetier;
import rx.observables.BlockingObservable;
@SpringApplicationConfiguration(classes = MetierConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {
// capa [electionsMetier]
@Autowired
private IRxElectionsMetier electionsMetier;
// mapeador jSON
private final ObjectMapper mapper = new ObjectMapper();
// usuarios
static private User admin;
static private User user;
static private User unknown;
@BeforeClass
public static void initTest() {
admin = new User("admin", "admin");
user = new User("user", "user");
unknown = new User("x", "y");
}
@Test()
public void checkUserUser() {
ElectionsException se = null;
try {
BlockingObservable.from(electionsMetier.authenticate(user)).firstOrDefault(null);
} catch (ElectionsException e) {
se = e;
}
Assert.assertNotNull(se);
Assert.assertEquals("403 Forbidden", se.getErreurs().get(0));
}
@Test()
public void checkUserUnknown() {
ElectionsException se = null;
try {
BlockingObservable.from(electionsMetier.authenticate(unknown)).firstOrDefault(null);
} catch (ElectionsException e) {
se = e;
}
Assert.assertNotNull(se);
Assert.assertEquals("401 Unauthorized", se.getErreurs().get(0));
}
@Test()
public void checkUserAdmin() {
ElectionsException se = null;
try {
BlockingObservable.from(electionsMetier.authenticate(admin)).firstOrDefault(null);
} catch (ElectionsException e) {
se = e;
}
Assert.assertNull(se);
}
/**
* vérification 1 : méthode de calcul des sièges on fixe en dur les listes
*/
@Test
public void calculSieges1() {
// se crea la tabla de las 7 listas de candidatos
ListeElectorale[] listes = new ListeElectorale[7];
listes[0] = new ListeElectorale("A", 32000, 0, false);
listes[1] = new ListeElectorale("B", 25000, 0, false);
listes[2] = new ListeElectorale("C", 16000, 0, false);
listes[3] = new ListeElectorale("D", 12000, 0, false);
listes[4] = new ListeElectorale("E", 8000, 0, false);
listes[5] = new ListeElectorale("F", 4500, 0, false);
listes[6] = new ListeElectorale("G", 2500, 0, false);
// se calculan los escaños de cada una de las listas
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// se comprueban los resultados
Assert.assertEquals(2, listes[0].getSieges());
Assert.assertFalse(listes[0].isElimine());
Assert.assertEquals(2, listes[1].getSieges());
Assert.assertFalse(listes[1].isElimine());
Assert.assertEquals(1, listes[2].getSieges());
Assert.assertFalse(listes[2].isElimine());
Assert.assertEquals(1, listes[3].getSieges());
Assert.assertFalse(listes[3].isElimine());
Assert.assertEquals(0, listes[4].getSieges());
Assert.assertFalse(listes[4].isElimine());
Assert.assertEquals(0, listes[5].getSieges());
Assert.assertTrue(listes[5].isElimine());
Assert.assertEquals(0, listes[6].getSieges());
Assert.assertTrue(listes[6].isElimine());
}
/**
* vérification 2 : méthode de calcul des sièges on demande les listes à la couche [metier] puis on fixe en dur les
* voix
*/
@Test
public void calculSieges2() {
// se crea la tabla de las 7 listas de candidatos
ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// se fijan los votos
listes[0].setVoix(32000);
listes[1].setVoix(25000);
listes[2].setVoix(16000);
listes[3].setVoix(12000);
listes[4].setVoix(8000);
listes[5].setVoix(4500);
listes[6].setVoix(2500);
// se calculan los escaños obtenidos por cada una de las listas
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// se comprueban los resultados
Assert.assertEquals(2, listes[0].getSieges());
Assert.assertFalse(listes[0].isElimine());
Assert.assertEquals(2, listes[1].getSieges());
Assert.assertFalse(listes[1].isElimine());
Assert.assertEquals(1, listes[2].getSieges());
Assert.assertFalse(listes[2].isElimine());
Assert.assertEquals(1, listes[3].getSieges());
Assert.assertFalse(listes[3].isElimine());
Assert.assertEquals(0, listes[4].getSieges());
Assert.assertFalse(listes[4].isElimine());
Assert.assertEquals(0, listes[5].getSieges());
Assert.assertTrue(listes[5].isElimine());
Assert.assertEquals(0, listes[6].getSieges());
Assert.assertTrue(listes[6].isElimine());
}
/**
* vérification 3 méthode de calcul des sièges on provoque une exception
*/
@Test(expected = ElectionsException.class)
public void calculSieges3() {
// Se crea una tabla con 24 listas candidatas, cada una con 1 voto
ListeElectorale[] listes = new ListeElectorale[25];
// las 25 listas tendrán el mismo número de votos (4 %)
for (int i = 0; i < listes.length; i++) {
listes[i] = new ListeElectorale("Liste" + (i + 1), 1, 0, false);
}
// cálculo de escaños: normalmente debería salir un ElectionsException
// con un umbral electoral del 5 %
BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
}
/**
* enregistrement des résultats de l'élection
*
* @throws JsonProcessingException
*/
@Test
public void ecritureResultatsElections() throws JsonProcessingException {
// se crea la tabla de las 7 listas candidatas
ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// se fijan los votos de forma definitiva
listes[0].setVoix(32000);
listes[1].setVoix(25000);
listes[2].setVoix(16000);
listes[3].setVoix(12000);
listes[4].setVoix(8000);
listes[5].setVoix(4500);
listes[6].setVoix(2500);
// se calculan los escaños obtenidos por cada una de las listas
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// se muestran los resultados
for (int i = 0; i < listes.length; i++) {
System.out.println(mapper.writeValueAsString(listes[i]));
}
// se guardan los resultados en la base de datos
BlockingObservable.from(electionsMetier.recordResultats(admin, listes)).firstOrDefault(null);
// se comprueban los resultados
listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// se muestran los resultados
for (int i = 0; i < listes.length; i++) {
System.out.println(mapper.writeValueAsString(listes[i]));
}
Assert.assertEquals(2, listes[0].getSieges());
Assert.assertFalse(listes[0].isElimine());
Assert.assertEquals(2, listes[1].getSieges());
Assert.assertFalse(listes[1].isElimine());
Assert.assertEquals(1, listes[2].getSieges());
Assert.assertFalse(listes[2].isElimine());
Assert.assertEquals(1, listes[3].getSieges());
Assert.assertFalse(listes[3].isElimine());
Assert.assertEquals(0, listes[4].getSieges());
Assert.assertFalse(listes[4].isElimine());
Assert.assertEquals(0, listes[5].getSieges());
Assert.assertTrue(listes[5].isElimine());
Assert.assertEquals(0, listes[6].getSieges());
Assert.assertTrue(listes[6].isElimine());
}
}
Analicemos los cambios:
- línea 48: el método estático [BlockingObservable.from(Observable).first]:
- se suscribe al parámetro observable de [from];
- inicia la ejecución del código asociado al observable;
- espera a recibir el primer resultado. Por lo tanto, se trata de una operación sincrónica;
Aquí utilizamos el método [firstOrDefault(null)] porque el observable [metier.authenticate] no devuelve ningún resultado cuando se ejecuta. El resultado del método [firstOrDefault(null)] será, por tanto, null, un valor que aquí no se aprovecha;
Repetimos este esquema en el resto del código cada vez que queremos recurrir a la capa [métier].
La prueba unitaria [Test01] debe superarse:
![]() |
Tarea pendiente: comprobar que la prueba [Test01] se supere.
20.1.4.2. Test02
Modificamos la prueba [Test01] para que ahora pruebe la interfaz asíncrona [IRxElectionsMetier] realizando llamadas asíncronas a sus métodos.
Analicemos una primera prueba:
// semáforo de sincronización de subprocesos
private CountDownLatch latch;
// -----------------------------------
private ElectionsException checkUserUserException;
@Test()
public void checkUserUser() throws InterruptedException {
// semáforo a 1
latch = new CountDownLatch((1));
// operación asíncrona
electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
.subscribe((result) -> {
},
(th) -> {
checkUserUserException = (ElectionsException) th;
latch.countDown();
},
() -> {
latch.countDown();
});
// espera de semáforo
latch.await();
// verificación de resultados
Assert.assertNotNull(checkUserUserException);
Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
- línea 2: un semáforo es una herramienta que se utiliza para sincronizar hilos entre sí. Los hilos son flujos de ejecución que se ejecutan en paralelo. Para ejecutar una tarea T1, el hilo [Thread1] puede necesitar que finalice una tarea T2 ejecutada por un hilo [Thread2]. Por lo tanto, espera a que el hilo [Thread2] le envíe una señal indicando que la tarea T2 ha finalizado. Existen diversas formas de gestionar esta sincronización entre dos hilos. El método utilizado aquí es el siguiente:
- línea 10: el hilo [Thread1] crea un semáforo con el valor 1;
- línea 12: el hilo [Thread1] crea e inicia un hilo [Thread2]. Esto se consigue mediante la sintaxis:
electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
El método [Observable.subscribeOn] establece el hilo en el que se ejecutará el proceso observado. El parámetro de [subscribeOn] es un grupo de subprocesos. La biblioteca RxJava proporciona varios grupos adaptados a diferentes situaciones. El grupo [Schedulers.io()] es el recomendado para operaciones de red;
- (continuación)
- líneas 12-13: la operación
electionsMetier.authenticate(user).subscribeOn(Schedulers.io()).subscribe(...)
ejecuta la operación síncrona encapsulada en el observable [authenticate(user)]. Pero, dado que esta operación síncrona se inicia en un hilo distinto al hilo [Thread1], este último no espera la respuesta del método [subscribe] y pasa a la instrucción siguiente;
- (continuación)
- línea 23: el hilo [Thread1] se detiene y espera a que el semáforo pase a 0 (por el momento está en 1);
- líneas 13-21: el método [subscribe] admite como parámetros tres funciones lambda:
- la primera, [(result)->{...}], se invoca cada vez que el observable [authenticate(user)] emite un resultado [result]. Aquí tenemos un observable [authenticate(user)] que realiza alguna acción, pero no emite ningún resultado. Por lo tanto, la lambda [(result)->{}] nunca se invocará. Por eso su código está vacío aquí: [{}];
- la segunda, [(th)->{...}], recibe como parámetro un tipo [Throwable]. Se invoca cuando la ejecución del observable encuentra una excepción. Aquí, tratamos el parámetro [Throwable th] de la siguiente manera:
- línea 16: lo almacenamos en un campo de la clase de prueba de tipo [ElectionsException], ya que el observable ejecutado solo lanza este tipo de excepción;
- línea 17: ponemos el semáforo a 0 para indicar que el hilo [Thread2] ha terminado su trabajo;
- el tercer observable, [()->{...}], se invoca cuando el observable ya no tiene elementos que emitir. Tratamos este evento de la siguiente manera:
- línea 20: ponemos el semáforo a 0 para indicar que el hilo [Thread2] ha terminado su trabajo;
Cabe señalar que la tercera función lambda no se invoca si se produce una excepción. Por eso, nos hemos visto obligados a poner el semáforo a 0 también en la línea 17;
- línea 25: cuando llegamos a esta línea, el observable ha terminado su trabajo. Entonces podemos realizar las mismas comprobaciones que en la prueba [Test01];
Analicemos otra prueba:
// -----------------------------------
private ElectionsException calculSieges1Exception;
private ListeElectorale[] listesCalculSieges1;
@Test
public void calculSieges1() throws InterruptedException {
// se crea la matriz de las 7 listas candidatas
ListeElectorale[] listes = new ListeElectorale[7];
listes[0] = new ListeElectorale("A", 32000, 0, false);
listes[1] = new ListeElectorale("B", 25000, 0, false);
listes[2] = new ListeElectorale("C", 16000, 0, false);
listes[3] = new ListeElectorale("D", 12000, 0, false);
listes[4] = new ListeElectorale("E", 8000, 0, false);
listes[5] = new ListeElectorale("F", 4500, 0, false);
listes[6] = new ListeElectorale("G", 2500, 0, false);
// semáforo a 1
latch = new CountDownLatch((1));
// operación asíncrona
// se calculan los escaños de cada una de las listas
electionsMetier.calculerSieges(admin, listes).subscribeOn(Schedulers.io())
.subscribe((result) -> {
listesCalculSieges1 = result;
},
(th) -> {
calculSieges1Exception = (ElectionsException) th;
latch.countDown();
},
() -> {
latch.countDown();
});
// espera de semáforo
latch.await();
// se comprueban los resultados
Assert.assertNull(calculSieges1Exception);
Assert.assertEquals(2, listesCalculSieges1[0].getSieges());
Assert.assertFalse(listesCalculSieges1[0].isElimine());
Assert.assertEquals(2, listesCalculSieges1[1].getSieges());
Assert.assertFalse(listesCalculSieges1[1].isElimine());
Assert.assertEquals(1, listesCalculSieges1[2].getSieges());
Assert.assertFalse(listesCalculSieges1[2].isElimine());
Assert.assertEquals(1, listesCalculSieges1[3].getSieges());
Assert.assertFalse(listesCalculSieges1[3].isElimine());
Assert.assertEquals(0, listesCalculSieges1[4].getSieges());
Assert.assertFalse(listesCalculSieges1[4].isElimine());
Assert.assertEquals(0, listesCalculSieges1[5].getSieges());
Assert.assertTrue(listesCalculSieges1[5].isElimine());
Assert.assertEquals(0, listesCalculSieges1[6].getSieges());
Assert.assertTrue(listesCalculSieges1[6].isElimine());
}
- líneas 20-30: ejecución asíncrona del observable [electionsMetier.calculerSieges(admin, listes)];
- líneas 21-23: la ejecución del observable devuelve un tipo [ListeElectorale[]] que se almacena en un campo de la clase de prueba, línea 3;
- líneas 34-48: estas verificaciones son las de la prueba [Test01], a las que se ha añadido la verificación de la línea 34, que garantiza que no se ha producido ninguna excepción;
La prueba completa [Test02] está disponible en el material del curso.
Tarea: ejecutar la prueba [Test02] y comprobar que se supera.
20.1.4.3. Test03
La prueba [Test03] hace lo mismo que la prueba [Test01]: comprueba la interfaz [IRxElectionsMetier] mediante llamadas sincrónicas a dicha interfaz. Es una copia de la prueba [Test02] con dos diferencias:
- los observables ya no se ejecutan en un hilo distinto al que ejecuta las pruebas. Cuando el hilo [Thread1] ejecuta el método [subscribe] de un observable, este inicia una operación HTTP hacia el servidor, también en el hilo [Thread1]. Todo el método [subscribe] pasa entonces a ser síncrono;
- dado que solo queda un hilo, la sincronización de hilos deja de ser necesaria y el semáforo desaparece;
A continuación se muestran dos ejemplos de pruebas:
// -----------------------------------
private ElectionsException checkUserUserException;
@Test()
public void checkUserUser() throws InterruptedException {
// operación síncrona
electionsMetier.authenticate(user)
.subscribe((result) -> {
},
(th) -> {
checkUserUserException = (ElectionsException) th;
},
() -> {
});
// verificación de resultados
Assert.assertNotNull(checkUserUserException);
Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
- línea 7: por defecto, el método [electionsMetier.authenticate(user).subscribe] se ejecuta en el hilo del código que lo invoca. Por lo tanto, se trata de una operación síncrona;
// -----------------------------------
private ElectionsException calculSieges1Exception;
private ListeElectorale[] listesCalculSieges1;
@Test
public void calculSieges1() throws InterruptedException {
// se crea la tabla de las 7 listas de candidatos
ListeElectorale[] listes = new ListeElectorale[7];
listes[0] = new ListeElectorale("A", 32000, 0, false);
listes[1] = new ListeElectorale("B", 25000, 0, false);
listes[2] = new ListeElectorale("C", 16000, 0, false);
listes[3] = new ListeElectorale("D", 12000, 0, false);
listes[4] = new ListeElectorale("E", 8000, 0, false);
listes[5] = new ListeElectorale("F", 4500, 0, false);
listes[6] = new ListeElectorale("G", 2500, 0, false);
// operación síncrona
// se calculan los escaños de cada una de las listas
electionsMetier.calculerSieges(admin, listes)
.subscribe((result) -> {
listesCalculSieges1 = result;
},
(th) -> {
calculSieges1Exception = (ElectionsException) th;
},
() -> {
});
// se comprueban los resultados
Assert.assertNull(calculSieges1Exception);
Assert.assertEquals(2, listesCalculSieges1[0].getSieges());
Assert.assertFalse(listesCalculSieges1[0].isElimine());
Assert.assertEquals(2, listesCalculSieges1[1].getSieges());
Assert.assertFalse(listesCalculSieges1[1].isElimine());
Assert.assertEquals(1, listesCalculSieges1[2].getSieges());
Assert.assertFalse(listesCalculSieges1[2].isElimine());
Assert.assertEquals(1, listesCalculSieges1[3].getSieges());
Assert.assertFalse(listesCalculSieges1[3].isElimine());
Assert.assertEquals(0, listesCalculSieges1[4].getSieges());
Assert.assertFalse(listesCalculSieges1[4].isElimine());
Assert.assertEquals(0, listesCalculSieges1[5].getSieges());
Assert.assertTrue(listesCalculSieges1[5].isElimine());
Assert.assertEquals(0, listesCalculSieges1[6].getSieges());
Assert.assertTrue(listesCalculSieges1[6].isElimine());
}
Tarea pendiente: pasar la prueba [Test03] y comprobar que se supera.
20.2. Paso 2
Ahora vamos a convertir la aplicación de consola síncrona del capítulo 17.5 en una aplicación que siga siendo síncrona, pero que utilice la interfaz asíncrona [RxJava, metier, DAO];
![]() |
Partimos del proyecto [elections-console-metier-dao-security-webjson] [1] del capítulo 17.5, que duplicamos en un nuevo proyecto [elections-console-rxjava- metier-dao-security-webjson] [2]:
![]() | ![]() |
- en [3-4]; en el nuevo proyecto eliminamos la dependencia de la antigua capa síncrona [métier];
![]() | ![]() |
- en [5-9], se añade una dependencia de la nueva capa asíncrona [métier];
![]() | ![]() |
- en [10-14], se cambia el nombre de la clase [ElectionsConsole] a [ElectionsConsole01];
Del mismo modo, se renombra la clase [BootElectionsConsole] a [BootElectionsConsole01]:
![]() |
El código actual de la clase [BootElectionsConsole01] es el siguiente:
package elections.security.client.boot;
import elections.security.client.console.IElectionsUI;
public class BootElectionsConsole01 extends AbstractBootElections{
public static void main(String[] arguments) {
new BootElectionsConsole01().run();
}
@Override
protected IElectionsUI getUI() {
return ctx.getBean("electionsConsole",IElectionsUI.class);
}
}
- línea 13: dado que se ha cambiado el nombre de la clase [ElectionsConsole] por [ElectionsConsole01], ahora hay que escribir:
return ctx.getBean("electionsConsole01",IElectionsUI.class);
Volvamos al código de la clase [ElectionsConsole01]:
@Component
public class ElectionsConsole01 implements IElectionsUI {
@Autowired
private IElectionsMetier electionsMetier;
@Autowired
private User admin;
@Override
public void run() {
// las listas en liza
ListeElectorale[] listes;
// introducción de datos
try (Scanner clavier = new Scanner(System.in)) {
// se solicitan las listas en liza a la capa [metier]
listes = electionsMetier.getListesElectorales(admin);
...
// se realiza el cálculo de escaños
listes=electionsMetier.calculerSieges(admin,listes);
// se registran los resultados
electionsMetier.recordResultats(admin,listes);
...
}
Si seguimos el ejemplo de la prueba [Test01] del apartado 20.1.4.1, las líneas 5, 17, 20 y 22 cambiarán de la siguiente manera:
@Component
public class ElectionsConsole01 implements IElectionsUI {
@Autowired
private IRxElectionsMetier electionsMetier;
@Autowired
private User admin;
@Override
public void run() {
// las listas en liza
ListeElectorale[] listes;
// Introducción de datos
try (Scanner clavier = new Scanner(System.in)) {
// se solicitan las listas en liza a la capa [metier]
listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
...
// se realiza el cálculo de escaños
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// se registran los resultados
BlockingObservable.from(electionsMetier.recordResultats(admin, listes));
...
}
Tarea: configura el proyecto para ejecutar la clase [BootElectionsConsole01] con los tres parámetros [SS, Heures travaillées, Jours travaillés] y comprueba que la ejecución del proyecto así configurado da los resultados esperados.
Tarea: configure el proyecto para ejecutar el par [BootElectionsConsole02, ElectionsConsole02], en el que la clase [ElectionsConsole02] se habrá escrito siguiendo el modelo de la prueba [Test02] del apartado 20.1.4.2.
Tarea: configure el proyecto para ejecutar el par [BootElectionsConsole03, ElectionsConsole03], en el que la clase [ElectionsConsole03] se habrá escrito siguiendo el modelo de la prueba [Test03] del apartado 20.1.4.3.
20.3. Paso 3
Pasamos ahora a la adaptación de la aplicación Swing a un entorno asíncrono.
![]() |
Comenzamos duplicando el proyecto [elections-swing-metier-dao-security-webjson] [1] del capítulo 17.6 en un nuevo proyecto [elections-swing-rxjava-metier-dao-security-webjson] [2]:
![]() | ![]() |
- en [3, 4], eliminamos la dependencia de la capa síncrona [console];
![]() | ![]() |
- en [5-9], añadimos una dependencia de la capa de consola asíncrona;
La capa [swing] realizará llamadas asíncronas reales a la capa [métier]. Al llamar a un método de esta última, habrá dos subprocesos:
- el hilo de UI, que gestiona los eventos;
- un hilo de E/S que ejecutará la llamada HTTP al servidor;
Durante toda la duración de la llamada asíncrona, deberíamos mostrar una imagen de espera y un botón de cancelación. No lo haremos aquí, pero se te propondrá como mejora de la aplicación. Las modificaciones se realizan en las dos clases que realizan llamadas a la capa [métier]:
![]() |
20.3.1. Configuración de Maven
Aquí vamos a utilizar la biblioteca [RxSwing], que añade a la biblioteca [RxJava] funcionalidades disponibles únicamente en un entorno Swing. Para ello, modificamos el archivo [pom.xml] de la siguiente manera:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.elections</groupId>
<artifactId>elections-swing-rxjava-metier-dao-security-webjson</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>elections-swing-rxjava-metier-dao-security-webjson</name>
<description>couche swing asynchrone du client web / jSON</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- RxSwing -->
<!-- https://mvnrepository.com/artifact/io.reactivex/rxswing -->
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxswing</artifactId>
<version>0.27.0</version>
</dependency>
<!-- capas inferiores -->
<dependency>
<groupId>istia.st.elections</groupId>
<artifactId>elections-console-rxjava-metier-dao-security-webjson</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
20.3.2. La clase [ElectionsConnectForm]
En un funcionamiento asíncrono, la clase [ElectionsConnectForm] queda de la siguiente manera:
package elections.security.client.swing;
import elections.security.client.console.IElectionsUI;
import elections.security.client.entities.User;
import java.awt.Dimension;
import java.awt.Toolkit;
import javax.swing.SwingUtilities;
import elections.security.client.metier.IRxElectionsMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.schedulers.Schedulers;
import rx.schedulers.SwingScheduler;
@Component
public class ElectionsConnectForm extends AbstractElectionsConnectForm implements IElectionsUI {
private static final long serialVersionUID = 1L;
// referencia en la capa [métier] asíncrona
@Autowired
private IRxElectionsMetier metier;
// usuario conectado
private User user;
// formulario principal
@Autowired
private ElectionsMainForm electionsMainForm;
// sesión UI
@Autowired
private UiSession uiSession;
@Override
protected void doConnect() {
if (isPageValid()) {
// autenticación del usuario
metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(
// no hay respuesta
(result) -> {
},
// gestión de la excepción
(th) -> {
// se registra el error
String info = getInfoForException("Les erreurs suivantes se sont produites :", th);
// se muestra la información
jTextPaneErreurs.setText(info);
jTextPaneErreurs.setCaretPosition(0);
},
// la autenticación ha finalizado
() -> {
// se guarda el usuario en la sesión
uiSession.setUser(user);
// se oculta la pantalla de inicio de sesión
setVisible(false);
// se muestra la vista principal
electionsMainForm.run();
});
}
}
// inicializaciones
@Override
protected void init() {
...
}
@Override
public void run() {
// se muestra la interfaz gráfica
SwingUtilities.invokeLater(new Runnable() {
public void run() {
init();
setVisible(true);
}
});
}
private boolean isPageValid() {
...
}
private String getInfoForException(String message, Throwable ex) {
...
}
}
- líneas 36-63: el método [doConnect] se ejecuta cuando el usuario pulsa la opción de menú [Connexion]:
![]() |
Todo está en la línea 40:
metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(...)
- el proceso observado es [metier.authenticate(user)];
- se ejecutará en un hilo de E/S tomado del grupo [Schedulers.io()];
- se observará en el hilo de UI, el que gestiona los eventos de la interfaz Swing [observeOn(SwingScheduler.getInstance())]. Este hilo se obtiene mediante el método [SwingScheduler.getInstance()], donde [SwingScheduler] es una clase proporcionada por la biblioteca [RxSwing]. Esto es obligatorio. Al obtener el resultado de la operación asíncrona, este se suele utilizar para modificar elementos de la interfaz Swing. Sin embargo, esta solo se puede modificar en el hilo de UI; de lo contrario, se produce una excepción. Por lo tanto, las líneas 41-61 deben ejecutarse en el hilo de UI. Esto se garantiza aquí mediante el método [observeOn(SwingScheduler.getInstance())];
Comentemos el resto del código:
- líneas 42-43: estas líneas están ahí para respetar la sintaxis del método [subscribe]. Nunca se ejecutarán, ya que el proceso [metier.authenticate(user)] no devuelve ningún resultado;
- líneas 35-52: al recibir una excepción, esta se muestra;
- líneas 54-61: se ejecutan cuando el proceso [metier.authenticate(user)] señala el final de sus emisiones;
20.3.3. La clase [ElectionsMainForm]
![]() |
20.3.3.1. Inicialización de la interfaz gráfica
package elections.security.client.swing;
import elections.security.client.console.IElectionsUI;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import elections.security.client.metier.IRxElectionsMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.schedulers.Schedulers;
import rx.schedulers.SwingScheduler;
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
@Component
public class ElectionsMainForm extends AbstractElectionsMainForm implements IElectionsUI {
private static final long serialVersionUID = 1L;
// referencia a la capa asíncrona [métier]
@Autowired
private IRxElectionsMetier metier;
// Sesión UI
@Autowired
private UiSession uiSession;
// usuario conectado
private User user;
// plantillas de las listas JList
private DefaultListModel<String> modèleNomsVoix = null;
private DefaultListModel<String> modèleRésultats = null;
// las listas en competición
private ListeElectorale[] listes;
// listas introducidas por el usuario
private final List<ListeElectorale> listesSaisies = new ArrayList<>();
private ListeElectorale[] tListesSaisies;
// inicializaciones
@Override
protected void init() {
// generación de componentes por la clase padre
super.init();
// estado del formulario
Utilitaires.setEnabled(new JLabel[]{jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer}, false);
Utilitaires.setEnabled(
new JMenuItem[]{jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer}, false);
// centrar la ventana
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
Dimension frameSize = getSize();
if (frameSize.height > screenSize.height) {
frameSize.height = screenSize.height;
}
if (frameSize.width > screenSize.width) {
frameSize.width = screenSize.width;
}
setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2);
// usuario conectado
user = uiSession.getUser();
// inicializaciones locales
modèleNomsVoix = new DefaultListModel<>();
jListNomsVoix.setModel(modèleNomsVoix);
modèleRésultats = new DefaultListModel<>();
jListResultats.setModel(modèleRésultats);
// se solicitan las listas a la capa [métier]
metier.getListesElectorales(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// respuesta
listesElectorales -> {
// se almacenan las listas
listes = listesElectorales;
},
// excepción
(th) -> showException(th),
// fin observable
() -> {
// siguiente paso
doInitStep2();
});
}
...
- línea 46: el método [init] se ejecuta cuando se va a mostrar la ventana asociada. Su objetivo es inicializar los componentes [1-3] que se indican a continuación:
![]() |
- líneas 71-85: se solicitan de forma asíncrona las listas de candidatos (componente [1]);
- línea 71: el proceso observado es [metier.getListesElectorales(user)]. Se ejecuta en un hilo de E/S [subscribeOn(Schedulers.io())] y se observa en el hilo de UI [observeOn(SwingScheduler.getInstance()];
- líneas 74-77: el resultado devuelto por el proceso observado se almacena en el campo [listes] de la línea 38;
- línea 79: cualquier excepción se gestiona mediante el siguiente método:
private void showException(Throwable th) {
// se muestra la excepción
jTextPaneMessages.setText(getInfoForException("Les erreurs suivantes se sont produites : ", th));
jTextPaneMessages.setCaretPosition(0);
}
- líneas 81-84: al finalizar el proceso observado, se ejecutan las líneas 81-84. Estas líneas no se ejecutan si se ha producido una excepción. El método [doInitStep2] lleva a cabo el paso 2 de la inicialización de la siguiente manera:
private void doInitStep2() {
// se asocian los nombres de las listas al cuadro combinado jComboBoxNomsListes
for (int i = 0; i < listes.length; i++) {
jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
}
// número de plazas disponibles
metier.getNbSiegesAPourvoir(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// respuesta
nbSiegesAPourvoir -> {
// se inicializa la etiqueta vinculada a esta información
jLabelSAP.setText(jLabelSAP.getText() + nbSiegesAPourvoir);
},
// excepción
(th) -> showException(th),
// fin observable
() -> {
// siguiente paso
doInitStep3();
});
}
- líneas 3-5: se utiliza el resultado del paso anterior para rellenar la lista desplegable con los nombres de las listas de candidatos;
- líneas 7-20: se solicita el número de escaños por cubrir de forma asíncrona;
- línea 7: el proceso observado es [metier.getNbSiegesAPourvoir(user)]. Se ejecuta en un hilo de E/S [subscribeOn(Schedulers.io())] y se observa en el hilo de UI [observeOn(SwingScheduler.getInstance()];
- líneas 10-13: el resultado devuelto por el proceso se utiliza para actualizar la interfaz gráfica;
- línea 15: se muestra la posible excepción;
- líneas 17-20: al recibir la señal de fin del observable, se pasa a la etapa 3 del proceso de inicialización;
El paso 3 de la inicialización se lleva a cabo mediante el siguiente código:
private void doInitStep3() {
// umbral electoral
metier.getSeuilElectoral(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// respuesta
seuilElectoral -> {
// se inicializa la etiqueta vinculada a esta información
jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
},
// excepción
(th) -> showException(th),
// fin de la observación
() -> {
});
}
- líneas 3-4: se solicita el umbral electoral de forma asíncrona;
- línea 3: el proceso observado es [metier.getSeuilElectoral(user)]. Se ejecuta en un hilo de E/S [subscribeOn(Schedulers.io())] y se observa en el hilo de UI [observeOn(SwingScheduler.getInstance()];
- líneas 6-9: el resultado devuelto por el proceso se utiliza para actualizar la interfaz gráfica;
- línea 11: se muestra la posible excepción;
- líneas 13-14: al recibir la señal de fin del observable, no se realiza ninguna acción: el proceso de inicialización de la interfaz gráfica ha finalizado;
20.3.3.2. Cálculo de los escaños obtenidos por las diferentes listas
El método [doCalculer] tiene como función calcular el número de escaños obtenidos por las distintas listas:
@Override
protected void doCalculer() {
tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
// cálculo de escaños
String info = null;
metier.calculerSieges(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// procesamiento del resultado
result -> consumeResultSieges(result),
// tratamiento de la excepción
th -> showException(th),
// fin observable
() -> {
}
);
}
- líneas 6-15: se calculan de forma asíncrona los escaños obtenidos por las distintas listas;
- línea 6: el proceso observado es [metier.calculerSieges(user, tListesSaisies)]. Se ejecuta en un hilo de E/S [subscribeOn(Schedulers.io())] y se observa en el hilo de UI [observeOn(SwingScheduler.getInstance()];
- línea 9: el resultado devuelto por el proceso es utilizado por el método [consumeResultSieges];
- línea 11: se muestra la posible excepción;
- líneas 13-14: al recibir la señal de fin del observable, no se realiza ninguna acción;
Línea 9: el método [consumeResultSieges] procesa el resultado devuelto por el proceso observado, las listas de candidatos con sus campos [sieges, elimine] actualizados:
private void consumeResultSieges(ListeElectorale[] tListesSaisies) {
// se guarda el resultado
this.tListesSaisies = tListesSaisies;
// visualización de los resultados
modèleRésultats.clear();
for (int i = 0; i < tListesSaisies.length; i++) {
modèleRésultats.addElement(tListesSaisies[i].toString());
}
// actualización del estado del formulario
Utilitaires.setEnabled(new JLabel[]{jLabelEnregistrer}, true);
Utilitaires.setEnabled(new JLabel[]{jLabelCalculer}, false);
Utilitaires.setEnabled(new JMenuItem[]{jMenuItemEnregistrer}, true);
Utilitaires.setEnabled(new JMenuItem[]{jMenuItemCalculer}, false);
jTextPaneMessages.setText("Calcul terminé");
}
- líneas 4-14: el resultado obtenido se utiliza para actualizar la interfaz gráfica;
20.3.3.3. Registro de los resultados de la elección
El registro de los resultados de la elección se realiza mediante el siguiente método [doEnregistrer]:
@Override
protected void doEnregistrer() {
// se solicita el registro a la capa [métier]
metier.recordResultats(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// procesamiento del resultado: aquí no hay ninguno
(param) -> {
},
// tratamiento de la excepción
(th) -> showException(th),
// fin observable
() -> {
// actualización del formulario
Utilitaires.setEnabled(new JLabel[]{jLabelEnregistrer}, false);
Utilitaires.setEnabled(new JMenuItem[]{jMenuItemEnregistrer}, false);
jTextPaneMessages.setText("Enregistrement des résultats réalisé");
}
);
}
- líneas 4-17: se registran los resultados de la elección de forma asíncrona;
- línea 4: el proceso observado es [metier.recordResultats(user, tListesSaisies)]. Se ejecuta en un hilo de E/S [subscribeOn(Schedulers.io())] y se observa en el hilo de UI [observeOn(SwingScheduler.getInstance()];
- líneas 7-8: estas líneas nunca se ejecutarán, ya que el proceso observado no devuelve ningún resultado;
- línea 10: se muestra la posible excepción;
- líneas 14-16: al recibir la señal de fin del observable, se actualiza la interfaz gráfica;
Tarea pendiente: comprueba que la aplicación Swing funcione. A continuación, modifica la interfaz gráfica y el código para que, durante una operación asíncrona con el servidor web / jSON, aparezca una imagen de espera y una opción para cancelar la operación en curso.























