20. Programação assíncrona com RxJava
Documento a ler: [Introduction à RxJava. Application aux environnements Swing et Android.]
Neste capítulo, voltamos ao capítulo 17.6, onde criámos uma aplicação cliente/servidor com a seguinte arquitetura:
![]() |
Algumas ações do utilizador na interface Swing em [1] desencadeiam ações que chegam até à base de dados em [3] através de uma rede HTTP [2]. Devido a isso, a resposta à ação do utilizador pode demorar mais ou menos tempo a chegar. Seria bom poder colocar um indicador de espera na interface do utilizador com uma opção para cancelar a operação iniciada, caso esta demore demasiado tempo. No capítulo 17.6, cada ação do utilizador que exija a troca de informações com o servidor é síncrona. O gestor de eventos executado pelo código só termina quando a resposta é recebida. Durante todo esse tempo, a interface gráfica fica congelada: não responde a novas ações do utilizador. Estas ações são simplesmente colocadas numa fila para serem processadas assim que o gestor de eventos que está atualmente a ser executado terminar. Assim, se fosse apresentado um botão de cancelamento, o utilizador poderia clicar nele, mas nada aconteceria enquanto a operação em curso não estivesse concluída. O botão de cancelamento não teria, então, qualquer utilidade.
Para que o clique no botão de cancelamento tenha efeito, é necessário que a operação em curso esteja concluída. Para tal, deve iniciar a operação potencialmente demorada de forma assíncrona:
- o gestor de eventos inicia a operação demorada, mas não aguarda o seu resultado e devolve o controlo ao thread do UI, que gere os eventos da interface gráfica. A operação demorada é iniciada num thread diferente do do UI, o que não bloqueia este último;
- se o utilizador clicar no botão de cancelamento antes do fim da operação demorada, o thread do UI, que está desocupado, pode processar esse evento. É então possível abandonar a operação demorada, ignorando o seu resultado;
- se a operação demorada não tiver sido cancelada, a chegada da resposta irá provocar um evento na thread do UI. Esta, se estiver desocupada, irá então executar o código associado a esse evento, que irá processar a resposta;
A interface do utilizador funcionará como anteriormente. Se os tempos de resposta do servidor forem rápidos, o utilizador não notará a diferença. Se forem percetíveis, o utilizador verá aparecer um botão de cancelamento e terá a possibilidade de interromper a operação em curso.
A biblioteca [Rx] permite a programação assíncrona. O seu grande interesse reside no facto de ter sido portada para vários ambientes (Java, .NET, JS, ...) e que o domínio da mesma num ambiente pode ser facilmente transposto para outro. Vamos basear-nos aqui no capítulo 2 do documento [Introduction à RxJava. Application aux environnements Swing et Android]. Convidamos o leitor a lê-lo. A seguir, retomamos código proveniente dos exemplos desse capítulo.
Vamos desenvolver a arquitetura da aplicação da seguinte forma:
![]() |
- no [1], inserimos uma camada [RxJava] entre a camada [swing] e a camada [métier]. Os métodos desta última passarão a ser chamados de forma assíncrona;
Vamos proceder em várias etapas:
- etapa 1: a camada [metier, DAO] apresenta, por enquanto, uma interface síncrona com a camada [ui]. Vamos transformá-la numa camada assíncrona [RxJava, metier, DAO];
- etapa 2: iremos converter a aplicação de consola síncrona numa aplicação que continue a ser síncrona, mas que utilize a interface assíncrona [RxJava, metier, DAO];
- etapa 3: iremos converter a aplicação Swing síncrona numa aplicação Swing assíncrona;
20.1. etapa 1
Transformamos a camada síncrona atual [metier, DAO] numa camada assíncrona [RxJava, metier, DAO].
20.1.1. Criação
Partimos do projeto Maven do capítulo 17.4, que abrimos com o NetBeans:
![]() | ![]() |
Duplicamos este projeto [1] (copiar/colar) num novo projeto [elections-rxjava-metier-dao-security-webjson] [2].
20.1.2. Configuração do Maven
Alteramos o ficheiro [pom.xml] do novo projeto para adicionar a dependência da 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 pelo 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 pelo 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 registos -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Teste do Spring Boot -->
<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>
- linhas 65-70: adicionámos a dependência da biblioteca RxJava;
20.1.3. Implementação assíncrona da camada [métier]
![]() |
Para implementar a camada [RxJava, métier], adicionamos uma interface assíncrona [IRxElectionsMetier] [1] e a sua implementação [RxElectionsMetier] [2] ao projeto:
![]() |
A interface [IRxElectionsMetier] é a interface assíncrona da camada [RxJava, métier]. O seu código é o seguinte:
package elections.security.client.metier;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import rx.Observable;
public interface IRxElectionsMetier {
// autenticação
Observable<Void> authenticate(User user);
// obter as listas em competição
Observable<ListeElectorale[]> getListesElectorales(User user);
// o número de lugares a preencher
Observable<Integer> getNbSiegesAPourvoir(User user);
// limiar eleitoral
Observable<Double> getSeuilElectoral(User user);
// registo dos resultados
Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales);
// cálculo dos lugares
Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales);
}
A interface [IRxElectionsMetier] retoma os métodos da interface [IElectionsMetier], mas, enquanto um método M da interface [IElectionsMetier] devolvia um resultado do tipo T, o método M da interface [IRxElectionsMetier] devolve um resultado do tipo Observable<T>. O tipo [Observable] é fornecido pela biblioteca RxJava. Um tipo Observable<T> fornece o método [subscribe], que irá obter o tipo T de forma assíncrona. Estão associados a este método três eventos:
- onSuccess(T result), que avisa que está disponível um resultado do tipo T. A operação assíncrona pode fornecer vários resultados;
- onError(Throwable th), que avisa que a operação assíncrona encontrou um erro;
- onCompleted(), que avisa que a operação assíncrona está concluída;
Enquanto o método [Observable.subscribe] não for chamado, a operação assíncrona associada ao observável não é iniciada. O código que chama um método M da interface [IRxElectionsMetier] não obtém o resultado T esperado, mas sim um tipo Observable<T> que lhe permitirá, posteriormente, obter o resultado T ao chamar o método [Observable.subscribe].
A implementação [RxElectionsMetier] da interface [IRxElectionsMetier] é a seguinte:
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 {
// chamada do método síncrono e, em seguida, resposta ao subscritor
subscriber.onNext(metier.getListesElectorales(user));
// é sinalizado o fim do observável
subscriber.onCompleted();
} catch (Exception e) {
// a exceção é encaminhada
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) {
...
}
}
- linhas 12-13: injeção Spring da camada de negócio síncrona;
- linhas 20-34: vamos comentar o método [getListesElectorales] que, em vez de devolver um tipo [ListeElectorale[]] devolve um tipo [Observable<ListeElectorale[]>];
- linhas 22-32: o método estático [Observable.create] permite criar um Observable a partir de um tipo [Subscriber]. O tipo [Subscriber] representa um subscritor dos fluxos de resultados produzidos pelo processo observado (o Observable). Este tipo disponibiliza três métodos:
- [Subscriber.onNext] (linha 25) para receber um resultado do processo observado;
- [Subscriber.onError] (linha 30) para receber uma exceção do processo observado. Após uma exceção, o tipo [Observable] deixa de emitir resultados;
- [Subscriber.onCompleted] (linha 27) para receber o sinal de fim de emissão do processo observado. Neste caso, o processo observado emite apenas um elemento. Note-se que este sinal não é emitido caso ocorra uma exceção. Este é o comportamento por predefinição dos Observáveis: a emissão de uma exceção também sinaliza o fim das emissões. Os subscritores sabem disso;
- linhas 22-34: o método [Observable.create] aceita como parâmetro um tipo [Observable.OnSubscribe]. Este tipo é uma interface funcional. Este conceito foi introduzido com o Java 8 e designa uma interface com um único método. Neste caso, o único método da interface [Observable.OnSubscribe] é o seguinte:
Para implementar uma interface funcional com um único método m(param1, param2, ..., paramn), pode utilizar-se a seguinte sintaxe simplificada:
É isso que se faz nas linhas 22-34:
- [subscriber] é o parâmetro do método [Observable.OnSubscribe.call];
- linhas 23-32: o código que se pretende atribuir ao método [call];
- linha 25: solicitam-se as listas eleitorais de forma síncrona à camada [métier] injetada na linha 13. Haverá, portanto, uma espera pelo resultado. Quando este for recebido, é passado para o método [onNext] do subscritor;
- linha 28: em caso de erro, a exceção é passada para o método [onError] do subscritor;
- linha 31: aguarda-se apenas um resultado. Quando este for obtido (as listas eleitorais ou uma exceção), indica-se ao subscritor que o processo observado terminou de emitir resultados;
É importante lembrar que o método [RxElectionsMetier] devolve um tipo Observable<ListeElectorale[]> e não o próprio tipo ListeElectorale[]. O código chamador terá de chamar o método Observable<ListeElectorale[]>.subscribe para que o código das linhas 23-33 seja executado e devolva as listas eleitorais através da linha 25.
O código dos outros métodos é semelhante:
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 {
// chamada de método síncrono
metier.authenticate(user);
// é sinalizado o fim do observável
subscriber.onCompleted();
} catch (Exception e) {
// a exceção é encaminhada
subscriber.onError(e);
}
});
}
@Override
public Observable<ListeElectorale[]> getListesElectorales(User user) {
return Observable.create(subscriber -> {
try {
// chamada de método síncrono e, em seguida, resposta ao subscritor
subscriber.onNext(metier.getListesElectorales(user));
// é sinalizado o fim do observável
subscriber.onCompleted();
} catch (Exception e) {
// a exceção é encaminhada
subscriber.onError(e);
}
});
}
@Override
public Observable<Integer> getNbSiegesAPourvoir(User user) {
return Observable.create(subscriber -> {
try {
// chamada ao método síncrono e, em seguida, resposta ao subscritor
subscriber.onNext(metier.getNbSiegesAPourvoir(user));
// é sinalizado o fim do observável
subscriber.onCompleted();
} catch (Exception e) {
// a exceção é encaminhada
subscriber.onError(e);
}
});
}
@Override
public Observable<Double> getSeuilElectoral(User user) {
return Observable.create(subscriber -> {
try {
// chamada ao método síncrono e, em seguida, resposta ao subscritor
subscriber.onNext(metier.getSeuilElectoral(user));
// é sinalizado o fim do observável
subscriber.onCompleted();
} catch (Exception e) {
// a exceção é encaminhada
subscriber.onError(e);
}
});
}
@Override
public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
return Observable.create(subscriber -> {
try {
// chamada de método síncrono
metier.recordResultats(user, listesElectorales);
// é sinalizado o fim do observável
subscriber.onCompleted();
} catch (Exception e) {
// a exceção é encaminhada
subscriber.onError(e);
}
});
}
@Override
public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
return Observable.create(subscriber -> {
try {
// chamada de método síncrono seguida de resposta ao subscritor
subscriber.onNext(metier.calculerSieges(user, listesElectorales));
// é sinalizado o fim do observável
subscriber.onCompleted();
} catch (Exception e) {
// a exceção é encaminhada
subscriber.onError(e);
}
});
}
}
- linhas 20 e 81: o método [onNext] do subscritor não é chamado porque este não espera resultados;
20.1.4. Os testes JUnit da camada [métier]
![]() |
20.1.4.1. Test01
Retomamos o teste unitário [Test01] analisado no parágrafo 17.4.4. Este foi concebido para efetuar chamadas síncronas à interface [IElectionsMetier]. Alteramo-lo para que efetue chamadas síncronas à nova interface [IRxElectionsMetier]. Com efeito, é possível efetuar chamadas síncronas a uma interface assíncrona RxJava. O código passa a ser o seguinte:
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 {
// camada [electionsMetier]
@Autowired
private IRxElectionsMetier electionsMetier;
// mapeador jSON
private final ObjectMapper mapper = new ObjectMapper();
// utilizadores
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() {
// cria-se a tabela das 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);
// calculam-se os lugares de cada uma das listas
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// verificam-se os 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() {
// cria-se a tabela das 7 listas de candidatos
ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// fixam-se os 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);
// calculam-se os lugares obtidos por cada uma das listas
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// verifica-se os 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() {
// cria-se uma tabela com 24 listas candidatas, cada uma com 1 voto
ListeElectorale[] listes = new ListeElectorale[25];
// as 25 listas terão o mesmo 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 dos lugares — normalmente deve resultar um ElectionsException
// com um limiar eleitoral de 5%
BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
}
/**
* enregistrement des résultats de l'élection
*
* @throws JsonProcessingException
*/
@Test
public void ecritureResultatsElections() throws JsonProcessingException {
// cria-se a tabela das 7 listas candidatas
ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// definem-se os votos de forma fixa
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);
// calculam-se os lugares obtidos por cada uma das listas
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// são apresentados os resultados
for (int i = 0; i < listes.length; i++) {
System.out.println(mapper.writeValueAsString(listes[i]));
}
// os resultados são gravados na base de dados
BlockingObservable.from(electionsMetier.recordResultats(admin, listes)).firstOrDefault(null);
// verifica-se os resultados
listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// são apresentados os 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());
}
}
Vamos analisar as alterações:
- linha 48: o método estático [BlockingObservable.from(Observable).first]:
- subscreve o parâmetro observável de [from];
- inicia a execução do código associado ao observável;
- aguarda a receção do primeiro resultado. Trata-se, portanto, de uma operação síncrona;
Utilizamos aqui o método [firstOrDefault(null)] porque o observável [metier.authenticate] não devolve qualquer resultado quando é executado. O resultado do método [firstOrDefault(null)] será, portanto, null, valor que não é utilizado aqui;
Retomamos este esquema no resto do código sempre que queremos recorrer à camada [métier].
O teste unitário [Test01] deve ser bem-sucedido:
![]() |
Tarefa a realizar: verificar se o teste [Test01] é bem-sucedido.
20.1.4.2. Test02
Alteramos o teste [Test01] para, a partir de agora, testar a interface assíncrona [IRxElectionsMetier], efetuando chamadas assíncronas aos seus métodos.
Analisemos um primeiro teste:
// semáforo de sincronização de threads
private CountDownLatch latch;
// -----------------------------------
private ElectionsException checkUserUserException;
@Test()
public void checkUserUser() throws InterruptedException {
// semáforo com valor 1
latch = new CountDownLatch((1));
// operação assíncrona
electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
.subscribe((result) -> {
},
(th) -> {
checkUserUserException = (ElectionsException) th;
latch.countDown();
},
() -> {
latch.countDown();
});
// espera pelo semáforo
latch.await();
// verificação dos resultados
Assert.assertNotNull(checkUserUserException);
Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
- linha 2: um semáforo é uma ferramenta utilizada para sincronizar threads entre si. As threads são fluxos de execução que decorrem em paralelo. Para executar uma tarefa T1, o thread [Thread1] pode necessitar que uma tarefa T2, executada por um thread [Thread2], esteja concluída. Assim, aguarda que o thread [Thread2] lhe envie um sinal a indicar que a tarefa T2 está concluída. Existem várias formas de gerir esta sincronização entre dois threads. O método utilizado aqui é o seguinte:
- linha 10: o thread [Thread1] cria um semáforo com o valor 1;
- linha 12: o thread [Thread1] cria e inicia um thread [Thread2]. Isto é conseguido através da sintaxe:
electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
O método [Observable.subscribeOn] define o thread no qual o processo observado será executado. O parâmetro de [subscribeOn] é um conjunto de threads. A biblioteca RxJava fornece vários conjuntos adaptados a diferentes situações. O conjunto [Schedulers.io()] é o recomendado para operações de rede;
- (continuação)
- linhas 12-13: a operação
electionsMetier.authenticate(user).subscribeOn(Schedulers.io()).subscribe(...)
executa a operação síncrona encapsulada no observável [authenticate(user)]. Mas, como esta operação síncrona é iniciada num thread diferente do thread [Thread1], este último não aguarda a resposta do método [subscribe] e passa para a instrução seguinte;
- (continuação)
- linha 23: o thread [Thread1] pára e aguarda que o semáforo passe para 0 (neste momento está em 1);
- linhas 13-21: o método [subscribe] aceita como parâmetros três funções lambda:
- a primeira, [(result)->{...}], é chamada sempre que o observável [authenticate(user)] emite um resultado [result]. Aqui temos um observável [authenticate(user)] que faz alguma coisa, mas não emite nenhum resultado. A função lambda [(result)->{}] nunca será, portanto, chamada. É por isso que o seu código está vazio: [{}];
- o segundo, [(th)->{...}], recebe como parâmetro um tipo [Throwable]. É chamado quando a execução do observável encontra uma exceção. Aqui, tratamos o parâmetro [Throwable th] da seguinte forma:
- linha 16: armazenamo-lo num campo da classe de teste do tipo [ElectionsException], uma vez que o observável executado apenas emite este tipo de exceção;
- linha 17: colocamos o semáforo a 0 para indicar que o thread [Thread2] terminou o seu trabalho;
- o terceiro, [()->{...}], é chamado quando o observável já não tem mais elementos para emitir. Tratamos este evento da seguinte forma:
- linha 20: colocamos o semáforo a 0 para indicar que o thread [Thread2] concluiu o seu trabalho;
É importante notar que a terceira função lambda não é chamada se ocorrer uma exceção. Por isso, fomos obrigados a definir o semáforo para 0 também na linha 17;
- linha 25: quando chegamos a esta linha, o observável concluiu o seu trabalho. Podemos então efetuar as mesmas verificações que no teste [Test01];
Vamos analisar outro teste:
// -----------------------------------
private ElectionsException calculSieges1Exception;
private ListeElectorale[] listesCalculSieges1;
@Test
public void calculSieges1() throws InterruptedException {
// cria-se a tabela das 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));
// operação assíncrona
// cálculo dos lugares de cada uma das listas
electionsMetier.calculerSieges(admin, listes).subscribeOn(Schedulers.io())
.subscribe((result) -> {
listesCalculSieges1 = result;
},
(th) -> {
calculSieges1Exception = (ElectionsException) th;
latch.countDown();
},
() -> {
latch.countDown();
});
// espera pelo semáforo
latch.await();
// verifica-se os 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());
}
- linhas 20-30: execução assíncrona do observável [electionsMetier.calculerSieges(admin, listes)];
- linhas 21-23: a execução do observável devolve um tipo [ListeElectorale[]] que é armazenado num campo da classe de teste, linha 3;
- linhas 34-48: estas verificações correspondem às do teste [Test01], às quais foi adicionada a verificação da linha 34, que garante que não ocorreu nenhuma exceção;
O teste [Test02] na íntegra está disponível nos materiais do curso.
Tarefa a realizar: executar o teste [Test02] e verificar se este é bem-sucedido.
20.1.4.3. Test03
O teste [Test03] faz o mesmo que o teste [Test01]: testa a interface [IRxElectionsMetier] através de chamadas síncronas a essa interface. Trata-se de uma cópia do teste [Test02], com duas diferenças:
- os observáveis já não são executados num thread diferente daquele que executa os testes. Quando o thread [Thread1] executa o método [subscribe] de um observável, este inicia uma operação HTTP para o servidor, também no thread [Thread1]. Todo o método [subscribe] torna-se então síncrono;
- uma vez que já só existe um thread, a sincronização de threads torna-se desnecessária e o semáforo desaparece;
Eis dois exemplos de testes:
// -----------------------------------
private ElectionsException checkUserUserException;
@Test()
public void checkUserUser() throws InterruptedException {
// operação síncrona
electionsMetier.authenticate(user)
.subscribe((result) -> {
},
(th) -> {
checkUserUserException = (ElectionsException) th;
},
() -> {
});
// verificação dos resultados
Assert.assertNotNull(checkUserUserException);
Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
- linha 7: por predefinição, o método [electionsMetier.authenticate(user).subscribe] é executado no thread do código chamador. Temos, portanto, uma operação síncrona;
// -----------------------------------
private ElectionsException calculSieges1Exception;
private ListeElectorale[] listesCalculSieges1;
@Test
public void calculSieges1() throws InterruptedException {
// cria-se a tabela das 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);
// operação síncrona
// calculam-se os lugares de cada uma das listas
electionsMetier.calculerSieges(admin, listes)
.subscribe((result) -> {
listesCalculSieges1 = result;
},
(th) -> {
calculSieges1Exception = (ElectionsException) th;
},
() -> {
});
// verificação dos 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());
}
Tarefa a realizar: passar o teste [Test03] e verificar se este é bem-sucedido.
20.2. Etapa 2
Vamos agora converter a aplicação de consola síncrona do capítulo 17.5 numa aplicação que continua a ser síncrona, mas que utiliza a interface assíncrona [RxJava, metier, DAO];
![]() |
Partimos do projeto [elections-console-metier-dao-security-webjson] [1] do capítulo 17.5, que duplicamos num novo projeto [elections-console-rxjava- metier-dao-security-webjson] [2]:
![]() | ![]() |
- em [3-4]; no novo projeto, eliminamos a dependência da antiga camada síncrona [métier];
![]() | ![]() |
- em [5-9], adiciona-se uma dependência da nova camada assíncrona [métier];
![]() | ![]() |
- em [10-14], renomeia-se a classe [ElectionsConsole] para [ElectionsConsole01];
Da mesma forma, renomeia-se a classe [BootElectionsConsole] para [BootElectionsConsole01]:
![]() |
O código atual da classe [BootElectionsConsole01] é o seguinte:
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);
}
}
- linha 13: uma vez que alterámos o nome da classe [ElectionsConsole] para [ElectionsConsole01], agora é necessário escrever:
return ctx.getBean("electionsConsole01",IElectionsUI.class);
Voltemos ao código da classe [ElectionsConsole01]:
@Component
public class ElectionsConsole01 implements IElectionsUI {
@Autowired
private IElectionsMetier electionsMetier;
@Autowired
private User admin;
@Override
public void run() {
// as listas em competição
ListeElectorale[] listes;
// introdução de dados
try (Scanner clavier = new Scanner(System.in)) {
// solicitam-se as listas em competição à camada [metier]
listes = electionsMetier.getListesElectorales(admin);
...
// cálculo dos lugares
listes=electionsMetier.calculerSieges(admin,listes);
// registro dos resultados
electionsMetier.recordResultats(admin,listes);
...
}
Se seguirmos o exemplo do teste [Test01] do parágrafo 20.1.4.1, as linhas 5, 17, 20 e 22 sofrerão as seguintes alterações:
@Component
public class ElectionsConsole01 implements IElectionsUI {
@Autowired
private IRxElectionsMetier electionsMetier;
@Autowired
private User admin;
@Override
public void run() {
// as listas em disputa
ListeElectorale[] listes;
// introdução dos dados
try (Scanner clavier = new Scanner(System.in)) {
// solicitam-se as listas em concorrência à camada [metier]
listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
...
// cálculo dos lugares
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// os resultados são registados
BlockingObservable.from(electionsMetier.recordResultats(admin, listes));
...
}
Tarefa: configure o projeto para executar a classe [BootElectionsConsole01] com os três parâmetros [SS, Heures travaillées, Jours travaillés] e verifique se a execução do projeto assim configurado produz os resultados esperados.
Tarefa a realizar: configure o projeto para executar o par [BootElectionsConsole02, ElectionsConsole02], em que a classe [ElectionsConsole02] terá sido escrita seguindo o modelo do teste [Test02] do parágrafo 20.1.4.2.
Tarefa a realizar: configure o projeto para executar o par [BootElectionsConsole03, ElectionsConsole03], em que a classe [ElectionsConsole03] terá sido escrita seguindo o modelo do teste [Test03] do parágrafo 20.1.4.3.
20.3. Etapa 3
Passamos agora à adaptação da aplicação Swing para um ambiente assíncrono.
![]() |
Começamos por duplicar o projeto [elections-swing-metier-dao-security-webjson] [1] do capítulo 17.6, num novo projeto [elections-swing-rxjava-metier-dao-security-webjson] [2]:
![]() | ![]() |
- no [3, 4], eliminamos a dependência da camada síncrona [console];
![]() | ![]() |
- em [5-9], adicionamos uma dependência da camada de consola assíncrona;
A camada [swing] irá efetuar chamadas assíncronas reais à camada [métier]. Ao chamar um método desta última, haverá duas threads:
- o thread da UI, que gere os eventos;
- um thread de E/S que executará a chamada HTTP ao servidor;
Durante todo o tempo que durar a chamada assíncrona, deveríamos apresentar uma imagem de espera, bem como um botão de cancelamento. Não o faremos aqui, mas isso será sugerido como uma melhoria da aplicação. As alterações ocorrem nas duas classes que fazem chamadas à camada [métier]:
![]() |
20.3.1. Configuração do Maven
Vamos utilizar aqui a biblioteca [RxSwing], que adiciona à biblioteca [RxJava] funcionalidades disponíveis apenas num ambiente Swing. Para tal, alteramos o ficheiro [pom.xml] da seguinte forma:
<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>
<!-- camadas 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. A classe [ElectionsConnectForm]
Num funcionamento assíncrono, a classe [ElectionsConnectForm] passa a ter o seguinte aspeto:
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;
// referência na camada [métier] assíncrona
@Autowired
private IRxElectionsMetier metier;
// utilizador conectado
private User user;
// formulário principal
@Autowired
private ElectionsMainForm electionsMainForm;
// sessão UI
@Autowired
private UiSession uiSession;
@Override
protected void doConnect() {
if (isPageValid()) {
// autenticação do utilizador
metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(
// não há resposta
(result) -> {
},
// gestão da exceção
(th) -> {
// registo do erro
String info = getInfoForException("Les erreurs suivantes se sont produites :", th);
// a informação é apresentada
jTextPaneErreurs.setText(info);
jTextPaneErreurs.setCaretPosition(0);
},
// a autenticação está concluída
() -> {
// o utilizador é guardado na sessão
uiSession.setUser(user);
// a página de início de sessão é ocultada
setVisible(false);
// a página principal é apresentada
electionsMainForm.run();
});
}
}
// inicializações
@Override
protected void init() {
...
}
@Override
public void run() {
// é apresentada a interface gráfica
SwingUtilities.invokeLater(new Runnable() {
public void run() {
init();
setVisible(true);
}
});
}
private boolean isPageValid() {
...
}
private String getInfoForException(String message, Throwable ex) {
...
}
}
- linhas 36-63: o método [doConnect] é executado quando o utilizador clica na opção de menu [Connexion]:
![]() |
Tudo está na linha 40:
metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(...)
- o processo observado é o [metier.authenticate(user)];
- será executado num thread de E/S retirado do pool [Schedulers.io()];
- será observado na thread UI, que gere os eventos da interface Swing [observeOn(SwingScheduler.getInstance())]. Esta thread é obtida através do método [SwingScheduler.getInstance()], em que [SwingScheduler] é uma classe fornecida pela biblioteca [RxSwing]. Isto é obrigatório. Ao obter o resultado da operação assíncrona, este é frequentemente utilizado para alterar elementos da interface Swing. No entanto, esta só pode ser alterada na thread do UI; caso contrário, ocorre uma exceção. Por isso, as linhas 41-61 têm de ser executadas no thread do UI. Isso é garantido aqui pelo método [observeOn(SwingScheduler.getInstance())];
Vamos comentar o resto do código:
- linhas 42-43: estas linhas existem para respeitar a sintaxe do método [subscribe]. Nunca serão executadas, pois o processo [metier.authenticate(user)] não devolve qualquer resultado;
- linhas 35-52: ao receber uma exceção, esta é apresentada;
- linhas 54-61: executadas quando o processo [metier.authenticate(user)] sinaliza o fim das suas emissões;
20.3.3. A turma [ElectionsMainForm]
![]() |
20.3.3.1. Inicialização da interface 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;
// referência à camada assíncrona [métier]
@Autowired
private IRxElectionsMetier metier;
// sessão UI
@Autowired
private UiSession uiSession;
// utilizador conectado
private User user;
// modelos das listas JList
private DefaultListModel<String> modèleNomsVoix = null;
private DefaultListModel<String> modèleRésultats = null;
// listas em competição
private ListeElectorale[] listes;
// listas introduzidas pelo utilizador
private final List<ListeElectorale> listesSaisies = new ArrayList<>();
private ListeElectorale[] tListesSaisies;
// inicializações
@Override
protected void init() {
// geração de componentes pela classe pai
super.init();
// estado do formulário
Utilitaires.setEnabled(new JLabel[]{jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer}, false);
Utilitaires.setEnabled(
new JMenuItem[]{jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer}, false);
// centrar a janela
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);
// utilizador conectado
user = uiSession.getUser();
// inicializações locais
modèleNomsVoix = new DefaultListModel<>();
jListNomsVoix.setModel(modèleNomsVoix);
modèleRésultats = new DefaultListModel<>();
jListResultats.setModel(modèleRésultats);
// solicitação de listas à camada [métier]
metier.getListesElectorales(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// resposta
listesElectorales -> {
// as listas são armazenadas
listes = listesElectorales;
},
// exceção
(th) -> showException(th),
// fim observável
() -> {
// próximo passo
doInitStep2();
});
}
...
- linha 46: o método [init] é executado quando a janela associada está prestes a ser apresentada. Tem como objetivo inicializar os componentes [1-3] abaixo:
![]() |
- linhas 71-85: as listas de candidatos são solicitadas de forma assíncrona (componente [1]);
- linha 71: o processo observado é o [metier.getListesElectorales(user)]. É executado num thread de E/S [subscribeOn(Schedulers.io())] e observado no thread do UI [observeOn(SwingScheduler.getInstance()];
- linhas 74-77: o resultado devolvido pelo processo observado é armazenado no campo [listes] da linha 38;
- linha 79: qualquer exceção é tratada pelo método seguinte:
private void showException(Throwable th) {
// exibe-se a exceção
jTextPaneMessages.setText(getInfoForException("Les erreurs suivantes se sont produites : ", th));
jTextPaneMessages.setCaretPosition(0);
}
- linhas 81-84: no final do processo observado, executam-se as linhas 81-84. Estas linhas não são executadas se tiver ocorrido uma exceção. O método [doInitStep2] assegura a etapa 2 da inicialização da seguinte forma:
private void doInitStep2() {
// associa-se os nomes das listas ao menu suspenso jComboBoxNomsListes
for (int i = 0; i < listes.length; i++) {
jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
}
// número de vagas a preencher
metier.getNbSiegesAPourvoir(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// resposta
nbSiegesAPourvoir -> {
// inicializa-se o rótulo associado a esta informação
jLabelSAP.setText(jLabelSAP.getText() + nbSiegesAPourvoir);
},
// exceção
(th) -> showException(th),
// fim observável
() -> {
// próxima etapa
doInitStep3();
});
}
- linhas 3-5: utiliza-se o resultado da etapa anterior para preencher a lista suspensa com os nomes das listas de candidatos;
- linhas 7-20: solicita-se o número de lugares a preencher de forma assíncrona;
- linha 7: o processo observado é o [metier.getNbSiegesAPourvoir(user)]. É executado num thread de E/S [subscribeOn(Schedulers.io())] e observado no thread do UI [observeOn(SwingScheduler.getInstance()];
- linhas 10-13: o resultado devolvido pelo processo é utilizado para atualizar a interface gráfica;
- linha 15: exibe-se a eventual exceção;
- linhas 17-20: ao receber o sinal de fim do observável, passa-se para a etapa 3 do processo de inicialização;
A etapa 3 da inicialização é assegurada pelo código seguinte:
private void doInitStep3() {
// limiar eleitoral
metier.getSeuilElectoral(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// resposta
seuilElectoral -> {
// inicializa-se a etiqueta associada a esta informação
jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
},
// exceção
(th) -> showException(th),
// fim da observação
() -> {
});
}
- linhas 3-4: solicita-se o limiar eleitoral de forma assíncrona;
- linha 3: o processo observado é o [metier.getSeuilElectoral(user)]. É executado num thread de E/S [subscribeOn(Schedulers.io())] e observado no thread do UI [observeOn(SwingScheduler.getInstance()];
- linhas 6-9: o resultado devolvido pelo processo é utilizado para atualizar a interface gráfica;
- linha 11: é apresentada a eventual exceção;
- linhas 13-14: ao receber o sinal de fim do observável, nada é feito: o processo de inicialização da interface gráfica está concluído;
20.3.3.2. Cálculo dos lugares obtidos pelas diferentes listas
O método [doCalculer] tem como função calcular o número de lugares obtidos pelas diferentes listas:
@Override
protected void doCalculer() {
tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
// cálculo dos lugares
String info = null;
metier.calculerSieges(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// processamento do resultado
result -> consumeResultSieges(result),
// processamento de exceção
th -> showException(th),
// fim observável
() -> {
}
);
}
- linhas 6-15: calculam-se de forma assíncrona os lugares obtidos pelas diferentes listas;
- linha 6: o processo observado é o [metier.calculerSieges(user, tListesSaisies)]. É executado num thread de E/S [subscribeOn(Schedulers.io())] e observado no thread do UI [observeOn(SwingScheduler.getInstance()];
- linha 9: o resultado devolvido pelo processo é utilizado pelo método [consumeResultSieges];
- linha 11: é apresentada a eventual exceção;
- linhas 13-14: ao receber o sinal de fim do observável, nada é feito;
Na linha 9, o método [consumeResultSieges] utiliza o resultado devolvido pelo processo observado, as listas de candidatos com os seus campos [sieges, elimine] atualizados:
private void consumeResultSieges(ListeElectorale[] tListesSaisies) {
// o resultado é guardado
this.tListesSaisies = tListesSaisies;
// exibição dos resultados
modèleRésultats.clear();
for (int i = 0; i < tListesSaisies.length; i++) {
modèleRésultats.addElement(tListesSaisies[i].toString());
}
// atualização do estado do formulário
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é");
}
- linhas 4-14: o resultado obtido é utilizado para atualizar a interface gráfica;
20.3.3.3. Registo dos resultados da eleição
O registo dos resultados da eleição é efetuado através do seguinte método [doEnregistrer]:
@Override
protected void doEnregistrer() {
// solicita-se o registo à camada [métier]
metier.recordResultats(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// processamento do resultado — não há nenhum aqui
(param) -> {
},
// processamento da exceção
(th) -> showException(th),
// fim observável
() -> {
// atualização do formulário
Utilitaires.setEnabled(new JLabel[]{jLabelEnregistrer}, false);
Utilitaires.setEnabled(new JMenuItem[]{jMenuItemEnregistrer}, false);
jTextPaneMessages.setText("Enregistrement des résultats réalisé");
}
);
}
- linhas 4-17: os resultados da eleição são registados de forma assíncrona;
- linha 4: o processo observado é o [metier.recordResultats(user, tListesSaisies)]. É executado num thread de E/S [subscribeOn(Schedulers.io())] e observado no thread do UI [observeOn(SwingScheduler.getInstance()];
- linhas 7-8: estas linhas nunca serão executadas, uma vez que o processo observado não devolve qualquer resultado;
- linha 10: exibe-se a eventual exceção;
- linhas 14-16: ao receber o sinal de fim do observável, atualiza-se a interface gráfica;
Tarefa a realizar: verifique se a aplicação Swing funciona. Em seguida, adapte a interface gráfica e o código para que, durante uma operação assíncrona com o servidor web / jSON, apareça uma imagem de espera, bem como uma opção para cancelar a operação em curso.























