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.















