Skip to content

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.