15. [TD]: criação de um cliente para o serviço web
Palavras-chave: arquitetura multicamadas, Spring, injeção de dependências, serviço web / jSON, cliente / servidor
15.1. Support
![]() |
Os projetos deste capítulo encontram-se na pasta [support / chap-15].
15.2. A arquitetura cliente/servidor
Pretendemos criar a seguinte arquitetura cliente/servidor:
![]() |
A camada [ui] será a que já foi desenvolvida nos parágrafos 9 e 10. Isto será possível porque a camada [métier] acima referida implementará a mesma interface [IElectionsMetier] que a camada [métier] do parágrafo 8:
package elections.client.metier;
import elections.client.entities.ListeElectorale;
public interface IElectionsMetier {
// obter as listas em competição
public ListeElectorale[] getListesElectorales();
// o número de lugares a preencher
public int getNbSiegesAPourvoir();
// o limiar eleitoral
public double getSeuilElectoral();
// o registo dos resultados
public void recordResultats(ListeElectorale[] listesElectorales);
// cálculo dos lugares
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
}
No parágrafo 7, a camada [DAO] trocava dados com uma SGBD. Aqui, a camada [DAO] troca dados com um servidor web / jSON.
Numa primeira fase, iremos centrar-nos na seguinte arquitetura:
![]() |
15.3. O projeto Eclipse
O projeto Eclipse é o seguinte:
![]() | ![]() | ![]() |
Esta estrutura segue a do projeto de exemplo do parágrafo 13.6.1. Vamos seguir o mesmo procedimento.
15.4. Configuração do Maven
É a descrita no parágrafo 13.6.2.
15.5. Implementação da camada [DAO]
![]() |
![]() |
- o pacote [elections.client.config] contém a configuração Spring da camada [DAO];
- o pacote [elections .client.dao] contém a implementação da camada [DAO];
- o pacote [elections .client.entities] contém os objetos trocados com o serviço web / jSON;
- o pacote [elections .client.metier] contém a camada [métier]
- o pacote [elections .client.ui] contém a camada [UI]
15.5.1. Configuração da camada [métier]
![]() |
A classe [MetierConfig] realiza a configuração Spring da camada [métier]. O seu código é o seguinte:
package elections.client.config;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Scope;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
@ComponentScan({ "elections.client.dao","elections.client.metier" })
public class MetierConfig {
// constantes
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8080";
@Bean
public RestTemplate restTemplate(int timeout) {
// criação do componente RestTemplate
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// tempo limite das trocas de dados
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// resultado
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
// mapeador jSON
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
Este código foi explicado no parágrafo 13.6.3.1. É mais simples, uma vez que aqui não há filtros jSON para gerir.
15.5.2. As entidades
![]() |
As entidades processadas pelas camadas [DAO] e [métier] são aquelas que estas trocam com o serviço web /jSON. Trata-se dos objetos do tipo [ElectionsConfig] e [ListeElectorale]. Do lado do servidor, estas entidades tinham anotações de persistência JPA. Aqui, essas anotações foram removidas. Reproduzimos o código das entidades a título de recordação:
[AbstractEntity]
package spring.webjson.client.entities;
public abstract class AbstractEntity {
// propriedades
protected Long id;
protected Long version;
// construtores
public AbstractEntity() {
}
public AbstractEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
// redefinição de [equals] e [hashcode]
@Override
public int hashCode() {
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return id != null && this.id == other.id.longValue();
}
// getters e setters
...
}
[ElectionsConfig]
package elections.webjson.client.entities;
public class ElectionsConfig extends AbstractEntity {
// campos
private int nbSiegesAPourvoir;
private double seuilElectoral;
// construtores
public ElectionsConfig() {
}
public ElectionsConfig(int nbSiegesAPourvoir, double seuilElectoral) {
this.nbSiegesAPourvoir = nbSiegesAPourvoir;
this.seuilElectoral = seuilElectoral;
}
// getters e setters
...
}
[ListeElectorale]
package elections.webjson.client.entities;
public class ListeElectorale extends AbstractEntity {
// campos
private String nom;
private int voix;
private int sieges;
private boolean elimine;
// construtores
public ListeElectorale() {
}
public ListeElectorale(String nom, int voix, int sieges, boolean elimine) {
setNom(nom);
setVoix(voix);
setSieges(sieges);
setElimine(elimine);
}
// getters e setters
...
}
15.5.3. A interface da camada [DAO]
![]() |
A camada [DAO] apresenta a seguinte interface [IClientDao]:
package elections.client.dao;
public interface IClientDao {
// consulta genérica
String getResponse(String url, String jsonPost);
}
A interface possui apenas um único método [getResponse]:
- o primeiro parâmetro é o URL do servidor a consultar;
- o segundo parâmetro é o valor jSON do valor a enviar, null se não houver nada para enviar;
- o resultado é a cadeia jSON de um objeto [Response<T>], cuja classe [Response] foi descrita no parágrafo 14.7;
15.5.4. Implementação das trocas com o serviço web / jSON
![]() |
A classe [ClientDao] implementa a interface [IClientDao] da seguinte forma:
package elections.client.dao;
import java.net.URI;
import java.net.URISyntaxException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import elections.client.entities.ElectionsException;
@Component
public class ClientDao implements IClientDao {
// dados
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// solicitação genérica
@Override
public String getResponse(String url, String jsonPost) {
try {
// URL: URL para contacto
// jsonPost: o valor jSON a enviar
// execução da solicitação
RequestEntity<?> request;
if (jsonPost != null) {
// consulta POST
request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
} else {
// solicitação GET
request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
.accept(MediaType.APPLICATION_JSON).build();
}
// executa-se a consulta
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e1) {
throw new ElectionsException(200, e1);
} catch (RuntimeException e2) {
throw new ElectionsException(201, e2);
}
}
}
Este código foi descrito no parágrafo 13.6.3.6.
15.6. Implementação da camada [métier]
![]() |
Como já foi referido, a camada [métier] apresenta a mesma interface [IElectionsMetier] que no parágrafo 8.4:
package elections.client.metier;
import elections.client.entities.ListeElectorale;
public interface IElectionsMetier {
// para obter as listas em competição
public ListeElectorale[] getListesElectorales();
// o número de lugares a preencher
public int getNbSiegesAPourvoir();
// o limiar eleitoral
public double getSeuilElectoral();
// o registo dos resultados
public void recordResultats(ListeElectorale[] listesElectorales);
// cálculo dos lugares
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
}
Esta interface é implementada pela seguinte classe [ElectionsMetier]:
package elections.client.metier;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import elections.client.dao.IClientDao;
import elections.client.entities.ElectionsConfig;
import elections.client.entities.ElectionsException;
import elections.client.entities.ListeElectorale;
@Component
public class ElectionsMetier implements IElectionsMetier {
@Autowired
private IClientDao dao;
@Autowired
private ApplicationContext context;
// configuração da eleição
private ElectionsConfig electionsConfig;
@PostConstruct
public void init() {
// mapadores jSON
ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
try {
// consulta
Response<ElectionsConfig> response = mapperResponse.readValue(dao.getResponse("/getElectionsConfig", null),
new TypeReference<Response<ElectionsConfig>>() {
});
// erro?
if (response.getStatus() != 0) {
// é lançada 1 exceção
throw new ElectionsException(response.getStatus(), response.getMessages());
} else {
electionsConfig = response.getBody();
}
} catch (ElectionsException e1) {
throw e1;
} catch (Exception e2) {
throw new ElectionsException(100, getMessagesForException(e2));
}
}
@Override
public ListeElectorale[] getListesElectorales() {
...
}
@Override
public int getNbSiegesAPourvoir() {
return electionsConfig.getNbSiegesAPourvoir();
}
@Override
public double getSeuilElectoral() {
return electionsConfig.getSeuilElectoral();
}
@Override
public void recordResultats(ListeElectorale[] listesElectorales) {
...
}
@Override
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales) {
...
}
// lista de mensagens de erro de uma exceção
private List<String> getMessagesForException(Exception exception) {
// recupera-se a lista de mensagens de erro da exceção
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
// recupera-se a mensagem apenas se esta for !=null e não estiver vazia
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// causa seguinte
cause = cause.getCause();
}
return erreurs;
}
}
O tipo [Response] utilizado na linha 37 é a resposta do servidor web / jSON descrita no parágrafo 14.7;
Tarefa a realizar: seguindo o parágrafo 13.6.3.7, complete a classe [ElectionsMetier];
15.7. O teste Junit
Voltemos à arquitetura cliente/servidor que estamos a construir:
![]() |
A camada [JUnit] [1] comunica com a camada [Métier] do servidor [5] através das camadas [2-4]. Ao garantir que as camadas [Métier], [2] e [5] tenham a mesma interface, torna-se possível tornar as camadas [2-4] transparentes. A camada [1] parece comunicar diretamente com a camada [5]. O ponto interessante é que, em [1], poderemos utilizar o teste JUnit que tinha sido utilizado para testar a camada [Métier] [5].
![]() |
Tarefa a realizar: execute o teste JUnit do projeto para verificar a sua implementação, bem como a do servidor e do seu cliente.
15.8. Implementação da camada [UI]
Voltemos à arquitetura que pretendemos construir:
![]() |
Agora que a camada [métier] [2] foi construída e testada, podemos construir a camada [ui] [1].
![]() |
Como a interface [IElectionsMetier] da camada [métier] é idêntica à do projeto descrito no parágrafo 8, podemos, no [3], copiar o projeto da camada [ui] do parágrafo 10. Esse projeto era um projeto NetBeans. Basta copiar e colar as classes Java em questão do NetBeans para o Eclipse. Feito isso, é necessário efetuar alguns ajustes nos pacotes e nas importações.
Faremos o mesmo para as classes executáveis dos pacotes [elections.client.boot] e [4].
A classe [AbstractBootElections] é a seguinte:
package elections.client.boot;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import elections.client.config.UiConfig;
import elections.client.entities.ElectionsException;
import elections.client.ui.IElectionsUI;
public abstract class AbstractBootElections {
// recuperação do contexto Spring
protected AnnotationConfigApplicationContext ctx;
public void run() {
// instanciação da camada [ui]
IElectionsUI electionsUI = null;
try {
// recuperação do contexto Spring
ctx = new AnnotationConfigApplicationContext(UiConfig.class);
// recuperação da camada [ui]
electionsUI = getUI();
...
- linha 19: o contexto Spring definido pela classe de configuração [UiConfig] é instanciado. Esta classe é a seguinte:
![]() |
A classe [UiConfig] é a seguinte:
package elections.client.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
@Import(MetierConfig.class)
@ComponentScan(basePackages = { "elections.client.ui" })
public class UiConfig {
}
- linha 6: importam-se os beans da camada [métier];
- linha 7: indica-se que existem beans Spring no pacote [elections.client.ui];
Tarefa a realizar: verifique se as versões console e Swing da camada [ui] funcionam.















