Skip to content

15. [TD]: creación de un cliente para el servicio web

Palabras clave: arquitectura multicapa, Spring, inyección de dependencias, servicio web / jSON, cliente / servidor

15.1. Support

  

Los proyectos de este capítulo se encuentran en la carpeta [support / chap-15].

15.2. La arquitectura cliente/servidor

Queremos crear la siguiente arquitectura cliente/servidor:

La capa [ui] será la que ya se ha desarrollado en los apartados 9 y 10. Esto será posible porque la capa [métier] anterior implementará la misma interfaz [IElectionsMetier] que la capa [métier] del apartado 8:


package elections.client.metier;

import elections.client.entities.ListeElectorale;

public interface IElectionsMetier {

    // se obtienen las listas en competición
    public ListeElectorale[] getListesElectorales();

    // el número de escaños por cubrir
    public int getNbSiegesAPourvoir();

    // el umbral electoral
    public double getSeuilElectoral();

    // el registro de los resultados
    public void recordResultats(ListeElectorale[] listesElectorales);

    // el cálculo de escaños
    public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);

}

En el apartado 7, la capa [DAO] intercambiaba datos con una SGBD. Aquí, la capa [DAO] intercambia datos con un servidor web / jSON.

En un primer momento, nos centraremos en la siguiente arquitectura:

15.3. El proyecto Eclipse

El proyecto Eclipse es el siguiente:

Esta estructura sigue la del proyecto de ejemplo del apartado 13.6.1. Seguiremos el mismo procedimiento.

15.4. Configuración de Maven

Es la descrita en el apartado 13.6.2.

15.5. Implementación de la capa [DAO]

  
  • el paquete [elections.client.config] contiene la configuración de Spring de la capa [DAO];
  • el paquete [elections .client.dao] contiene la implementación de la capa [DAO];
  • el paquete [elections .client.entities] contiene los objetos intercambiados con el servicio web / jSON;
  • el paquete [elections .client.metier] contiene la capa [métier]
  • el paquete [elections .client.ui] contiene la capa [UI]

15.5.1. Configuración de la capa [métier]

  

La clase [MetierConfig] realiza la configuración de Spring de la capa [métier]. Su código es el siguiente:


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) {
        // creación del componente RestTemplate
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // tiempo de espera de los intercambios
        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 se ha explicado en el apartado 13.6.3.1. Es más sencillo, ya que aquí no hay que gestionar ningún filtro jSON.

15.5.2. Las entidades

  

Las entidades que gestionan las capas [DAO] y [métier] son las que intercambian con el servicio web /jSON. Se trata de los objetos de tipo [ElectionsConfig] y [ListeElectorale]. En el lado del servidor, estas entidades tenían anotaciones de persistencia JPA. Aquí, dichas anotaciones se han eliminado. A modo de recordatorio, volvemos a incluir el código de las entidades:

[AbstractEntity]


package spring.webjson.client.entities;


public abstract class AbstractEntity {
    // propiedades
    protected Long id;
    protected Long version;

    // constructores
    public AbstractEntity() {

    }

    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }

    // redefinición de [equals] y [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 y setters
    ...
}

[ElectionsConfig]


package elections.webjson.client.entities;


public class ElectionsConfig extends AbstractEntity {

    // campos
    private int nbSiegesAPourvoir;
    private double seuilElectoral;

    // constructores
    public ElectionsConfig() {

    }

    public ElectionsConfig(int nbSiegesAPourvoir, double seuilElectoral) {
        this.nbSiegesAPourvoir = nbSiegesAPourvoir;
        this.seuilElectoral = seuilElectoral;
    }

    // getters y 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;

    // constructores
    public ListeElectorale() {
    }

    public ListeElectorale(String nom, int voix, int sieges, boolean elimine) {
        setNom(nom);
        setVoix(voix);
        setSieges(sieges);
        setElimine(elimine);
    }

    // getters y setters
    ...
}

15.5.3. La interfaz de la capa [DAO]

  

La capa [DAO] presenta la siguiente interfaz [IClientDao]:


package elections.client.dao;

public interface IClientDao {

    // consulta genérica
    String getResponse(String url, String jsonPost);

}

La interfaz solo tiene un único método, [getResponse]:

  • el primer parámetro es el URL del servidor al que se va a realizar la consulta;
  • el segundo parámetro es el valor jSON del valor que se va a enviar; si no hay nada que enviar, se utiliza null;
  • el resultado es la cadena jSON de un objeto [Response<T>], cuya clase [Response] se ha descrito en el apartado 14.7;

15.5.4. Implementación de los intercambios con el servicio web / jSON

  

La clase [ClientDao] implementa la interfaz [IClientDao] de la siguiente manera:


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 {

    // datos
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // solicitud genérica
    @Override
    public String getResponse(String url, String jsonPost) {

        try {
            // URL: URL (contacto)
            // jsonPost: el valor jSON que se debe enviar

            // ejecución de la solicitud
            RequestEntity<?> request;
            if (jsonPost != null) {
                // solicitud POST
                request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
            } else {
                // consulta GET
                request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON).build();
            }
            // se ejecuta la 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 se ha descrito en el apartado 13.6.3.6.

15.6. Implementación de la capa [métier]

 

Como se ha indicado, la capa [métier] presenta la misma interfaz [IElectionsMetier] que en el apartado 8.4:


package elections.client.metier;

import elections.client.entities.ListeElectorale;

public interface IElectionsMetier {

    // para obtener las listas de candidatos
    public ListeElectorale[] getListesElectorales();

    // el número de escaños por cubrir
    public int getNbSiegesAPourvoir();

    // el umbral electoral
    public double getSeuilElectoral();

    // el registro de los resultados
    public void recordResultats(ListeElectorale[] listesElectorales);

    // cálculo de escaños
    public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);

}

Esta interfaz está implementada por la siguiente clase [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;

    // configuración de la elección
    private ElectionsConfig electionsConfig;

    @PostConstruct
    public void init() {
        // mapas jSON
        ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
        try {
            // consulta
            Response<ElectionsConfig> response = mapperResponse.readValue(dao.getResponse("/getElectionsConfig", null),
                    new TypeReference<Response<ElectionsConfig>>() {
                    });
            // ¿Error?
            if (response.getStatus() != 0) {
                // se lanza 1 excepción
                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 mensajes de error de una excepción
    private List<String> getMessagesForException(Exception exception) {
        // se recupera la lista de mensajes de error de la excepción
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            // se recupera el mensaje solo si !=null y no está vacío
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // causa siguiente
            cause = cause.getCause();
        }
        return erreurs;
    }
}

El tipo [Response] utilizado en la línea 37 es la respuesta del servidor web / jSON descrita en el apartado 14.7;


Tarea: siguiendo el apartado 13.6.3.7, completa la clase [ElectionsMetier];


15.7. La prueba Junit

Volvamos a la arquitectura cliente/servidor que estamos construyendo:

La capa [JUnit] [1] se comunica con la capa [Métier] del servidor [5] a través de las capas [2-4]. Al garantizar que las capas [Métier], [2] y [5] tengan la misma interfaz, se consigue que las capas [2-4] sean transparentes. La capa [1] da la impresión de comunicarse directamente con la capa [5]. Lo interesante es que en [1] podremos utilizar la prueba JUnit que se había utilizado para probar la capa [Métier] [5].

  

Tarea pendiente: ejecuta la prueba JUnit del proyecto para verificar tu implementación, tanto del servidor como del cliente.


15.8. Implementación de la capa [UI]

Volvamos a la arquitectura que queremos construir:

Ahora que se ha construido y probado la capa [métier] [2], podemos construir la capa [ui] [1].

Dado que la interfaz [IElectionsMetier] de la capa [métier] es idéntica a la del proyecto descrito en el apartado 8, en [3] podemos copiar el proyecto de la capa [ui] del apartado 10. Este proyecto era un proyecto de NetBeans. Basta con copiar y pegar las clases Java correspondientes de NetBeans a Eclipse. Una vez hecho esto, hay que realizar algunos ajustes en los paquetes y las importaciones.

Haremos lo mismo con las clases ejecutables de los paquetes [elections.client.boot] y [4].

La clase [AbstractBootElections] es la siguiente:


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 {

    // Recuperación del contexto de Spring
    protected AnnotationConfigApplicationContext ctx;

    public void run() {
        // instanciación de la capa [ui]
        IElectionsUI electionsUI = null;
        try {
            // Recuperación del contexto de Spring
            ctx = new AnnotationConfigApplicationContext(UiConfig.class);
            // Recuperación de la capa [ui]
            electionsUI = getUI();

...
  • línea 19: se instancia el contexto Spring definido por la clase de configuración [UiConfig]. Esta clase es la siguiente:
  

La clase [UiConfig] es la siguiente:


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 {
}
  • línea 6: se importan los beans de la capa [métier];
  • línea 7: se indica que hay beans de Spring en el paquete [elections.client.ui];

Tarea: comprueba que las versiones de consola y Swing de la capa [ui] funcionan.