20. Asynchronous programming with RxJava
Document to read: [Introduction à RxJava. Application aux environnements Swing et Android.]
In this chapter, we revisit Chapter 17.6, where we built a client/server application with the following architecture:
![]() |
Certain user actions on the Swing interface in [1] trigger actions all the way down to the database in [3] via a network HTTP [2]. Because of this, the response to the user’s action may take a varying amount of time. It would be helpful to include a loading indicator on the user interface with an option to cancel the initiated operation if it takes too long. In Chapter 17.6, every user action requiring an exchange of information with the server is synchronous. The event handler executed by the code is not completed until the response is received. During this time, the graphical interface is frozen: it does not respond to new user actions. These actions are simply queued to be processed once the currently running event handler has finished. Thus, if a cancel button were displayed, the user could click it, but nothing would happen until the current operation was complete. The cancel button would then serve no purpose.
For clicking the cancel button to have an effect, the current operation must be finished. To achieve this, it must launch the potentially long-running operation asynchronously:
- The event handler initiates the long-running operation but does not wait for its result and returns control to the UI thread, which handles GUI events. The long-running operation is launched on a thread different from that of UI, which means the latter is not blocked;
- if the user clicks the cancel button before the long-running operation finishes, the idle UI thread can handle this event. The long-running operation can then be abandoned by ignoring its result;
- if the long-running operation has not been canceled, the arrival of the response will trigger an event in the UI thread. If this thread is idle, it will then execute the code associated with this event, which will process the response;
The user interface will function as before. If the server response times are fast, the user will not notice any difference. If they are noticeable, the user will see a cancel button appear and will have the option to interrupt the current operation.
The [Rx] library enables asynchronous programming. Its main advantage lies in the fact that it has been ported to numerous environments (Java, .NET, JS, ...) and that expertise gained in one environment can be easily transferred to another. We will refer here to Chapter 2 of document [Introduction à RxJava. Application aux environnements Swing et Android]. The reader is encouraged to read it. In the following, we will use code from the examples in that chapter.
We will evolve the application architecture as follows:
![]() |
- in [1], we insert a [RxJava] layer between the [swing] layer and the [métier] layer. The methods of this layer will now be called asynchronously;
We will proceed in several steps:
- Step 1: The [metier, DAO] layer currently has a synchronous interface with the [ui] layer. We will convert it into an asynchronous layer, [RxJava, metier, DAO];
- Step 2: We will port the synchronous console application to a still-synchronous application but one that uses the asynchronous interface [RxJava, metier, DAO];
- Step 3: We will port the synchronous Swing application to an asynchronous Swing application;
20.1. Step 1
We are converting the current synchronous layer [metier, DAO] into an asynchronous layer [RxJava, metier, DAO].
20.1.1. Creation
We start with the Maven project from Chapter 17.4, which we open with Netbeans:
![]() | ![]() |
We duplicate this project [1] (copy/paste) into a new project [elections-rxjava-metier-dao-security-webjson] [2].
20.1.2. Maven Configuration
We update the [pom.xml] file in the new project to add the dependency on the [RxJava] library:
<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>
<!-- jSON library used by 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>
<!-- component used by 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>
<!-- log library -->
<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>
- lines 65-70: we added the dependency on the RxJava library;
20.1.3. Asynchronous implementation of the [métier] layer
![]() |
To implement the [RxJava, métier] layer, we add an asynchronous interface [IRxElectionsMetier] [1] and its implementation [RxElectionsMetier] [2] to the project:
![]() |
The [IRxElectionsMetier] interface is the asynchronous interface for the [RxJava, métier] layer. Its code is as follows:
package elections.security.client.metier;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import rx.Observable;
public interface IRxElectionsMetier {
// authentication
Observable<Void> authenticate(User user);
// get the lists in competition
Observable<ListeElectorale[]> getListesElectorales(User user);
// the number of seats to be filled
Observable<Integer> getNbSiegesAPourvoir(User user);
// the electoral threshold
Observable<Double> getSeuilElectoral(User user);
// recording results
Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales);
// calculating seats
Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales);
}
The [IRxElectionsMetier] interface includes the methods of the [IElectionsMetier] interface, but whereas a method M of the [IElectionsMetier] interface returned a result of type T, the method M of the [IRxElectionsMetier] interface returns a result of type Observable<T>. The type [Observable] is provided by the RxJava library. An Observable<T> type provides the [subscribe] method, which retrieves the type T asynchronously. Three events are associated with this method:
- onSuccess(T result), which notifies that a result of type T is available. The asynchronous operation may return multiple results;
- onError(Throwable th), which notifies that the asynchronous operation encountered an error;
- onCompleted(), which notifies that the asynchronous operation is complete;
As long as the [Observable.subscribe] method is not called, the asynchronous operation associated with the observable is not initiated. The code that calls a method M of the [IRxElectionsMetier] interface does not obtain the expected result T, but rather an Observable<T> type that will later allow it to obtain the result T by calling the [Observable.subscribe] method.
The implementation [RxElectionsMetier] of the interface [IRxElectionsMetier] is as follows:
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 {
// call synchronous method then reply to subscriber
subscriber.onNext(metier.getListesElectorales(user));
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
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) {
...
}
}
- lines 12-13: Spring injection of the synchronous business layer;
- lines 20-34: we will comment out the method [getListesElectorales], which instead of returning a type [ListeElectorale[]] returns a type [Observable<ListeElectorale[]>];
- lines 22–32: the static method [Observable.create] allows creating an Observable from a type [Subscriber]. The type [Subscriber] represents a subscriber to the streams of results produced by the observed process (the Observable). It provides three methods:
- [Subscriber.onNext] (line 25) to receive a result from the observed process;
- [Subscriber.onError] (line 30) to receive an exception from the observed process. After an exception, the type [Observable] no longer emits results;
- [Subscriber.onCompleted] (line 27) to receive the end-of-emission signal from the observed process. Here, the observed process emits only one element. Note that this signal is not emitted if an exception occurs. This is the default behavior of Observables: the emission of an exception also signals the end of emissions. Subscribers are aware of this;
- lines 22–34: the method [Observable.create] takes a type [Observable.OnSubscribe] as a parameter. This type is a functional interface. This concept was introduced with Java 8 and refers to an interface with a single method. Here, the single method of the [Observable.OnSubscribe] interface is as follows:
To implement a single-method functional interface m(param1, param2, ..., paramn), you can use the following simplified syntax:
This is what is done in lines 22–34:
- [subscriber] is the parameter of the method [Observable.OnSubscribe.call];
- lines 23–32: the code we want to assign to the method [call];
- line 25: the voter lists are requested synchronously from the [métier] layer injected on line 13. There will therefore be a wait for the result. When it is received, it is passed to the [onNext] method of the subscriber;
- line 28: in case of an error, the exception is passed to the subscriber’s [onError] method;
- line 31: we wait for a result. Once it has been obtained (the voter rolls or an exception), the subscriber is notified that the observed process has finished producing results;
Note that the method [RxElectionsMetier] returns a type Observable<ListeElectorale[]> and not the type ListeElectorale[] itself. The calling code must call the method Observable<ListeElectorale[]>.subscribe for the code in lines 23–33 to be executed and return the electoral rolls via line 25.
The code for the other methods is 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 {
// synchronous method call
metier.authenticate(user);
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<ListeElectorale[]> getListesElectorales(User user) {
return Observable.create(subscriber -> {
try {
// call synchronous method then reply to subscriber
subscriber.onNext(metier.getListesElectorales(user));
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<Integer> getNbSiegesAPourvoir(User user) {
return Observable.create(subscriber -> {
try {
// call synchronous method then reply to subscriber
subscriber.onNext(metier.getNbSiegesAPourvoir(user));
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<Double> getSeuilElectoral(User user) {
return Observable.create(subscriber -> {
try {
// call synchronous method then reply to subscriber
subscriber.onNext(metier.getSeuilElectoral(user));
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
return Observable.create(subscriber -> {
try {
// synchronous method call
metier.recordResultats(user, listesElectorales);
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
return Observable.create(subscriber -> {
try {
// call synchronous method then reply to subscriber
subscriber.onNext(metier.calculerSieges(user, listesElectorales));
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
}
- lines 20 and 81: the subscriber's [onNext] method is not called because the subscriber does not expect any results;
20.1.4. The JUnit tests of the [métier] layer
![]() |
20.1.4.1. Test01
We return to the unit test [Test01] discussed in Section 17.4.4. It was designed to make synchronous calls to the [IElectionsMetier] interface. We modify it so that it makes synchronous calls to the new interface [IRxElectionsMetier]. It is indeed possible to make synchronous calls to an asynchronous interface RxJava. The code becomes as follows:
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 {
// layer [electionsMetier]
@Autowired
private IRxElectionsMetier electionsMetier;
// mapper jSON
private final ObjectMapper mapper = new ObjectMapper();
// users
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() {
// create the table of 7 candidate lists
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);
// the seats for each list are calculated
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// check results
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() {
// create the table of 7 candidate lists
ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// the voices are hard-fixed
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);
// the seats obtained by each list are calculated
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// check results
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() {
// we create a table of 24 candidate lists, each with 1 vote
ListeElectorale[] listes = new ListeElectorale[25];
// all 25 lists will have the same number of votes (4%)
for (int i = 0; i < listes.length; i++) {
listes[i] = new ListeElectorale("Liste" + (i + 1), 1, 0, false);
}
// calculation of seats - normally there should be a ElectionsException
// with an electoral threshold of 5%
BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
}
/**
* enregistrement des résultats de l'élection
*
* @throws JsonProcessingException
*/
@Test
public void ecritureResultatsElections() throws JsonProcessingException {
// create the table of 7 candidate lists
ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// the voices are hard-fixed
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);
// the seats obtained by each list are calculated
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// display results
for (int i = 0; i < listes.length; i++) {
System.out.println(mapper.writeValueAsString(listes[i]));
}
// results are entered into the database
BlockingObservable.from(electionsMetier.recordResultats(admin, listes)).firstOrDefault(null);
// check results
listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// display results
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());
}
}
Let's examine the changes:
- line 48: the static method [BlockingObservable.from(Observable).first]:
- subscribes to the observable parameter of [from];
- triggers the execution of the code associated with the observable;
- waits to receive the first result. This is therefore a synchronous operation;
We use the [firstOrDefault(null)] method here because the [metier.authenticate] observable does not return a result when executed. The result of the [firstOrDefault(null)] method will therefore be null, a value that is not used here;
We repeat this pattern throughout the rest of the code whenever we want to call the [métier] layer.
The unit test [Test01] must pass:
![]() |
Work to be done: verify that the [Test01] test passes.
20.1.4.2. Test02
We modify the [Test01] test so that it now tests the asynchronous interface [IRxElectionsMetier] by making asynchronous calls to its methods.
Let’s examine an initial test:
// thread synchronization semaphore
private CountDownLatch latch;
// -----------------------------------
private ElectionsException checkUserUserException;
@Test()
public void checkUserUser() throws InterruptedException {
// 1" semaphore
latch = new CountDownLatch((1));
// asynchronous operation
electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
.subscribe((result) -> {
},
(th) -> {
checkUserUserException = (ElectionsException) th;
latch.countDown();
},
() -> {
latch.countDown();
});
// waiting for semaphore
latch.await();
// checking results
Assert.assertNotNull(checkUserUserException);
Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
- Line 2: A semaphore is a tool used to synchronize threads with each other. Threads are execution flows running in parallel. To execute task T1, thread [Thread1] may need task T2, executed by thread [Thread2], to be completed. It then waits for thread [Thread2] to send it a signal indicating that task T2 is complete. There are various ways to manage this synchronization between two threads. The method used here is as follows;
- line 10: the thread [Thread1] creates a semaphore with the value 1;
- line 12: the [Thread1] thread creates and launches a [Thread2] thread. This is achieved using the syntax:
electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
The [Observable.subscribeOn] method sets the thread on which the observed process will run. The parameter of [subscribeOn] is a thread pool. The RxJava library provides several pools suited to different situations. The [Schedulers.io()] pool is the one recommended for network operations;
- (continued)
- lines 12–13: the operation
electionsMetier.authenticate(user).subscribeOn(Schedulers.io()).subscribe(...)
executes the synchronous operation encapsulated in the observable [authenticate(user)]. But because this synchronous operation is launched on a different thread than the [Thread1] thread, the latter does not wait for the response from the [subscribe] method and moves on to the next instruction;
- (continued)
- line 23: the [Thread1] thread pauses and waits for the semaphore to go to 0 (it is currently at 1);
- lines 13–21: the method [subscribe] takes three lambda functions as parameters:
- the first, [(result)->{...}], is called every time the observable [authenticate(user)] emits a result [result]. Here we have an observable [authenticate(user)] that does something but emits no result. The lambda [(result)->{}] will therefore never be called. That is why its code is empty here: [{}];
- the second one, [(th)->{...}], receives a [Throwable] type as a parameter. It is called when the observable’s execution encounters an exception. Here, we handle the [Throwable th] parameter as follows:
- line 16: we store it in a field of the test class of type [ElectionsException] because the executed observable only throws this type of exception;
- line 17: we set the semaphore to 0 to indicate that the [Thread2] thread has finished its work;
- The third [()->{...}] is called when the observable has no more items to emit. We handle this event as follows:
- line 20: we set the semaphore to 0 to indicate that the thread [Thread2] has finished its work;
Note that the third lambda is not called if an exception occurs. This is why we had to set the semaphore to 0 in line 17 as well;
- line 25: when we reach this line, the observable has finished its work. We can then perform the same checks as in the [Test01] test;
Let’s examine another test:
// -----------------------------------
private ElectionsException calculSieges1Exception;
private ListeElectorale[] listesCalculSieges1;
@Test
public void calculSieges1() throws InterruptedException {
// create the table of 7 candidate lists
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);
// 1" semaphore
latch = new CountDownLatch((1));
// asynchronous operation
// the seats for each list are calculated
electionsMetier.calculerSieges(admin, listes).subscribeOn(Schedulers.io())
.subscribe((result) -> {
listesCalculSieges1 = result;
},
(th) -> {
calculSieges1Exception = (ElectionsException) th;
latch.countDown();
},
() -> {
latch.countDown();
});
// waiting for semaphore
latch.await();
// check results
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());
}
- lines 20–30: asynchronous execution of the observable [electionsMetier.calculerSieges(admin, listes)];
- lines 21-23: the execution of the observable returns a type [ListeElectorale[]] which is stored in a field of the test class, line 3;
- lines 34–48: these checks are those from the [Test01] test, to which the check on line 34 has been added to ensure that no exception occurred;
The entire [Test02] test is available in the course materials.
Assignment: Run the [Test02] test and verify that it passes.
20.1.4.3. Test03
The [Test03] test does the same thing as the [Test01] test: it tests the [IRxElectionsMetier] interface using synchronous calls to that interface. This is a copy of the [Test02] test with two differences:
- the observables are no longer executed in a thread different from the one running the tests. When the [Thread1] thread executes an observable’s [subscribe] method, it initiates a HTTP operation to the server, also on the [Thread1] thread. The entire [subscribe] method then becomes synchronous;
- since there is now only one thread, thread synchronization becomes unnecessary and the semaphore disappears;
Here are two test examples:
// -----------------------------------
private ElectionsException checkUserUserException;
@Test()
public void checkUserUser() throws InterruptedException {
// synchronous operation
electionsMetier.authenticate(user)
.subscribe((result) -> {
},
(th) -> {
checkUserUserException = (ElectionsException) th;
},
() -> {
});
// checking results
Assert.assertNotNull(checkUserUserException);
Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
- line 7: by default, the [electionsMetier.authenticate(user).subscribe] method runs in the calling code's thread. This is therefore a synchronous operation;
// -----------------------------------
private ElectionsException calculSieges1Exception;
private ListeElectorale[] listesCalculSieges1;
@Test
public void calculSieges1() throws InterruptedException {
// create the table of 7 candidate lists
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);
// synchronous operation
// the seats for each list are calculated
electionsMetier.calculerSieges(admin, listes)
.subscribe((result) -> {
listesCalculSieges1 = result;
},
(th) -> {
calculSieges1Exception = (ElectionsException) th;
},
() -> {
});
// check results
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());
}
Task: Run the [Test03] test and verify that it passes.
20.2. Step 2
We will now port the synchronous console application from Chapter 17.5 into an application that is still synchronous but uses the asynchronous interface [RxJava, metier, DAO];
![]() |
We start with the [elections-console-metier-dao-security-webjson] [1] project from Chapter 17.5, which we duplicate into a new project [elections-console-rxjava- metier-dao-security-webjson] [2]:
![]() | ![]() |
- In [3-4], within the new project, we remove the dependency on the old synchronous layer [métier];
![]() | ![]() |
- in [5-9], we add a dependency on the new asynchronous layer [métier];
![]() | ![]() |
- In [10-14], the class [ElectionsConsole] is renamed to [ElectionsConsole01];
Similarly, rename the class [BootElectionsConsole] to [BootElectionsConsole01]:
![]() |
The current code for the [BootElectionsConsole01] class is as follows:
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);
}
}
- Line 13: Because we changed the class name from [ElectionsConsole] to [ElectionsConsole01], we must now write:
return ctx.getBean("electionsConsole01",IElectionsUI.class);
Let’s go back to the code for the [ElectionsConsole01] class:
@Component
public class ElectionsConsole01 implements IElectionsUI {
@Autowired
private IElectionsMetier electionsMetier;
@Autowired
private User admin;
@Override
public void run() {
// competing lists
ListeElectorale[] listes;
// data entry
try (Scanner clavier = new Scanner(System.in)) {
// lists in competition are requested from the [metier] layer
listes = electionsMetier.getListesElectorales(admin);
...
// we calculate the number of seats
listes=electionsMetier.calculerSieges(admin,listes);
// we record the results
electionsMetier.recordResultats(admin,listes);
...
}
If we follow the example of test [Test01] from section 20.1.4.1, lines 5, 17, 20, and 22 will change as follows:
@Component
public class ElectionsConsole01 implements IElectionsUI {
@Autowired
private IRxElectionsMetier electionsMetier;
@Autowired
private User admin;
@Override
public void run() {
// competing lists
ListeElectorale[] listes;
// data entry
try (Scanner clavier = new Scanner(System.in)) {
// lists in competition are requested from the [metier] layer
listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
...
// we calculate the number of seats
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// we record the results
BlockingObservable.from(electionsMetier.recordResultats(admin, listes));
...
}
Task: Configure the project to run the [BootElectionsConsole01] class with the three [SS, Heures travaillées, Jours travaillés] parameters and verify that running the project as configured produces the expected results.
Task: Configure the project to run the pair [BootElectionsConsole02, ElectionsConsole02], where the class [ElectionsConsole02] has been written following the model of the [Test02] test in section 20.1.4.2.
Task: Configure the project to run the pair [BootElectionsConsole03, ElectionsConsole03], where the class [ElectionsConsole03] has been written following the model of the test [Test03] in section 20.1.4.3.
20.3. Step 3
We will now proceed to port the Swing application to an asynchronous environment.
![]() |
We start by duplicating the [elections-swing-metier-dao-security-webjson] [1] project from Chapter 17.6 into a new project named [elections-swing-rxjava-metier-dao-security-webjson] [2]:
![]() | ![]() |
- In [3, 4], we remove the dependency on the synchronous layer [console];
![]() | ![]() |
- in [5-9], we add a dependency on the asynchronous console layer;
The [swing] layer will make true asynchronous calls to the [métier] layer. When calling a method on the latter, there will be two threads:
- the UI thread, which handles events;
- an I/O thread that will execute the HTTP call to the server;
Throughout the duration of the asynchronous call, we should display a loading image and a cancel button. We will not do this here, and it will be suggested to you as an improvement to the application. The changes are made in the two classes that make calls to the [métier] layer:
![]() |
20.3.1. Maven Configuration
Here, we will use the [RxSwing] library, which adds features to the [RxJava] library that are only available in a Swing environment. To do this, we modify the [pom.xml] file as follows:
<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>
<!-- lower layers -->
<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. The [ElectionsConnectForm] class
In asynchronous operation, the [ElectionsConnectForm] class becomes the following:
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;
// reference to the asynchronous [business] layer
@Autowired
private IRxElectionsMetier metier;
// logged-in user
private User user;
// main form
@Autowired
private ElectionsMainForm electionsMainForm;
// session UI
@Autowired
private UiSession uiSession;
@Override
protected void doConnect() {
if (isPageValid()) {
// user authentication
metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(
// there is no answer
(result) -> {
},
// exception management
(th) -> {
// we note the error
String info = getInfoForException("Les erreurs suivantes se sont produites :", th);
// display info
jTextPaneErreurs.setText(info);
jTextPaneErreurs.setCaretPosition(0);
},
// authentication is complete
() -> {
// the user is saved in the session
uiSession.setUser(user);
// connection view is hidden
setVisible(false);
// the main view is displayed
electionsMainForm.run();
});
}
}
// initializations
@Override
protected void init() {
...
}
@Override
public void run() {
// the graphical interface is displayed
SwingUtilities.invokeLater(new Runnable() {
public void run() {
init();
setVisible(true);
}
});
}
private boolean isPageValid() {
...
}
private String getInfoForException(String message, Throwable ex) {
...
}
}
- lines 36–63: the [doConnect] method is executed when the user presses the option menu item [Connexion]:
![]() |
It's all in line 40:
metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(...)
- The observed process is [metier.authenticate(user)];
- it will be executed on an I/O thread taken from the [Schedulers.io()] pool;
- it will be observed in the UI thread, which handles events from the Swing interface [observeOn(SwingScheduler.getInstance())]. This thread is obtained via the [SwingScheduler.getInstance()] method, where [SwingScheduler] is a class provided by the [RxSwing] library. This is mandatory. When the result of the asynchronous operation is obtained, it is often used to modify elements of the Swing interface. However, the interface can only be modified in the UI thread; otherwise, an exception occurs. Lines 41–61 must therefore be executed in the UI thread. This is ensured here by the [observeOn(SwingScheduler.getInstance())] method;
Let’s comment on the rest of the code:
- lines 42–43: these lines are there to comply with the syntax of the [subscribe] method. They will never be executed because the [metier.authenticate(user)] process returns no result;
- lines 35–52: upon receiving an exception, it is displayed;
- lines 54–61: executed when the [metier.authenticate(user)] process signals the end of its output;
20.3.3. The [ElectionsMainForm] class
![]() |
20.3.3.1. Initialization of the graphical user interface
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;
// reference to the asynchronous [business] layer
@Autowired
private IRxElectionsMetier metier;
// session UI
@Autowired
private UiSession uiSession;
// logged-in user
private User user;
// list templates JList
private DefaultListModel<String> modèleNomsVoix = null;
private DefaultListModel<String> modèleRésultats = null;
// competing lists
private ListeElectorale[] listes;
// user-entered lists
private final List<ListeElectorale> listesSaisies = new ArrayList<>();
private ListeElectorale[] tListesSaisies;
// initializations
@Override
protected void init() {
// generation of components by the parent class
super.init();
// form status
Utilitaires.setEnabled(new JLabel[]{jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer}, false);
Utilitaires.setEnabled(
new JMenuItem[]{jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer}, false);
// center window
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);
// logged-in user
user = uiSession.getUser();
// local initializations
modèleNomsVoix = new DefaultListModel<>();
jListNomsVoix.setModel(modèleNomsVoix);
modèleRésultats = new DefaultListModel<>();
jListResultats.setModel(modèleRésultats);
// lists are requested from the [business] layer
metier.getListesElectorales(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// answer
listesElectorales -> {
// memorize lists
listes = listesElectorales;
},
// exception
(th) -> showException(th),
// observable purpose
() -> {
// next step
doInitStep2();
});
}
...
- line 46: the [init] method is executed when the associated window is about to be displayed. Its purpose is to initialize the [1-3] components below:
![]() |
- lines 71–85: the candidate lists are requested asynchronously (component [1]);
- Line 71: The observed process is [metier.getListesElectorales(user)]. It is running on an I/O thread ([subscribeOn(Schedulers.io())]) and is being observed on the UI and [observeOn(SwingScheduler.getInstance()] threads;
- lines 74–77: the result returned by the observed process is stored in the [listes] field on line 38;
- line 79: any exception is handled by the following method:
private void showException(Throwable th) {
// exception is displayed
jTextPaneMessages.setText(getInfoForException("Les erreurs suivantes se sont produites : ", th));
jTextPaneMessages.setCaretPosition(0);
}
- lines 81-84: at the end of the observed process, lines 81-84 are executed. These lines are not executed if an exception occurred. The method [doInitStep2] handles step 2 of the initialization as follows:
private void doInitStep2() {
// associate list names with the jComboBoxNomsListes combo
for (int i = 0; i < listes.length; i++) {
jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
}
// number of seats to be filled
metier.getNbSiegesAPourvoir(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// answer
nbSiegesAPourvoir -> {
// on initialise le label lié à cette information
jLabelSAP.setText(jLabelSAP.getText() + nbSiegesAPourvoir);
},
// exception
(th) -> showException(th),
// observable purpose
() -> {
// next step
doInitStep3();
});
}
- lines 3–5: we use the result from the previous step to populate the dropdown list with the names of the candidate lists;
- lines 7–20: we request the number of seats to be filled asynchronously;
- line 7: the observed process is [metier.getNbSiegesAPourvoir(user)]. It is executed on an I/O thread [subscribeOn(Schedulers.io())] and observed on the thread UI [observeOn(SwingScheduler.getInstance()];
- lines 10–13: the result returned by the process is used to update the graphical user interface;
- line 15: any exceptions are displayed;
- lines 17–20: upon receiving the end signal from the observable, we proceed to step 3 of the initialization process;
Step 3 of the initialization is handled by the following code:
private void doInitStep3() {
// electoral threshold
metier.getSeuilElectoral(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// answer
seuilElectoral -> {
// on initialise le label lié à cette information
jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
},
// exception
(th) -> showException(th),
// observable purpose
() -> {
});
}
- lines 3-4: the electoral threshold is requested asynchronously;
- line 3: the observed process is [metier.getSeuilElectoral(user)]. It is executed on an I/O thread [subscribeOn(Schedulers.io())] and observed on the thread UI [observeOn(SwingScheduler.getInstance()];
- lines 6–9: The result returned by the process is used to update the GUI;
- line 11: any exceptions are displayed;
- lines 13–14: upon receiving the end-of-observable signal, no action is taken: the GUI initialization process is complete;
20.3.3.2. Calculating the seats won by the various lists
The [doCalculer] method calculates the number of seats won by the various lists:
@Override
protected void doCalculer() {
tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
// calculation of seats
String info = null;
metier.calculerSieges(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// processing result
result -> consumeResultSieges(result),
// exceptional treatment
th -> showException(th),
// observable purpose
() -> {
}
);
}
- lines 6–15: the seats won by the various lists are calculated asynchronously;
- line 6: the observed process is [metier.calculerSieges(user, tListesSaisies)]. It is executed on an I/O thread [subscribeOn(Schedulers.io())] and observed on the thread of UI [observeOn(SwingScheduler.getInstance()];
- line 9: the result returned by the process is used by the [consumeResultSieges] method;
- line 11: any exception is displayed;
- lines 13–14: upon receiving the end signal from the observable, no action is taken;
Line 9: the [consumeResultSieges] method processes the result returned by the observed process, the candidate lists with their [sieges, elimine] fields updated:
private void consumeResultSieges(ListeElectorale[] tListesSaisies) {
// the result is stored
this.tListesSaisies = tListesSaisies;
// display of results
modèleRésultats.clear();
for (int i = 0; i < tListesSaisies.length; i++) {
modèleRésultats.addElement(tListesSaisies[i].toString());
}
// maj state form
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é");
}
- lines 4-14: the result is used to update the GUI;
20.3.3.3. Recording the election results
The election results are recorded using the following [doEnregistrer] method:
@Override
protected void doEnregistrer() {
// registration is requested from the [business] layer
metier.recordResultats(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// result processing - there's none here
(param) -> {
},
// exception handling
(th) -> showException(th),
// observable purpose
() -> {
// uPGRADE FORM
Utilitaires.setEnabled(new JLabel[]{jLabelEnregistrer}, false);
Utilitaires.setEnabled(new JMenuItem[]{jMenuItemEnregistrer}, false);
jTextPaneMessages.setText("Enregistrement des résultats réalisé");
}
);
}
- lines 4–17: the election results are saved asynchronously;
- line 4: the observed process is [metier.recordResultats(user, tListesSaisies)]. It is executed on an I/O thread [subscribeOn(Schedulers.io())] and observed on the UI [observeOn(SwingScheduler.getInstance()] thread;
- lines 7-8: these lines will never be executed because the observed process does not return a result;
- line 10: any exception is displayed;
- lines 14-16: upon receiving the end signal from the observable, the GUI is updated;
Task: Verify that the Swing application works. Then modify the GUI and the code so that during an asynchronous operation with the web server / jSON, a loading image appears along with a option to cancel the current operation.























