2. El servidor Spring 4
![]() |
En la arquitectura anterior, abordamos ahora la construcción del servicio web / JSON, desarrollado con el marco Spring 4. Lo escribiremos en varias etapas:
- primero las capas [métier] y [DAO] (objetos de acceso a datos). Aquí utilizaremos Spring Data;
- luego, el servicio web JSON sin autenticación. Aquí utilizaremos Spring MVC;
- luego agregaremos la parte de autenticación con Spring Security.
Comenzamos por explicar la estructura de la base de datos que sustenta la aplicación.
2.1. La base de datos
![]() |
La base de datos, a la que en adelante nos referiremos como [dbrdvmedecins] , es una base de datos MySQL5 con las siguientes tablas:
![]() |
Las citas se administran mediante las siguientes tablas:
- [medecins]: contiene la lista de médicos del consultorio;
- [clients]: contiene la lista de pacientes de la clínica;
- [creneaux]: contiene los horarios de cada uno de los médicos;
- [rv]: contiene la lista de citas de los médicos.
Las tablas [roles], [users] y [users_roles] son tablas relacionadas con la autenticación. Por ahora, no nos ocuparemos de ellas.
Las relaciones entre las tablas que administran las citas son las siguientes:
![]() |
- un horario pertenece a un médico; un médico tiene 0 o más horarios;
- una cita reúne a un cliente y a un médico a través de un horario de este último;
- un cliente tiene 0 o más citas;
- a un horario se le asocian 0 o más citas (en días diferentes).
2.1.1. La tabla [MEDECINS]
Contiene información sobre los médicos administrados por la aplicación [RdvMedecins].
![]() | ![]() |
- ID: número que identifica al médico —clave primaria de la tabla
- VERSION: número que identifica la versión de la fila en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la fila.
- NOM: el apellido del médico
- PRENOM: su nombre
- TITRE: su título (Srta., Sra., Sr.)
2.1.2. La tabla [CLIENTS]
Los clientes de los distintos médicos se registran en la tabla [CLIENTS]:
![]() | ![]() |
- ID: número que identifica al cliente —clave primaria de la tabla
- VERSION: número que identifica la versión de la fila en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la fila.
- NOM: el nombre del cliente
- PRENOM: su nombre
- TITRE: su tratamiento (Srta., Sra., Sr.)
2.1.3. La tabla [CRENEAUX]
Enumera los horarios en los que son posibles los RV:
![]() |
![]() |
- ID: número que identifica el horario —clave primaria de la tabla (línea 8)
- VERSION: número que identifica la versión de la fila en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la fila.
- ID_MEDECIN: número que identifica al médico al que pertenece este horario – clave externa en la columna MEDECINS (ID).
- HDEBUT: hora de inicio del horario
- MDEBUT: minutos de inicio del horario
- HFIN: hora de finalización del horario
- MFIN: minutos de fin del intervalo
La segunda fila de la tabla [CRENEAUX] (véase [1] más arriba) indica, por ejemplo, que el horario n.º 2 comienza a las 8:20 y termina a las 8:40, y pertenece a la doctora n.º 1 (la Sra. Marie PELISSIER).
2.1.4. La tabla [RV]
Enumera los RV asignados a cada médico:
![]() |
- ID: número que identifica de manera única al RV – clave primaria
- JOUR: día del RV
- ID_CRENEAU: horario de la cita del RV —clave externa en el campo [ID] de la tabla [CRENEAUX]—; determina tanto el horario de la cita como el médico correspondiente.
- ID_CLIENT: número del cliente para quien se realiza la reservación – clave externa en el campo [ID] de la tabla [CLIENTS]
Esta tabla tiene una restricción de unicidad en la e sobre los valores de las columnas unidas (JOUR, ID_CRENEAU):
Si una fila de la tabla [RV] tiene el valor (JOUR1, ID_CRENEAU1) para las columnas (JOUR, ID_CRENEAU), este valor no puede aparecer en ningún otro lugar. De lo contrario, significaría que se registraron dos RV al mismo tiempo para el mismo médico. Desde el punto de vista de la programación en Java, el controlador JDBC de la base de datos inicia un SQLException cuando ocurre este caso.
La línea de id igual a 3 (véase [1] más arriba) significa que se reservó un RV para el horario n.º 20 y el cliente n.º 4 el 23/08/2006. La tabla [CRENEAUX] nos indica que el horario n.º 20 corresponde al intervalo de 16:20 a 16:40 y pertenece a la doctora n.º 1 (la Sra. Marie PELISSIER). La tabla [CLIENTS] nos indica que el cliente n.º 4 es la Srta. Brigitte BISTROU.
2.2. Introducción a Spring Data
Vamos a implementar la capa [DAO] del proyecto con Spring Data, una rama del ecosistema Spring.
![]() |
En el sitio web de Spring hay numerosos tutoriales para dar los primeros pasos con Spring [http://spring.io/guides]. Utilizaremos uno de ellos para introducir Spring Data. Para ello, utilizaremos Spring Tool Suite (STS).
![]() |
- en [1], importamos uno de los tutoriales de [spring.io/guides];
![]() |
- en [2], elegimos el tutorial [Accessing Data Jpa] que muestra cómo acceder a una base de datos con Spring Data;
- en [3], elegimos un proyecto configurado por Maven;
- en [4], el tutorial puede obtenerse en dos formatos: [initial], que es una versión vacía que se completa siguiendo el tutorial, o [complete], que es la versión final del tutorial. Elegimos esta última;
- en [5], podemos elegir visualizar el tutorial en un navegador;
- en [6], el proyecto final.
2.2.1. La configuración de Maven del proyecto
Las dependencias de Maven del proyecto se configuran en el archivo [pom.xml]:
<groupId>org.springframework</groupId>
<artifactId>gs-accessing-data-jpa</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
<properties>
<!-- usa UTF-8 para todo -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>hello.Application</start-class>
</properties>
- líneas 5-9: definen un proyecto padre de Maven. Este es el que define la mayor parte de las dependencias del proyecto. Pueden ser suficientes, en cuyo caso no se agregan más, o no, en cuyo caso se agregan las dependencias que faltan;
- líneas 12-15: definen una dependencia de [spring-boot-starter-data-jpa]. Este artefacto contiene las clases de Spring Data;
- líneas 16-19: definen una dependencia de SGBD y H2, que permiten crear y administrar bases de datos en memoria.
Veamos las clases que aportan estas dependencias:
![]() | ![]() | ![]() |
Son muchas:
- algunas pertenecen al ecosistema de Spring (las que comienzan con «spring»);
- otras pertenecen al ecosistema de Hibernate (hibernate, jboss), de las cuales aquí utilizamos la implementación JPA;
- otras son bibliotecas de pruebas (junit, hamcrest);
- otras son bibliotecas de registro (log4j, logback, slf4j);
Las vamos a conservar todas. Para una aplicación en producción, solo se deberían conservar las que sean necesarias.
En la línea 26 del archivo [pom.xml] se encuentra la línea:
<start-class>hello.Application</start-class>
Esta línea está relacionada con las siguientes:
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
En las líneas 6 a 9, el complemento [spring-boot-maven-plugin] permite generar el archivo JAR ejecutable de la aplicación. La línea 26 del archivo [pom.xml] indica, entonces, la clase ejecutable de este archivo JAR.
2.2.2. La capa [JPA]
El acceso a la base de datos se realiza a través de una capa [JPA], Java Persistence API:
![]() |
![]() |
La aplicación es básica y administra clientes [Customer]. La clase [Customer] forma parte de la capa [JPA] y es la siguiente:
package hello;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String firstName;
private String lastName;
protected Customer() {
}
public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
}
}
Un cliente tiene un identificador [id], un nombre [firstName] y un apellido [lastName]. Cada instancia [Customer] representa una fila de una tabla de la base de datos.
- línea 8: anotación JPA que hace que la persistencia de las instancias [Customer] (Crear, Leer, Actualizar, Eliminar) sea gestionada por una implementación JPA. Según las dependencias de Maven, se observa que se utiliza la implementación JPA / Hibernate;
- líneas 11-12: anotaciones JPA que asocian el campo [id] con la clave primaria de la tabla [Customer]. La línea 12 indica que la implementación JPA utilizará el método de generación de clave primaria propio de la implementación SGBD utilizada, en este caso H2;
No hay otras anotaciones para JPA. Por lo tanto, se utilizarán los valores por defecto:
- la tabla de [Customer] llevará el nombre de la clase, es decir, [Customer];
- las columnas de esta tabla llevarán el nombre de los campos de la clase: [id, firstName, lastName], teniendo en cuenta que no se distingue entre mayúsculas y minúsculas en el nombre de una columna de tabla;
Cabe señalar que en ningún momento se menciona el nombre de la implementación JPA utilizada.
2.2.3. La capa [DAO]
![]() |
![]() |
La clase [CustomerRepository] implementa la capa [DAO]. Su código es el siguiente:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
Por lo tanto, se trata de una interfaz y no de una clase (línea 7). Extiende la interfaz [CrudRepository], una interfaz de Spring Data (línea 5). Esta interfaz está definida por dos tipos: el primero es el tipo de los elementos gestionados, en este caso el tipo [Customer]; el segundo es el tipo de la clave primaria de los elementos gestionados, en este caso un tipo [Long]. La interfaz [CrudRepository] es la siguiente:
package org.springframework.data.repository;
import java.io.Serializable;
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> save(Iterable<S> entities);
T findOne(ID id);
boolean exists(ID id);
Iterable<T> findAll();
Iterable<T> findAll(Iterable<ID> ids);
long count();
void delete(ID id);
void delete(T entity);
void delete(Iterable<? extends T> entities);
void deleteAll();
}
Esta interfaz define las operaciones CRUD (Crear – Leer – Actualizar – Eliminar) que se pueden realizar sobre un tipo JPA T:
- línea 8: el método
savepermite persistir una entidadTen la base de datos. Persiste la entidad con la clave primaria que le asignó el métodoSGBD. También permite actualizar una entidadTidentificada por su clave primariaid. La elección de una u otra acción depende del valor de la clave primaria id: si este es null, se lleva a cabo la operación de persistencia; de lo contrario, se realiza la operación de actualización; - línea 10: lo mismo, pero para una lista de entidades;
- línea 12: el método findOne permite recuperar una entidad T identificada por su clave primaria id;
- línea 22: el método delete permite eliminar una entidad T identificada por su clave primaria id;
- líneas 24-28: variantes del método [delete];
- línea 16: el método [findAll] permite recuperar todas las entidades T persistentes;
- línea 18: lo mismo, pero limitado a las entidades para las que se ha pasado la lista de identificadores;
Volvamos a la interfaz [CustomerRepository]:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
- la línea 9 permite recuperar un [Customer] por su nombre [lastName];
Y eso es todo para la capa [DAO]. No existe una clase de implementación de la interfaz anterior. Esta se genera en tiempo de ejecución mediante [Spring Data]. Los métodos de la interfaz [CrudRepository] se implementan automáticamente. En cuanto a los métodos agregados a la interfaz [CustomerRepository], depende. Volvamos a la definición de [Customer]:
private long id;
private String firstName;
private String lastName;
El método de la línea 9 es implementado automáticamente por [Spring Data] porque hace referencia al campo [lastName] (línea 3) de [Customer]. Cuando encuentra un método [findBySomething] en la interfaz que se debe implementar, Spring Data lo implementa mediante la siguiente consulta JPQL (Java Persistence Query Language):
Por lo tanto, el tipo T debe tener un campo llamado [something]. De este modo, el método
se implementará mediante un código similar al siguiente:
return [em].createQuery("select c from Customer c where c.lastName=:value").setParameter("value",lastName).getResultList()
donde [em] hace referencia al contexto de persistencia JPA. Esto solo es posible si la clase [Customer] tiene un campo llamado [lastName], lo cual es el caso.
En conclusión, en los casos sencillos, Spring Data nos permite implementar la capa [DAO] con una simple interfaz.
2.2.4. La capa [console]
![]() |
![]() |
La clase [Application] es la siguiente:
package hello;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
// guardar un par de clientes
repository.save(new Customer("Jack", "Bauer"));
repository.save(new Customer("Chloe", "O'Brian"));
repository.save(new Customer("Kim", "Bauer"));
repository.save(new Customer("David", "Palmer"));
repository.save(new Customer("Michelle", "Dessler"));
// recupere todos los clientes
Iterable<Customer> customers = repository.findAll();
System.out.println("Customers found with findAll():");
System.out.println("-------------------------------");
for (Customer customer : customers) {
System.out.println(customer);
}
System.out.println();
// obtener un cliente específico mediante ID
Customer customer = repository.findOne(1L);
System.out.println("Customer found with findOne(1L):");
System.out.println("--------------------------------");
System.out.println(customer);
System.out.println();
// recuperar clientes por apellido
List<Customer> bauers = repository.findByLastName("Bauer");
System.out.println("Customer found with findByLastName('Bauer'):");
System.out.println("--------------------------------------------");
for (Customer bauer : bauers) {
System.out.println(bauer);
}
context.close();
}
}
- La línea 10: indica que la clase se utiliza para configurar Spring. De hecho, las versiones recientes de Spring pueden configurarse en Java en lugar de en XML. Ambos métodos pueden utilizarse simultáneamente. En el código de una clase que tiene la anotación [Configuration] normalmente se encuentran beans de Spring, es decir, definiciones de clases que se deben instanciar. Aquí no se define ningún bean. Cabe recordar que, cuando se trabaja con un SGBD, se deben definir varios beans de Spring:
- un [EntityManagerFactory] que define la implementación JPA que se utilizará,
- un [DataSource] que define la fuente de datos que se utilizará,
- un [TransactionManager] que define el administrador de transacciones que se utilizará;
Aquí no se define ninguno de estos beans.
- En la línea 11: la anotación [EnableAutoConfiguration] es una anotación proveniente del proyecto [Spring Boot] (líneas 5-6). Esta anotación le pide a Spring Boot, a través de la clase [SpringApplication] (línea 16), que configure la aplicación según las bibliotecas que se encuentren en su Classpath. Como las bibliotecas de Hibernate están en el Classpath, el bean [entityManagerFactory] se implementará con Hibernate. Dado que la biblioteca SGBD H2 se encuentra en el Classpath, el bean [dataSource] se implementará con H2. En el bean [dataSource], también hay que definir el usuario y su contraseña. En este caso, Spring Boot utilizará el administrador predeterminado de H2, que no tiene contraseña. Debido a que la biblioteca [spring-tx] se encuentra en el Classpath, se utilizará el administrador de transacciones de Spring.
Además, se escaneará la carpeta en la que se encuentra la clase [Application] en busca de beans reconocidos implícitamente por Spring o definidos explícitamente mediante anotaciones de Spring. De este modo, se inspeccionarán las clases [Customer] y [CustomerRepository]. Dado que la primera tiene la anotación [@Entity], se catalogará como una entidad que debe gestionar Hibernate. Dado que la segunda extiende la interfaz [CrudRepository], se registrará como un bean de Spring.
Analicemos las líneas 16 y 17 del código:
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
- línea 1: se ejecuta el método estático [run] de la clase [SpringApplication] del proyecto Spring Boot. Su parámetro es la clase que tiene una anotación [Configuration] o [EnableAutoConfiguration]. A continuación, se llevará a cabo todo lo explicado anteriormente. El resultado es un contexto de aplicación de Spring, es decir, un conjunto de beans administrados por Spring;
- línea 17: se solicita a este contexto de Spring un bean que implemente la interfaz [CustomerRepository]. Aquí recuperamos la clase generada por Spring Data para implementar esta interfaz.
Las operaciones siguientes solo utilizan los métodos del bean que implementa la interfaz [CustomerRepository]. Cabe destacar, en la línea 50, que se cierra el contexto. Los resultados en la consola son los siguientes:
- líneas 1-8: el logotipo del proyecto Spring Boot;
- línea 9: se ejecuta la clase [hello.Application];
- línea 10: [AnnotationConfigApplicationContext] es una clase que implementa la interfaz [ApplicationContext] de Spring. Es un contenedor de beans;
- línea 11: el bean [entityManagerFactory] se implementa con la clase [LocalContainerEntityManagerFactory], una clase de Spring;
- línea 12: aparece [hibernate]. Se ha elegido esta implementación, JPA;
- línea 19: un dialecto de Hibernate es la variante SQL que se utilizará con SGBD. Aquí, el dialecto [H2Dialect] indica que Hibernate trabajará con SGBD y H2;
- líneas 22-24: se crea la tabla [CUSTOMER]. Esto significa que Hibernate se ha configurado para generar las tablas a partir de las definiciones JPA; en este caso, la definición JPA de la clase [Customer];
- líneas 27-32: registros de Hibernate que muestran las inserciones de filas en la tabla [CUSTOMER]. Esto significa que Hibernate se ha configurado para generar registros;
- líneas 35-39: los cinco clientes insertados;
- líneas 42-44: resultado del método [findOne] de la interfaz;
- líneas 47-50: resultados del método [findByLastName];
- líneas 51 y siguientes: registros del cierre del contexto de Spring.
2.2.5. Configuración manual del proyecto Spring Data
Duplicamos el proyecto anterior en el proyecto [gs-accessing-data-jpa-2]:
![]() |
En este nuevo proyecto, no vamos a recurrir a la configuración automática que realiza Spring Boot. La haremos manualmente. Esto puede ser útil si las configuraciones predeterminadas no nos convienen.
En primer lugar, vamos a especificar las dependencias necesarias en el archivo [pom.xml]:
<dependencies>
<!-- Spring Core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<!-- Transacciones de Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.5.2.RELEASE</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>1.0.2.RELEASE</version>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.4.Final</version>
</dependency>
<!-- Base de datos H2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.178</version>
</dependency>
<!-- Commons DBCP -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
<version>1.6</version>
</dependency>
</dependencies>
- líneas 3-17: las bibliotecas básicas de Spring;
- líneas 19-28: las bibliotecas de Spring para gestionar transacciones con una base de datos;
- líneas 30-34: Spring Data, que se utiliza para acceder a la base de datos;
- líneas 36-40: Spring Boot para iniciar la aplicación;
- líneas 48-52: el SGBD H2;
- líneas 54-63: las bases de datos suelen utilizarse con grupos de conexiones abiertas que evitan tener que abrir y cerrar conexiones repetidamente. Aquí, la implementación utilizada es la de [commons-dbcp];
También en [pom.xml], se modifica el nombre de la clase ejecutable:
<properties>
...
<start-class>demo.console.Main</start-class>
</properties>
En el nuevo proyecto, la entidad [Customer] y la interfaz [CustomerRepository] no cambian. Vamos a modificar la clase [Application], que se dividirá en dos clases:
- [Config], que será la clase de configuración:
- [Main], que será la clase ejecutable;
![]() |
La clase ejecutable [Main] es la misma que la anterior, pero sin las anotaciones de configuración:
package demo.console;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
...
context.close();
}
}
- línea 12: la clase [Main] ya no tiene anotaciones de configuración;
- línea 16: la aplicación se inicia con Spring Boot. El parámetro [Config.class] es la nueva clase de configuración del proyecto;
La clase [Config] que configura el proyecto es la siguiente:
package demo.config;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
//@ComponentScan(basePackages = { "demo" })
//@EntityScan(basePackages = { "demo.entities" })
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "demo.repositories" })
@Configuration
public class Config {
// la fuente de datos H2
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:./demo");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
// el proveedor JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(true);
hibernateJpaVendorAdapter.setDatabase(Database.H2);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan("demo.entities");
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// Administrador de transacciones
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- línea 22: la anotación [@Configuration] convierte a la clase [Config] en una clase de configuración de Spring;
- línea 21: la anotación [@EnableJpaRepositories] permite designar las carpetas donde se encuentran las interfaces de Spring Data [CrudRepository]. Estas interfaces se convertirán en componentes de Spring y estarán disponibles en su contexto;
- línea 20: la anotación [@EnableTransactionManagement] indica que los métodos de las interfaces [CrudRepository] deben ejecutarse dentro de una transacción;
- línea 19: la anotación [@EntityScan] permite especificar las carpetas en las que se deben buscar las entidades JPA. Aquí se ha puesto en comentarios, porque esta información ya se proporcionó explícitamente en la línea 50. Esta anotación debería estar presente si se utiliza el modo [@EnableAutoConfiguration] y las entidades JPA no se encuentran en la misma carpeta que la clase de configuración;
- línea 18: la anotación [@ComponentScan] permite enumerar las carpetas en las que se deben buscar los componentes de Spring. Los componentes de Spring son clases etiquetadas con anotaciones de Spring como @Service, @Component, @Controller, etc. Aquí no hay otros más que los definidos dentro de la clase [Config], por lo que la anotación se ha puesto entre comentarios;
- líneas 25-33: definen la fuente de datos, la base de datos H2. Es la anotación @Bean de la línea 25 la que convierte al objeto creado por este método en un componente administrado por Spring. El nombre del método puede ser cualquiera en este caso. Sin embargo, debe llamarse [dataSource] si el EntityManagerFactory de la línea 47 no existe y se define mediante autoconfiguración;
- línea 29: la base de datos se llamará [demo] y se generará en la carpeta del proyecto;
- líneas 36-43: definen la implementación JPA utilizada, en este caso una implementación de Hibernate. El nombre del método puede ser cualquiera;
- línea 39: no hay registros de SQL;
- línea 30: la base de datos se creará si no existe;
- líneas 46-54: definen el EntityManagerFactory que se encargará de la persistencia de JPA. El método debe llamarse obligatoriamente [entityManagerFactory];
- línea 47: el método recibe dos parámetros del tipo de los dos beans definidos anteriormente. Estos serán creados y luego inyectados por Spring como parámetros del método;
- línea 49: establece la implementación JPA que se utilizará;
- línea 50: establece las carpetas donde se encuentran las entidades JPA;
- línea 51: establece la fuente de datos que se va a gestionar;
- líneas 57-62: el administrador de transacciones. El método debe llamarse obligatoriamente [transactionManager]. Recibe como parámetro el bean de las líneas 46-54;
- línea 60: el administrador de transacciones se asocia con EntityManagerFactory;
Los métodos anteriores se pueden definir en cualquier orden.
La ejecución del proyecto arroja los mismos resultados. Aparece un nuevo archivo en la carpeta del proyecto, el de la base de datos H2:
![]() |
Por último, se puede prescindir de Spring Boot. Se crea una segunda clase ejecutable, [Main2]:
![]() |
La clase [Main2] tiene el siguiente código:
package demo.console;
import java.util.List;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main2 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
....
context.close();
}
}
- línea 15: la clase de configuración [Config] ahora es utilizada por la clase Spring [AnnotationConfigApplicationContext]. En la línea 5 se puede ver que ya no hay dependencia de Spring Boot.
La ejecución arroja los mismos resultados que antes.
2.2.6. Creación de un archivo ejecutable
Para crear un archivo ejecutable del proyecto, se puede proceder de la siguiente manera:
![]() |
- en [1]: se crea una configuración de ejecución;
- en [2]: del tipo [Java Application]
- en [3]: indica el proyecto que se ejecutará (utilice el botón Browse);
- en [4]: indica la clase que se ejecutará;
- en [5]: el nombre de la configuración de ejecución; puede ser cualquiera;
![]() |
- en [6]: se exporta el proyecto;
- en [7]: en forma de un archivo ejecutable JAR;
- en [8]: indica la ruta y el nombre del archivo ejecutable que se creará;
- en [9]: el nombre de la configuración de ejecución creada en [5];
Una vez hecho esto, abrimos una consola en la carpeta que contiene el archivo ejecutable:
El archivo se ejecuta de la siguiente manera:
.....\dist>java -jar gs-accessing-data-jpa-2.jar
Los resultados obtenidos en la consola son los siguientes:
2.2.7. Crear un nuevo proyecto de Spring Data
Para crear un esqueleto de proyecto de Spring Data, se puede proceder de la siguiente manera:
![]() |
- en [1], se crea un nuevo proyecto;
- en [2]: de tipo [Spring Starter Project];
- el proyecto generado será un proyecto Maven. En [3], se indica el nombre del grupo del proyecto;
- en [4]: se indica el nombre del artefacto (en este caso, un archivo .jar) que se creará al compilar el proyecto;
- en [5]: se indica el paquete de la clase ejecutable que se creará en el proyecto;
- en [6]: el nombre del proyecto en Eclipse; puede ser cualquiera (no tiene que ser idéntico a [4]);
- en [7]: se indica que se va a crear un proyecto con una capa [JPA]. Las dependencias necesarias para dicho proyecto se incluirán entonces en el archivo [pom.xml];
![]() |
- en [8]: el proyecto creado;
El archivo [pom.xml] incluye las dependencias necesarias para un proyecto JPA:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
<relativePath/> <!-- buscar el elemento principal en el repositorio -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- líneas 9-12: las dependencias necesarias para JPA —incluirán [Spring Data]—;
- líneas 13-17: las dependencias necesarias para las pruebas JUnit integradas con Spring;
La clase ejecutable [Application] no hace nada, pero está preconfigurada:
package istia.st;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
La clase de pruebas [ApplicationTests] no hace nada, pero está preconfigurada:
package istia.st;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
@Test
public void contextLoads() {
}
}
- línea 9: la anotación [@SpringApplicationConfiguration] permite utilizar el archivo de configuración [Application]. De esta manera, la clase de prueba se beneficiará de todos los beans que se definan en ese archivo;
- línea 8: la anotación [@RunWith] permite la integración de Spring con JUnit: la clase podrá ejecutarse como una prueba JUnit. [@RunWith] es una anotación JUnit (línea 4), mientras que la clase [SpringJUnit4ClassRunner] es una clase de Spring (línea 6);
Ahora que contamos con un esqueleto de aplicación JPA, podemos completarlo para desarrollar el proyecto de la capa de persistencia del servidor de nuestra aplicación de gestión de citas.
2.3. El proyecto de Eclipse del servidor
![]() |
![]() |
Los elementos principales del proyecto son los siguientes:
- [pom.xml]: archivo de configuración de Maven del proyecto;
- [rdvmedecins.entities]: las entidades JPA;
- [rdvmedecins.repositories]: las interfaces de Spring Data para acceder a las entidades JPA;
- [rdvmedecins.metier]: la capa [métier];
- [rdvmedecins.domain]: las entidades manipuladas por la capa [métier];
- [rdvmdecins.config]: las clases de configuración de la capa de persistencia;
- [rdvmedecins.boot]: una aplicación de consola básica;
2.4. La configuración de Maven
![]() | ![]() | ![]() |
El archivo [pom.xml] del proyecto es el siguiente:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
</dependencies>
<properties>
<!-- usa UTF-8 para todo -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>istia.st.spring.data.main.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>org.jboss.repository.releases</id>
<name>JBoss Maven Release Repository</name>
<url>https://repository.jboss.org/nexus/content/repositories/releases</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
- líneas 8-12: el proyecto se basa en el proyecto padre [spring-boot-starter-parent]. Para las dependencias que ya están presentes en el proyecto padre, no se especifica ninguna versión. Se utilizará la versión definida en el proyecto padre. Las demás dependencias se declaran de manera normal;
- líneas 14-17: para Spring Data;
- líneas 18-22: para las pruebas JUnit;
- líneas 23-26: controlador JDBC de SGBD MySQL5;
- líneas 27-34: grupo de conexiones Commons DBCP;
- líneas 35-38: biblioteca Jackson de gestión de JSON;
- líneas 39-43: biblioteca de Google para la gestión de colecciones;
La versión 1.1.0.RC1 de [spring-boot-starter-parent] utiliza las siguientes versiones de las bibliotecas:
2.5. Las entidades JPA
![]() |
Las entidades JPA son los objetos que encapsularán las filas de las tablas de la base de datos.
![]() |
La clase [AbstractEntity] es la clase padre de las entidades [Personne, Creneau, Rv]. Su definición es la siguiente:
package rdvmedecins.entities;
import java.io.Serializable;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
@MappedSuperclass
public class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
protected Long id;
@Version
protected Long version;
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
// inicialización
public AbstractEntity build(Long id, Long version) {
this.id = id;
this.version = version;
return this;
}
@Override
public boolean equals(Object entity) {
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id == other.id;
}
// getters y setters
..
}
- línea 11: la anotación [@MappedSuperclass] indica que la clase anotada es la clase padre de las entidades JPA y [@Entity];
- líneas 15-17: definen la clave primaria [id] de cada entidad. Es la anotación [@Id] la que convierte el campo [id] en una clave primaria. La anotación [@GeneratedValue(strategy = GenerationType.AUTO)] indica que el valor de esta clave primaria es generado por SGBD y que no se impone ningún modo de generación;
- líneas 18-19: definen la versión de cada entidad. La implementación JPA incrementará este número de versión cada vez que se modifique la entidad. Este número sirve para evitar que dos usuarios diferentes actualicen la entidad al mismo tiempo: dos usuarios, U1 y U2, leen la entidad E con un número de versión igual a V1. U1 modifica E y guarda esta modificación en la base de datos: el número de versión pasa entonces a V1+1. U2 modifica a su vez E y guarda esta modificación en la base de datos: recibirá una excepción porque tiene una versión (V1) diferente a la que está en la base de datos (V1+1);
- líneas 29-33: el método [build] permite inicializar los dos campos de [AbstractEntity]. Este método inicializa la referencia de la instancia [AbstractEntity];
- líneas 36-44: se redefine el método [equals] de la clase: dos entidades se considerarán iguales si tienen el mismo nombre de clase y el mismo identificador id;
La entidad [Personne] es la clase padre de las entidades [Medecin] y [Client]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
@MappedSuperclass
public class Personne extends AbstractEntity {
private static final long serialVersionUID = 1L;
// atributos de una persona
@Column(length = 5)
private String titre;
@Column(length = 20)
private String nom;
@Column(length = 20)
private String prenom;
// constructor por defecto
public Personne() {
}
// constructor con parámetros
public Personne(String titre, String nom, String prenom) {
this.titre = titre;
this.nom = nom;
this.prenom = prenom;
}
// toString
public String toString() {
return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
}
// getters y setters
...
}
- línea 6: la anotación [@MappedSuperclass] indica que la clase anotada es la clase padre de las entidades JPA y [@Entity];
- líneas 10-15: una persona tiene un título (Melle), un nombre (Jacqueline) y un apellido (Tatou). No se proporciona información sobre las columnas de la tabla. Por lo tanto, por defecto tendrán los mismos nombres que los campos;
La entidad [Medecin] es la siguiente:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "medecins")
public class Medecin extends Personne {
private static final long serialVersionUID = 1L;
// constructor por defecto
public Medecin() {
}
// constructor con parámetros
public Medecin(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
public String toString() {
return String.format("Medecin[%s]", super.toString());
}
}
- línea 6: la clase es una entidad JPA;
- línea 7: asociada a la tabla [MEDECINS] de la base de datos;
- línea 8: la entidad [Medecin] deriva de la entidad [Personne];
Un médico se puede inicializar de la siguiente manera:
Si además se desea asignarle un identificador y una versión, se puede escribir:
donde el método [build] es el definido en [AbstractEntity].
La entidad [Client] es la siguiente:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "clients")
public class Client extends Personne {
private static final long serialVersionUID = 1L;
// constructor por defecto
public Client() {
}
// constructor con parámetros
public Client(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
// identidad
public String toString() {
return String.format("Client[%s]", super.toString());
}
}
- línea 6: la clase es una entidad JPA;
- línea 7: asociada a la tabla [CLIENTS] de la base de datos;
- línea 8: la entidad [Client] deriva de la entidad [Personne];
La entidad [Creneau] es la siguiente:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// características de un horario de RV
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// un horario está vinculado a un médico
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// clave externa
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
// creador por defecto
public Creneau() {
}
// constructor con parámetros
public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
this.medecin = medecin;
this.hdebut = hdebut;
this.mdebut = mdebut;
this.hfin = hfin;
this.mfin = mfin;
}
// toString
public String toString() {
return String.format("Créneau[%d, %d, %d, %d:%d, %d:%d]", id, version, idMedecin, hdebut, mdebut, hfin, mfin);
}
// clave externa
public long getIdMedecin() {
return idMedecin;
}
// setters - getters
...
}
- línea 10: la clase es una entidad JPA;
- línea 11: asociada a la tabla [CRENEAUX] de la base de datos;
- línea 12: la entidad [Creneau] deriva de la entidad [AbstractEntity] y, por lo tanto, hereda el identificador [id] y la versión [version];
- línea 16: hora de inicio de la franja horaria (14);
- línea 17: minutos de inicio del intervalo (20);
- línea 18: hora de finalización del intervalo (14);
- línea 19: minutos de finalización de la franja horaria (40);
- líneas 22-24: el médico titular de la franja horaria. La tabla [CRENEAUX] tiene una clave externa en la tabla [MEDECINS]. Esta relación se materializa en las líneas 22-24;
- línea 22: la anotación [@ManyToOne] indica una relación de varios (turnos) a uno (médico). El atributo [fetch=FetchType.LAZY] indica que, cuando se solicita una entidad [Creneau] al contexto de persistencia y esta debe buscarse en la base de datos, la entidad [Medecin] no se devuelve junto con ella. La ventaja de este modo es que la entidad [Medecin] solo se busca si el desarrollador lo solicita. De esta manera, se ahorra memoria y se mejora el rendimiento;
- línea 23: indica el nombre de la columna de clave externa en la tabla [CRENEAUX];
- líneas 27-28: la clave externa en la tabla [MEDECINS];
- línea 27: la columna [ID_MEDECIN] ya se utilizó en la línea 23. Esto significa que puede modificarse de dos formas diferentes, lo cual no permite la norma JPA. Por lo tanto, se agregan los atributos [insertable = false, updatable = false], lo que hace que la columna solo se pueda leer;
La entidad [Rv] es la siguiente:
package rdvmedecins.entities;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// características de un Rv
@Temporal(TemporalType.DATE)
private Date jour;
// un rv está vinculado a un cliente
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// un RV está vinculado a un horario
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// claves externas
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
// fabricante predeterminado
public Rv() {
}
// con parámetros
public Rv(Date jour, Client client, Creneau creneau) {
this.jour = jour;
this.client = client;
this.creneau = creneau;
}
// toString
public String toString() {
return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
}
// claves externas
public long getIdCreneau() {
return idCreneau;
}
public long getIdClient() {
return idClient;
}
// getters y setters
...
}
- línea 14: la clase es una entidad JPA;
- línea 15: asociada a la tabla [RV] de la base de datos;
- línea 16: la entidad [Rv] deriva de la entidad [AbstractEntity] y, por lo tanto, hereda el identificador [id] y la versión [version];
- línea 21: la fecha de la cita;
- línea 20: el tipo [Date] de Java contiene tanto una fecha como una hora. Aquí se especifica que solo se utiliza la fecha;
- líneas 24-26: el cliente para el cual se programó esta cita. La tabla [RV] tiene una clave externa en la tabla [CLIENTS]. Esta relación se refleja en las líneas 24-26;
- líneas 29-31: el horario de la cita. La tabla [RV] tiene una clave externa en la tabla [CRENEAUX]. Esta relación se refleja en las líneas 29-31;
- líneas 34-35: la clave externa [idClient];
- líneas 36-37: la clave externa [idCreneau];
2.6. La capa [DAO]
![]() |
Vamos a implementar la capa [DAO] con Spring Data:
![]() |
La capa [DAO] se implementa con cuatro interfaces de Spring Data:
- [ClientRepository]: brinda acceso a las entidades JPA y [Client];
- [CreneauRepository]: da acceso a las entidades JPA y [Creneau];
- [MedecinRepository]: da acceso a las entidades JPA y [Medecin];
- [RvRepository]: da acceso a las entidades JPA y [Rv];
La interfaz [MedecinRepository] es la siguiente:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Medecin;
public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
- línea 7: la interfaz [MedecinRepository] simplemente hereda los métodos de la interfaz [CrudRepository] sin agregar otros;
La interfaz [ClientRepository] es la siguiente:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Client;
public interface ClientRepository extends CrudRepository<Client, Long> {
}
- línea 7: la interfaz [ClientRepository] simplemente hereda los métodos de la interfaz [CrudRepository] sin agregar otros;
La interfaz [CreneauRepository] es la siguiente:
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Creneau;
public interface CreneauRepository extends CrudRepository<Creneau, Long> {
// lista de horarios de un médico
@Query("select c from Creneau c where c.medecin.id=?1")
Iterable<Creneau> getAllCreneaux(long idMedecin);
}
- línea 8: la interfaz [CreneauRepository] hereda los métodos de la interfaz [CrudRepository];
- líneas 10-11: el método [getAllCreneaux] permite obtener los horarios disponibles de un médico;
- línea 11: el parámetro es el identificador del médico. El resultado es una lista de horarios en forma de un objeto [Iterable<Creneau>];
- línea 10: la anotación [@Query] permite especificar la consulta JPQL (Java Persistence Query Language) que implementa el método. El parámetro [?1] será reemplazado por el parámetro [idMedecin] del método;
La interfaz [RvRepository] es la siguiente:
package rdvmedecins.repositories;
import java.util.Date;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Rv;
public interface RvRepository extends CrudRepository<Rv, Long> {
@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
}
- línea 10: la interfaz [RvRepository] hereda los métodos de la interfaz [CrudRepository];
- líneas 12-13: el método [getRvMedecinJour] permite obtener las citas de un médico para un día determinado;
- línea 13: los parámetros son el identificador del médico y el día. El resultado es una lista de citas en forma de un objeto [Iterable<Rv>];
- línea 12: la anotación [@Query] permite especificar la consulta JPQL que implementa el método. El parámetro [?1] será reemplazado por el parámetro [idMedecin] del método, y el parámetro [?2] será reemplazado por el parámetro [jour] del método. No basta con la siguiente consulta JPQL:
ya que los campos de la clase Rv, de tipos [Client] y [Creneau], se obtienen en modo [FetchType.LAZY], lo que significa que deben solicitarse explícitamente para poder obtenerlos. Esto se hace en la consulta JPQL con la sintaxis [left join fetch entité], que solicita que se realice una unión con la tabla a la que apunta la clave externa para recuperar la entidad a la que se hace referencia;
2.7. La capa [métier]
![]() |
![]() |
- [IMetier] es la interfaz de la capa [métier], y [Metier] es su implementación;
- [AgendaMedecinJour] y [CreneauMedecinJour] son dos entidades de negocio;
2.7.1. Las entidades
La entidad [CreneauMedecinJour] asocia un horario y la posible cita programada en ese horario:
package rdvmedecins.domain;
import java.io.Serializable;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// campos
private Creneau creneau;
private Rv rv;
// constructores
public CreneauMedecinJour() {
}
public CreneauMedecinJour(Creneau creneau, Rv rv) {
this.creneau=creneau;
this.rv=rv;
}
// toString
@Override
public String toString() {
return String.format("[%s %s]", creneau, rv);
}
// getters y setters
...
}
- línea 12: el horario;
- línea 13: la cita, si la hay – null de lo contrario;
La entidad [AgendaMedecinJour] es la agenda de un médico para un día determinado, es decir, la lista de sus citas:
package rdvmedecins.domain;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.entities.Medecin;
public class AgendaMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// campos
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
// constructores
public AgendaMedecinJour() {
}
public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
this.medecin = medecin;
this.jour = jour;
this.creneauxMedecinJour = creneauxMedecinJour;
}
public String toString() {
StringBuffer str = new StringBuffer("");
for (CreneauMedecinJour cr : creneauxMedecinJour) {
str.append(" ");
str.append(cr.toString());
}
return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
}
// getters y setters
...
}
- línea 13: el médico;
- línea 14: el día en la agenda;
- línea 15: sus franjas horarias con o sin citas;
2.7.2. El servicio
La interfaz de la capa [métier] es la siguiente:
package rdvmedecins.metier;
import java.util.Date;
import java.util.List;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
public interface IMetier {
// lista de clientes
public List<Client> getAllClients();
// lista de médicos
public List<Medecin> getAllMedecins();
// lista de horarios de un médico
public List<Creneau> getAllCreneaux(long idMedecin);
// lista de citas de un médico en un día determinado
public List<Rv> getRvMedecinJour(long idMedecin, Date jour);
// buscar un cliente identificado por su ID
public Client getClientById(long id);
// buscar un cliente identificado por su ID
public Medecin getMedecinById(long id);
// buscar una cita identificada por su ID
public Rv getRvById(long id);
// buscar un horario identificado por su ID
public Creneau getCreneauById(long id);
// agregar un RV
public Rv ajouterRv(Date jour, Creneau créneau, Client client);
// eliminar un RV
public void supprimerRv(Rv rv);
// profesión
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);
}
Los comentarios explican la función de cada uno de los métodos.
La implementación de la interfaz [IMetier] es la siguiente clase [Metier]:
package rdvmedecins.metier;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.domain.CreneauMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.repositories.ClientRepository;
import rdvmedecins.repositories.CreneauRepository;
import rdvmedecins.repositories.MedecinRepository;
import rdvmedecins.repositories.RvRepository;
import com.google.common.collect.Lists;
@Service("métier")
public class Metier implements IMetier {
// repositorios
@Autowired
private MedecinRepository medecinRepository;
@Autowired
private ClientRepository clientRepository;
@Autowired
private CreneauRepository creneauRepository;
@Autowired
private RvRepository rvRepository;
// Implementación de interfaz
@Override
public List<Client> getAllClients() {
return Lists.newArrayList(clientRepository.findAll());
}
@Override
public List<Medecin> getAllMedecins() {
return Lists.newArrayList(medecinRepository.findAll());
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return Lists.newArrayList(creneauRepository.getAllCreneaux(idMedecin));
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return Lists.newArrayList(rvRepository.getRvMedecinJour(idMedecin, jour));
}
@Override
public Client getClientById(long id) {
return clientRepository.findOne(id);
}
@Override
public Medecin getMedecinById(long id) {
return medecinRepository.findOne(id);
}
@Override
public Rv getRvById(long id) {
return rvRepository.findOne(id);
}
@Override
public Creneau getCreneauById(long id) {
return creneauRepository.findOne(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau créneau, Client client) {
return rvRepository.save(new Rv(jour, client, créneau));
}
@Override
public void supprimerRv(Rv rv) {
rvRepository.delete(rv.getId());
}
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
...
}
}
- línea 24: la anotación [@Service] es una anotación de Spring que convierte a la clase anotada en un componente administrado por Spring. Se le puede dar o no un nombre a un componente. Este se llama [métier];
- línea 25: la clase [Metier] implementa la interfaz [IMetier];
- línea 28: la anotación [@Autowired] es una anotación de Spring. El valor del campo así anotado será inicializado (inyectado) por Spring con la referencia de un componente de Spring del tipo o del nombre especificados. En este caso, la anotación [@Autowired] no especifica ningún nombre. Por lo tanto, se realizará una inyección por tipo;
- línea 29: el campo [medecinRepository] se inicializará con la referencia de un componente de Spring del tipo [MedecinRepository]. Será la referencia de la clase generada por Spring Data para implementar la interfaz [MedecinRepository] que ya hemos presentado;
- líneas 30-35: este proceso se repite para las otras tres interfaces analizadas;
- líneas 39-41: implementación del método [getAllClients];
- línea 40: utilizamos el método [findAll] de la interfaz [ClientRepository]. Este método devuelve un tipo [Iterable<Client>] que transformamos en [List<Client>] con el método estático [Lists.newArrayList]. La clase [Lists] está definida en la biblioteca Google Guava. En [pom.xml] se ha importado esta dependencia:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
- líneas 38-86: los métodos de la interfaz [IMetier] se implementan con la ayuda de las clases de la capa [DAO];
Solo el método de la línea 88 es específico de la capa [métier]. Se colocó aquí porque realiza un procesamiento de negocio que va más allá de un simple acceso a los datos. Sin este método, no habría habido razón para crear una capa [métier]. El método [getAgendaMedecinJour] es el siguiente:
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
// lista de horarios disponibles del médico
List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
// lista de citas de ese mismo médico para ese mismo día
List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
// se crea un diccionario a partir de las citas programadas
Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
for (Rv resa : reservations) {
hReservations.put(resa.getCreneau().getId(), resa);
}
// se crea la agenda para el día solicitado
AgendaMedecinJour agenda = new AgendaMedecinJour();
// el médico
agenda.setMedecin(getMedecinById(idMedecin));
// el día
agenda.setJour(jour);
// los horarios de reserva
CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
agenda.setCreneauxMedecinJour(creneauxMedecinJour);
// llenado de los horarios de reserva
for (int i = 0; i < creneauxHoraires.size(); i++) {
// línea i de la agenda
creneauxMedecinJour[i] = new CreneauMedecinJour();
// franja horaria
Creneau créneau = creneauxHoraires.get(i);
long idCreneau = créneau.getId();
creneauxMedecinJour[i].setCreneau(créneau);
// ¿El horario está libre o reservado?
if (hReservations.containsKey(idCreneau)) {
// el horario está ocupado: se registra la reserva
Rv resa = hReservations.get(idCreneau);
creneauxMedecinJour[i].setRv(resa);
}
}
// se devuelve el resultado
return agenda;
}
Se invita al lector a leer los comentarios. El algoritmo es el siguiente:
- se recuperan todos los horarios disponibles del médico indicado;
- se recuperan todas sus citas para el día indicado;
- con esta información, se puede determinar si un horario está libre u ocupado;
2.8. La configuración del proyecto
![]() |
La clase [DomainAndPersitenceConfig] configura todo el proyecto:
package rdvmedecins.config;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.orm.jpa.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
// la fuente de datos MySQL
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
return dataSource;
}
// El proveedor JPA —no es necesario si te satisfacen los valores predeterminados que usa Spring Boot
// aquí lo definimos para activar o desactivar los registros SQL
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory y TransactionManager se definen con valores predeterminados por Spring Boot
}
- línea 45: no vamos a definir los beans [EntityManagerFactory] y [TransactionManager]. Para ello, nos basaremos en la anotación [@EnableAutoConfiguration] de Spring Boot (línea 17);
- líneas 24-32: definen la fuente de datos MySQL5. Se trata de un bean que, por lo general, Spring Boot no puede deducir;
- líneas 36-43: también configuramos la implementación JPA para establecer el atributo [showSql] de Hibernate en «false» (línea 39). Por defecto, está en «true»;
- por el momento, los únicos componentes gestionados por Spring son los beans de las líneas 25 y 37, además de los beans [EntityManagerFactory] y [TransactionManager] mediante autoconfiguración. Debemos agregar los beans de las capas [métier] y [DAO];
- la línea 16 agrega al contexto de Spring las interfaces del paquete [rdvmdecins.repositories] que heredan de la interfaz [CrudRepository];
- la línea 18 agrega al contexto de Spring todas las clases del paquete [rdvmedecins] y sus descendientes que tengan una anotación de Spring. En el paquete [rdvmdecins.metier], se encontrará la clase [Metier] con su anotación [@Service] y se agregará al contexto de Spring;
- línea 45: Spring Boot definirá por defecto un bean [entityManagerFactory]. Se le debe indicar a este bean dónde se encuentran las entidades JPA que debe gestionar. Esto se hace en la línea 19;
- línea 20: indica que los métodos de las interfaces que heredan de la interfaz [CrudRepository] deben ejecutarse dentro de una transacción;
2.9. Las pruebas de la capa [métier]
La clase [rdvmedecins.tests.Metier] es una clase de prueba de Spring / JUnit 4:
package rdvmedecins.tests;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import org.junit.Assert;
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 rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Metier {
@Autowired
private IMetier métier;
@Test
public void test1(){
// visualización de clientes
List<Client> clients = métier.getAllClients();
display("Liste des clients :", clients);
// visualización de médicos
List<Medecin> medecins = métier.getAllMedecins();
display("Liste des médecins :", medecins);
// visualización de horarios de un médico
Medecin médecin = medecins.get(0);
List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
// lista de citas de un médico en un día determinado
Date jour = new Date();
display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// agregar un RV
Rv rv = null;
Creneau créneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
rv = métier.ajouterRv(jour, créneau, client);
// verificación
Rv rv2 = métier.getRvById(rv.getId());
Assert.assertEquals(rv, rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// Agregar un RV en el mismo horario del mismo día
// debe provocar una excepción
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
Boolean erreur = false;
try {
rv = métier.ajouterRv(jour, créneau, client);
System.out.println("Rv ajouté");
} catch (Exception ex) {
Throwable th = ex;
while (th != null) {
System.out.println(ex.getMessage());
th = th.getCause();
}
// se registra el error
erreur = true;
}
// se verifica que haya habido un error
Assert.assertTrue(erreur);
// lista de RV
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// visualización del calendario
AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
System.out.println(agenda);
Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
// eliminar un RV
System.out.println("Suppression du Rv ajouté");
métier.supprimerRv(rv);
// verificación
rv2 = métier.getRvById(rv.getId());
Assert.assertNull(rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
}
// método de utilidad: muestra los elementos de una colección
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- línea 22: la anotación [@SpringApplicationConfiguration] permite utilizar el archivo de configuración [DomainAndPersistenceConfig] analizado anteriormente. De este modo, la clase de prueba se beneficia de todos los beans definidos en dicho archivo;
- línea 23: la anotación [@RunWith] permite la integración de Spring con JUnit: la clase podrá ejecutarse como una prueba JUnit. [@RunWith] es una anotación JUnit (línea 9), mientras que la clase [SpringJUnit4ClassRunner] es una clase de Spring (línea 12);
- líneas 26-27: inyección en la clase de prueba de una referencia a la capa [métier];
- muchas pruebas son simplemente pruebas visuales:
- líneas 32-33: lista de clientes;
- líneas 35-36: lista de médicos;
- líneas 39-40: lista de horarios de un médico;
- línea 43: lista de citas de un médico;
- línea 50: agregar una nueva cita. El método [ajouterRv] devuelve la cita con información adicional, su clave primaria id;
- línea 53: se utiliza esta clave primaria para buscar la cita en la base de datos;
- línea 54: se verifica que la cita buscada y la cita encontrada sean la misma. Recordemos que el método [equals] de la entidad [Rv] se ha redefinido: dos citas son iguales si tienen el mismo id. En este caso, esto nos indica que la cita agregada se ha guardado correctamente en la base de datos;
- líneas 61-73: se intenta agregar por segunda vez la misma cita. Esto debe ser rechazado por el SGBD, ya que existe una restricción de unicidad:
CREATE TABLE IF NOT EXISTS `rv` (
`ID` bigint(20) NOT NULL AUTO_INCREMENT,
`JOUR` date NOT NULL,
`ID_CLIENT` bigint(20) NOT NULL,
`ID_CRENEAU` bigint(20) NOT NULL,
`VERSION` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`ID`),
UNIQUE KEY `UNQ1_RV` (`JOUR`,`ID_CRENEAU`),
KEY `FK_RV_ID_CRENEAU` (`ID_CRENEAU`),
KEY `FK_RV_ID_CLIENT` (`ID_CLIENT`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci AUTO_INCREMENT=60 ;
La línea 8 anterior indica que la combinación [JOUR, ID_CRENEAU] debe ser única, lo que impide introducir dos citas el mismo día en el mismo intervalo de tiempo.
- línea 73: se verifica que efectivamente se haya producido una excepción;
- línea 77: se solicita la agenda del médico para quien se acaba de agregar una cita;
- línea 79: se verifica que la cita agregada aparezca efectivamente en su agenda;
- línea 82: se elimina la cita agregada;
- línea 84: se busca en la base de datos la cita eliminada;
- línea 85: se verifica que se ha recuperado un puntero null, lo que indica que la cita buscada no existe;
La ejecución de la prueba se realiza con éxito:
![]() |
2.10. El programa muestra en la consola
![]() |
El programa de consola es básico. Muestra cómo recuperar una clave externa:
package rdvmedecins.boot;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
public class Boot {
// el arranque
public static void main(String[] args) {
// se prepara la configuración
SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
app.setLogStartupInfo(false);
// se inicia
ConfigurableApplicationContext context = app.run(args);
// función
IMetier métier = context.getBean(IMetier.class);
try {
// agregar un RV
Date jour = new Date();
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
Client client = (Client) new Client().build(1L, 1L);
Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
Rv rv = métier.ajouterRv(jour, créneau, client);
System.out.println(String.format("Rv ajouté = %s", rv));
// verificación
créneau = métier.getCreneauById(1L);
long idMedecin = créneau.getIdMedecin();
display("Liste des rendez-vous", métier.getRvMedecinJour(idMedecin, jour));
} catch (Exception ex) {
System.out.println("Exception : " + ex.getCause());
}
// cierre del contexto de Spring
context.close();
}
// método de utilidad: muestra los elementos de una colección
private static <T> void display(String message, Iterable<T> elements) {
System.out.println(message);
for (T element : elements) {
System.out.println(element);
}
}
}
El programa agrega una cita y luego verifica que se haya agregado.
- línea 19: la clase [SpringApplication] utilizará la clase de configuración [DomainAndPersistenceConfig];
- línea 20: se eliminan los registros de inicio de la aplicación;
- línea 22: se ejecuta la clase [SpringApplication]. Devuelve un contexto de Spring, es decir, la lista de beans registrados;
- línea 24: se obtiene una referencia al bean que implementa la interfaz [IMetier]. Por lo tanto, se trata de una referencia a la capa [métier];
- líneas 27-31: se agrega una nueva cita para hoy, para el cliente n.º 1 en el horario n.º 1. El cliente y el horario se crearon desde cero para demostrar que solo se utilizan los identificadores. Aquí se inicializó la versión, pero se podría haber puesto cualquier valor. No se utiliza aquí;
- línea 34: queremos saber qué médico tiene la franja n.º 1. Para ello, necesitamos buscar la franja n.º 1 en la base de datos. Como estamos en el modo [FetchType.LAZY], el médico no se muestra junto con la franja. Sin embargo, nos hemos asegurado de incluir un campo [idMedecin] en la entidad [Creneau] para recuperar la clave primaria del médico;
- línea 35: se recupera el código de atención primaria del médico;
- línea 36: se muestra la lista de citas del médico;
Los resultados en la consola son los siguientes:
2.11. Introducción a Spring MVC
![]() |
Ahora abordamos la construcción de la capa web. Esta se compone principalmente de métodos que procesan URL específicos y responden con una línea de texto en formato JSON (Notación de Objetos de JavaScript). Esta capa web es una interfaz web que a veces se denomina «API web». Implementaremos esta interfaz con Spring MVC, otra rama del ecosistema Spring. Comenzaremos por estudiar una de las guías que se encuentran en [http://spring.io].
2.11.1. El proyecto de demostración
![]() |
- en [1], importamos una de las guías de Spring;
![]() |
- en [2], elegimos el ejemplo [Rest Service];
- en [3], elegimos el proyecto Maven;
- en [4], tomamos la versión final de la guía;
- en [5], validamos;
- en [6], el proyecto importado;
Los servicios web accesibles a través de URL estándar y que entregan texto JSON suelen denominarse servicios REST (REpresentational State Transfer). En este documento, me limitaré a llamar al servicio que vamos a construir un servicio web / JSON. Se dice que un servicio es RESTful si cumple con ciertas reglas. No he buscado cumplir con ellas.
Ahora analicemos el proyecto importado, comenzando por su configuración de Maven.
2.11.2. Configuración de Maven
El archivo [pom.xml] es el siguiente:
<?xml version="1.0" encoding="UTF-8"?>
<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>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<url>http://repo.spring.io/release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>http://repo.spring.io/release</url>
</pluginRepository>
</pluginRepositories>
</project>
- líneas 10-14: al igual que en el proyecto [Spring Data], se encuentra el proyecto padre [Spring Boot];
- líneas 17-20: el artefacto [spring-boot-starter-web] incluye las bibliotecas necesarias para un proyecto Spring MVC. En particular, incluye un servidor Tomcat integrado. Es en este servidor donde se ejecutará la aplicación;
- líneas 21-24: la biblioteca Jackson se encarga de la transformación de un objeto Java en una cadena y viceversa;
Las bibliotecas que incluye esta configuración son muy numerosas:
![]() | ![]() |
Arriba se ven los tres archivos del servidor Tomcat.
2.11.3. La arquitectura de un servicio Spring REST
Spring MVC implementa el modelo de arquitectura conocido como MVC (Modelo – Vista – Controlador) de la siguiente manera:
![]() |
El procesamiento de una solicitud de un cliente se lleva a cabo de la siguiente manera:
- solicitud: las URL solicitadas tienen el formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... La [Dispatcher Servlet] es la clase de Spring que procesa las URL entrantes. Esta «enruta» el URL hacia la acción que debe procesarlo. Estas acciones son métodos de clases específicas llamadas [Contrôleurs]. La C de MVC es aquí la cadena [Dispatcher Servlet, Contrôleur, Action]. Si no se ha configurado ninguna acción para procesar el URL entrante, el servlet [Dispatcher Servlet] responderá que no se encontró el URL solicitado (error 404 NOT FOUND);
- procesamiento
- la acción seleccionada puede utilizar los parámetros parami que le ha transmitido el servlet [Dispatcher Servlet]. Estos pueden provenir de varias fuentes:
- la ruta [/param1/param2/...] del URL,
- de los parámetros [p1=v1&p2=v2] del URL,
- de los parámetros enviados por el navegador junto con su solicitud;
- al procesar la solicitud del usuario, la acción puede requerir la capa [metier] [2b]. Una vez procesada la solicitud del cliente, esta puede generar diversas respuestas. Un ejemplo clásico es:
- una página de error si la solicitud no se pudo procesar correctamente
- una página de confirmación en caso contrario
- la acción solicita que se muestre una vista específica [3]. Esta vista mostrará datos que se denominan el modelo de la vista. Este es el M de MVC. La acción creará este modelo M [2c] y solicitará que se muestre una vista V [3];
- respuesta: la vista V seleccionada utiliza la plantilla M generada por la acción para inicializar las partes dinámicas de la respuesta HTML que debe enviar al cliente y, a continuación, envía dicha respuesta.
Para un servicio web / JSON, la arquitectura anterior se modifica ligeramente:
![]() |
- en [4a], el modelo, que es una clase Java, se transforma en una cadena JSON mediante una biblioteca JSON;
- en [4b], esta cadena JSON se envía al navegador;
2.11.4. El controlador C
![]() |
La aplicación importada cuenta con el siguiente controlador:
package hello;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@RequestMapping("/greeting")
public @ResponseBody
Greeting greeting(@RequestParam(value = "name", required = false, defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}
- línea 9: la anotación [@Controller] convierte a la clase [GreetingController] en un controlador de Spring, es decir, que sus métodos están registrados para procesar URL;
- línea 15: la anotación [@RequestMapping] indica el URL que procesa el método; en este caso, el URL [/greeting]. Más adelante veremos que este URL se puede configurar y que es posible recuperar estos parámetros;
- línea 16: la anotación [@ResponseBody] indica que el método no genera una plantilla para una vista (JSP, JSF, Thymeleaf, ...) que luego se enviará al navegador del cliente, sino que produce por sí misma la respuesta que se envía al navegador. En este caso, genera un objeto de tipo [Greeting] (línea 18). Aunque aquí no se vea, este objeto se transformará primero en JSON antes de enviarse al navegador. Es la presencia de una biblioteca JSON en las dependencias del proyecto lo que hace que Spring Boot, mediante autoconfiguración, configure el proyecto de esta manera;
- línea 17: el método [greeting] tiene un parámetro [String name]. La anotación [@RequestParam(value = "name", required = false, defaultValue = "World"] indica que este parámetro debe inicializarse con un parámetro llamado [name](@RequestParam(value = "name"). Este último puede ser el parámetro de un GET o de un POST. Este parámetro no es obligatorio (required = false). En este último caso, el parámetro [name] del método se inicializará con el valor [World] (defaultValue = "World").
2.11.5. El modelo M
El modelo M generado por el método anterior es el siguiente objeto [Greeting]:
![]() |
package hello;
public class Greeting {
private final long id;
private final String content;
public Greeting(long id, String content) {
this.id = id;
this.content = content;
}
public long getId() {
return id;
}
public String getContent() {
return content;
}
}
La transformación JSON de este objeto creará la cadena de caracteres {"id":n,"content":"texto"}. Al final, la cadena JSON generada por el método del controlador tendrá la forma:
o
2.11.6. Configuración del proyecto
![]() |
El proyecto se configura mediante la siguiente clase [Application]:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- línea 11: curiosamente, esta clase se ejecuta con un método [main] propio de las aplicaciones de consola. Así es. La clase [SpringApplication] de la línea 12 iniciará el servidor Tomcat presente en las dependencias e implementará el servicio REST en él;
- línea 4: vemos que la clase [SpringApplication] pertenece al proyecto [Spring Boot];
- línea 12: el primer parámetro es la clase que configura el proyecto; el segundo, los parámetros adicionales, si los hay;
- línea 8: la anotación [@EnableAutoConfiguration] le pide a Spring Boot que configure el proyecto;
- línea 7: la anotación [@ComponentScan] hace que se explore la carpeta que contiene la clase [Application] para buscar los componentes de Spring. Se encontrará uno: la clase [GreetingController], que cuenta con la anotación [@Controller], lo que la convierte en un componente de Spring;
2.11.7. Ejecución del proyecto
Ejecutemos el proyecto:
![]() |
Se obtienen los siguientes registros de consola:
____ _ __ _ _
- línea 12: el servidor Tomcat se inicia en el puerto 8080 (línea 11);
- línea 16: el servlet [DispatcherServlet] está presente;
- línea 19: se ha detectado el método [GreetingController.greeting];
Para probar la aplicación web, se solicita el URL [http://localhost:8080/greeting]:
![]() | ![]() |
Se recibe correctamente la cadena esperada JSON. Puede ser interesante ver los encabezados HTTP enviados por el servidor. Para ello, utilizaremos el complemento de Chrome llamado [Advanced Rest Client] (véanse los anexos):
![]() |
- en [1], el URL solicitado;
- en [2], se utiliza el método GET;
- en [3], la respuesta JSON;
- en [4], el servidor indicó que enviaba una respuesta en el formato JSON;
- en [5], se solicita el mismo URL, pero esta vez con un POST;
- en [7], la información se envía al servidor en el formato [urlencoded];
- en [6], el parámetro «name» con su valor;
- en [8], el navegador le indica al servidor que le está enviando información [urlencoded];
- en [9], la respuesta JSON del servidor;
2.11.8. Creación de un archivo ejecutable
Es posible crear un archivo ejecutable fuera de Eclipse. La configuración necesaria se encuentra en el archivo [pom.xml]:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.Application</start-class>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- las líneas 9 a 12 definen el complemento que creará el archivo ejecutable;
- la línea 3 define la clase ejecutable del proyecto;
Se procede de la siguiente manera:
![]() |
- en [1]: se ejecuta un objetivo de Maven;
- en [2]: hay dos objetivos (goals): [clean] para eliminar la carpeta [target] del proyecto Maven, y [package] para regenerarla;
- en [3]: la carpeta [target] generada se guardará en esta carpeta;
- en [4]: se genera el archivo de destino;
En los registros que aparecen en la consola, es importante que aparezca el complemento [spring-boot-maven-plugin]. Este es el que genera el archivo ejecutable.
Con una consola, nos desplazamos a la carpeta generada:
- línea 5: el archivo generado;
Este archivo se ejecuta de la siguiente manera:
Ahora que la aplicación web está en ejecución, se puede acceder a ella con un navegador:
![]() |
2.11.9. Implementar la aplicación en un servidor Tomcat
Si bien Spring Boot resulta muy práctico en modo de desarrollo, es probable que una aplicación en producción se implemente en un servidor Tomcat real. A continuación se explica cómo hacerlo:
Modifica el archivo [pom.xml] de la siguiente manera:
<?xml version="1.0" encoding="UTF-8"?>
<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>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<packaging>war</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
....
</project>
Las modificaciones deben realizarse en dos lugares:
- línea 9: hay que indicar que se va a generar un archivo WAR (Web ARchive);
- líneas 26-30: hay que agregar una dependencia del artefacto [spring-boot-starter-tomcat]. Este artefacto incorpora todas las clases de Tomcat a las dependencias del proyecto;
- línea 29: este artefacto es [provided], es decir, que los archivos correspondientes no se incluirán en el WAR generado. De hecho, estos archivos se encontrarán en el servidor Tomcat en el que se ejecutará la aplicación;
Además, es necesario configurar la aplicación web. Al no existir el archivo [web.xml], esto se hace con una clase que hereda de [SpringBootServletInitializer]:
![]() |
La clase [ApplicationInitializer] es la siguiente:
package hello;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
public class ApplicationInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
- línea 6: la clase [ApplicationInitializer] extiende la clase [SpringBootServletInitializer];
- línea 9: el método [configure] se redefine (línea 8);
- línea 10: se proporciona la clase que configura el proyecto;
Para ejecutar el proyecto, se puede proceder de la siguiente manera:
![]() |
- en [1], se ejecuta el proyecto en uno de los servidores registrados en el IDE Eclipse;
- en [2], se selecciona [tc Server Developer], que aparece por defecto. Se trata de una variante de Tomcat;
Una vez hecho esto, se puede acceder a URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] desde un navegador:
![]() |
Ahora sabemos cómo generar un archivo WAR. A continuación, seguiremos trabajando con Spring Boot y su archivo JAR ejecutable.
2.11.10. Crear un nuevo proyecto web
Para crear un nuevo proyecto web, podemos proceder de la siguiente manera:
![]() |
- en [1]: Archivo / Nuevo / Proyecto Spring Starter
- en [2]: seleccionar [Web]. No se seleccionan bibliotecas de vistas porque en un servicio web / JSON no hay vistas;
- el proyecto creado será un proyecto Maven. En [3], se ingresa el grupo del artefacto Maven que se creará; en [4], el nombre del artefacto;
- en [5], se ingresa el nombre del paquete donde Spring colocará la clase de configuración del proyecto;
- en [6], se le da un nombre al proyecto de Eclipse —puede ser diferente de [4]—;
![]() |
2.12. La capa [web]
![]() |
![]() |
Construiremos la capa web en varias etapas:
- etapa 1: una capa web operativa sin autenticación;
- paso 2: implementación de la autenticación con Spring Security;
- etapa 3: implementación de CORS [Cross-origin resource sharing (CORS) is a mechanism that allows many resources (e.g. fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain the resource originated from. (Wikipedia)]. El cliente de nuestro servicio web será un cliente web Angular que no pertenecerá necesariamente al mismo dominio que nuestro servicio web. Por defecto, no podrá acceder a él a menos que el servicio web se lo permita. Veremos cómo;
2.12.1. Configuración de Maven
El archivo [pom.xml] del proyecto es el siguiente:
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.mvc</groupId>
<artifactId>rdvmedecins-webapi-v1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-webapi-v1</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
- líneas 7-11: el proyecto padre de Maven;
- líneas 13-16: las dependencias para un proyecto Spring MVC;
- líneas 17-21: las dependencias del proyecto de capas [métier, DAO, JPA];
2.12.2. La interfaz del servicio web
![]() |
- en [1], como se muestra arriba, el navegador solo puede solicitar un número limitado de URL con una sintaxis específica;
- en [4], recibe una respuesta JSON;
Todas las respuestas de nuestro servicio web tendrán el mismo formato, correspondiente a la transformación JSON de un objeto de tipo [Reponse] como se muestra a continuación:
package rdvmedecins.web.models;
public class Reponse {
// ----------------- propiedades
// estado de la operación
private int status;
// la respuesta JSON
private Object data;
// ---------------constructores
public Reponse() {
}
public Reponse(int status, Object data) {
this.status = status;
this.data = data;
}
// métodos
public void incrStatusBy(int increment) {
status += increment;
}
// ----------------------getters y setters
...
}
- línea 7: código de error de la respuesta 0: OK, cualquier otro valor: KO;
- línea 9: el cuerpo de la respuesta;
A continuación, presentamos las capturas de pantalla que muestran la interfaz del servicio web / JSON:
Lista de todos los pacientes del consultorio médico [/getAllClients]
![]() |
Lista de todos los médicos de la clínica [/getAllMedecins]
![]() |
Lista de horarios de un médico [/getAllCreneaux/{idMedecin}]
![]() |
Lista de citas de un médico [/getRvMedecinJour/{idMedecin}/{aaaa-mm-dd}
![]() |
Agenda de un médico [/getAgendaMedecinJour/{idMedecin}/{aaaa-mm-jj}]
![]() |
Para agregar o eliminar una cita, utilizamos la extensión de Chrome [Advanced Rest Client], ya que estas operaciones se realizan con un POST.
Agregar una cita [/ajouterRv]
![]() |
- en [0], el URL del servicio web;
- en [1], se utiliza el método POST;
- en [2], el texto JSON de la información transmitida al servicio web en el formato {día, idClient, idCreneau};
- en [3], el cliente le indica al servicio web que le envía información en formato JSON;
La respuesta es entonces la siguiente:
![]() |
- en [4]: el cliente envía el encabezado que indica que los datos que envía están en formato JSON;
- en [5]: el servicio web responde que él también envía JSON;
- en [6]: la respuesta JSON del servicio web. El campo [data] contiene la forma JSON de la cita agregada;
Se puede verificar la presencia de la nueva cita:
![]() |
Eliminar una cita [/supprimerRv]
![]() |
- en [1], el URL del servicio web;
- en [2], se utiliza el método POST;
- en [3], el texto JSON de la información transmitida al servicio web en el formato {idRv};
- en [4], el cliente le indica al servicio web que le envía información JSON;
La respuesta es entonces la siguiente:
![]() |
- en [5]: el campo [status] tiene valor 0, lo que indica que la operación se realizó con éxito;
Se puede verificar la eliminación de la cita:
![]() |
Como se puede ver arriba, la cita del paciente [Mme GERMAN] ya no aparece.
El servicio web también permite recuperar entidades mediante su identificador:
![]() |
![]() |
![]() |
![]() |
Todas estas URL son procesadas por el controlador [RdvMedecinsController], que presentamos a continuación.
2.12.3. La estructura del controlador [RdvMedecinsController]
![]() |
El controlador [RdvMedecinsController] es el siguiente:
package rdvmedecins.web.controllers;
import java.text.ParseException;
...
@RestController
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
private List<String> messages;
@PostConstruct
public void init() {
// mensajes de error de la aplicación
messages = application.getMessages();
}
// lista de médicos
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
public Reponse getAllMedecins() {
...
}
// lista de clientes
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
public Reponse getAllClients() {
...
}
// lista de horarios de un médico
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
...
}
// lista de citas de un médico
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin,
@PathVariable("jour") String jour) {
...
}
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
public Reponse getClientById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
public Reponse getMedecinById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
public Reponse getRvById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
public Reponse getCreneauById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
...
}
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
...
}
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(
@PathVariable("idMedecin") long idMedecin,
@PathVariable("jour") String jour) {
...
}
}
- línea 6: la anotación [@RestController] convierte a la clase [RdvMedecinsController] en un controlador de Spring. Además, esto también implica que los métodos que procesan los URL generarán una respuesta que se transformará automáticamente en JSON;
- líneas 9-10: Spring inyectará aquí un objeto de tipo [ApplicationModel];
- línea 13: la anotación [@PostConstruct] marca un método que se ejecutará inmediatamente después de la instanciación de la clase. Cuando este se ejecuta, los objetos inyectados por Spring ya están disponibles;
- todos los métodos devuelven un objeto de tipo [Reponse] como se muestra a continuación:
package rdvmedecins.web.models;
public class Reponse {
// ----------------- propiedades
// estado de la operación
private int status;
// la respuesta
private Object data;
...
}
Este objeto se serializa en JSON antes de enviarse al navegador del cliente;
- línea 20: la anotación [@RequestMapping] establece las condiciones de llamada del método. Aquí, el método procesa una solicitud GET del URL [/getAllMedecins]. Si este URL fuera solicitado por un POST, sería rechazado y Spring MVC enviaría un código de error HTTP al cliente web;
- línea 32: el URL se configura mediante {idMedecin}. Este parámetro se recupera con la anotación [@PathVariable] en la línea 33;
- línea 33: el único parámetro [long idMedecin] recibe su valor del parámetro {idMedecin} de URL [@PathVariable("idMedecin")]. El parámetro en URL y el del método pueden tener nombres diferentes. Cabe señalar aquí que [@PathVariable("idMedecin")] es de tipo String (todo el URL es un String), mientras que el parámetro [long idMedecin] es de tipo [long]. El cambio de tipo se realiza automáticamente. Se devuelve un código de error HTTP si este cambio de tipo falla;
- línea 65: la anotación [@RequestBody] se refiere al cuerpo de la consulta. En una solicitud GET, casi nunca hay un cuerpo (aunque es posible incluir uno). En una solicitud POST, suele haberlo (aunque es posible no incluirlo). Para el URL y el [ajouterRv], el cliente web envía en su POST la siguiente cadena:
La sintaxis [@RequestBody PostAjouterRv post] (línea 65) , sumada al hecho de que el método espera el JSON [consumes = "application/json; charset=UTF-8"] de la línea 64, hará que la cadena JSON enviada por el cliente web sea deserializada en un objeto de tipo [PostAjouter]. Este es el siguiente:
package rdvmedecins.web.models;
public class PostAjouterRv {
// datos de la publicación
private String jour;
private long idClient;
private long idCreneau;
// getter y setter
...
}
Aquí también se realizarán automáticamente los cambios de tipo necesarios;
- en las líneas 69-70, encontramos un mecanismo similar para URL y [/supprimerRv]. La cadena JSON enviada es la siguiente:
y el tipo [PostSupprimerRv] es el siguiente:
package rdvmedecins.web.models;
public class PostSupprimerRv {
// datos del post
private long idRv;
// getter y setter
...
}
2.12.4. Las plantillas del servicio web
![]() |
Ya hemos presentado los modelos [Reponse, PostAjouterRv, PostSupprimerRv]. El modelo [ApplicationModel] es el siguiente:
package rdvmedecins.web.models;
import java.util.Date;
...
@Component
public class ApplicationModel implements IMetier {
// la capa [métier]
@Autowired
private IMetier métier;
// datos procedentes de la capa [métier]
private List<Medecin> médecins;
private List<Client> clients;
// mensajes de error
private List<String> messages;
@PostConstruct
public void init() {
// se recuperan los médicos y los clientes
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
// getter
public List<String> getMessages() {
return messages;
}
// ------------------------- interfaz de la capa [métier]
@Override
public List<Client> getAllClients() {
return clients;
}
@Override
public List<Medecin> getAllMedecins() {
return médecins;
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return métier.getAllCreneaux(idMedecin);
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return métier.getRvMedecinJour(idMedecin, jour);
}
@Override
public Client getClientById(long id) {
return métier.getClientById(id);
}
@Override
public Medecin getMedecinById(long id) {
return métier.getMedecinById(id);
}
@Override
public Rv getRvById(long id) {
return métier.getRvById(id);
}
@Override
public Creneau getCreneauById(long id) {
return métier.getCreneauById(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
return métier.ajouterRv(jour, creneau, client);
}
@Override
public void supprimerRv(Rv rv) {
métier.supprimerRv(rv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
return métier.getAgendaMedecinJour(idMedecin, jour);
}
}
- línea 6: la anotación [@Component] convierte a la clase [ApplicationModel] en un componente de Spring. Al igual que todos los componentes de Spring que hemos visto hasta ahora (excepto @Controller), solo se instanciará un objeto de este tipo (singleton);
- línea 7: la clase [ApplicationModel] implementa la interfaz [IMetier];
- líneas 10-11: Spring inyecta una referencia en la capa [métier];
- línea 19: la anotación [@PostConstruct] hace que el método [init] se ejecute inmediatamente después de la instanciación de la clase [ApplicationModel];
- líneas 23-24: se recuperan las listas de médicos y clientes de la capa [métier];
- línea 26: si se produce una excepción, se almacenan los mensajes de la pila de excepciones en el campo de la línea 17;
La clase [ApplicationModel] nos servirá para dos cosas:
- como caché para almacenar las listas de médicos y pacientes (clientes);
- como interfaz única para los controladores;
La arquitectura de la capa web evoluciona de la siguiente manera:
![]() |
- en [2b], los métodos del controlador o controladores se comunican con el singleton [ApplicationModel];
Esta estrategia aporta flexibilidad en la gestión de la caché. Actualmente, los horarios de los médicos no se almacenan en caché. Para hacerlo, basta con modificar la clase [ApplicationModel]. Esto no tiene ningún impacto en el controlador, que seguirá utilizando el método [List<Creneau> getAllCreneaux(long idMedecin)] como lo hacía anteriormente. Lo que se modificará es la implementación de este método en [ApplicationModel].
2.12.5. La clase Static
La clase [Static] agrupa un conjunto de métodos estáticos de utilidad que no tienen un carácter «de negocio» ni «web»:
![]() |
Su código es el siguiente:
package rdvmedecins.web.helpers;
import java.text.SimpleDateFormat;
...
public class Static {
public Static() {
}
// lista de mensajes de error de una excepción
public static List<String> getErreursForException(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) {
erreurs.add(cause.getMessage());
cause = cause.getCause();
}
return erreurs;
}
// mapeadores Object --> Map
// --------------------------------------------------------
....
}
- línea 12: el método [Static.getErreursForException] que se utilizó (línea 8 a continuación) en el método [init] de la clase [ApplicationModel]:
@PostConstruct
public void init() {
// se recuperan los médicos y los clientes
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
El método crea un objeto [List<String>] con los mensajes de error [exception.getMessage()] de una excepción [exception] y de las que esta contiene, [exception.getCause()].
La clase [Static] contiene otros métodos de utilidad sobre los que hablaremos cuando los encontremos.
Ahora vamos a detallar el procesamiento de los URL del servicio web. En este procesamiento intervienen tres clases principales:
- el controlador [RdvMedecinsController];
- la clase de métodos de utilidad [Static];
- la clase de caché [ApplicationModel];
![]() |
2.12.6. El método [init] del controlador
El controlador [RdvMedecinsController] (véase el párrafo 2.12.3) cuenta con un método [init] que se ejecuta inmediatamente después de su instanciación:
@Autowired
private ApplicationModel application;
private List<String> messages;
@PostConstruct
public void init() {
// mensajes de error de la aplicación
messages = application.getMessages();
}
- línea 8: los mensajes de error almacenados en la caché de la aplicación [ApplicationModel] se guardan localmente en el campo de la línea 3. Esto permitirá a los métodos saber si la aplicación se ha inicializado correctamente.
2.12.7. La URL [/getAllMedecins]
El URL [/getAllMedecins] es procesado por el siguiente método del controlador [RdvMedecinsController]:
// lista de médicos
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
public Reponse getAllMedecins() {
// estado de la aplicación
if (messages != null) {
return new Reponse(-1, messages);
}
// lista de médicos
try {
return new Reponse(0, application.getAllMedecins());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
- línea 5: se verifica si la aplicación se ha inicializado correctamente (mensajes == null). Si no es así, se devuelve una respuesta con status=-1 y data=messages;
- línea 10: de lo contrario, se devuelve la lista de médicos con un status igual a 0. El método [application.getAllMedecins()] no lanza una excepción, ya que simplemente devuelve una lista que se encuentra en la caché. No obstante, mantendremos este manejo de excepciones por si acaso los médicos ya no se almacenaran en la caché;
Aún no hemos ilustrado el caso en el que la aplicación se haya inicializado incorrectamente. Detengamos SGBD y MySQL5, iniciemos el servicio web y luego solicitemos URL y [/getAllMedecins]:

Efectivamente, aparece un error. En condiciones normales, se obtiene la siguiente vista:
![]() |
2.12.8. El URL [/getAllClients]
El URL [/getAllClients] es procesado por el siguiente método del controlador [RdvMedecinsController]:
// lista de clientes
@RequestMapping(value = "/getAllClients")
public Reponse getAllClients() {
// estado de la aplicación
if (messages != null) {
return new Reponse(-1, messages);
}
// lista de clientes
try {
return new Reponse(0, application.getAllClients());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
Es similar al método [getAllMedecins] ya analizado. Los resultados obtenidos son los siguientes:
![]() |
2.12.9. El URL [/getAllCreneaux/{idMedecin}]
El URL [/getAllCreneaux/{idMedecin}] es procesado mediante el siguiente método del controlador [RdvMedecinsController]:
// lista de horarios de un médico
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
// estado de la aplicación
if (messages != null) {
return new Reponse(-1, messages);
}
// se busca al médico
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// horarios del médico
List<Creneau> créneaux = null;
try {
créneaux = application.getAllCreneaux(médecin.getId());
} catch (Exception e1) {
return new Reponse(3, Static.getErreursForException(e1));
}
// se devuelve la respuesta
return new Reponse(0, Static.getListMapForCreneaux(créneaux));
}
- línea 9: se solicita al médico identificado por el parámetro [id] mediante un método local:
private Reponse getMedecin(long id) {
// se recupera el médico
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// ¿Médico existente?
if (médecin == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, médecin);
}
Se regresa de este método con un status en [0,1,2]. Volvamos al código del método [getAllCreneaux]:
- líneas 10-12: si es status!=0, se devuelve inmediatamente la respuesta;
- línea 13: se recoge al médico;
- línea 17: se recuperan los horarios disponibles de este médico;
- línea 22: se envía como respuesta un objeto [Static.getListMapForCreneaux(créneaux)];
Recordemos la definición de la clase [Creneau]:
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// características de un horario de RV
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// un horario está vinculado a un médico
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// clave externa
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
...
}
- línea 13: se busca al médico en modo [FetchType.LAZY];
Recordemos la consulta JPQL que implementa el método [getAllCreneaux] en la capa [DAO]:
@Query("select c from Creneau c where c.medecin.id=?1")
La notación [c.medecin.id] obliga a realizar la unión entre las tablas [CRENEAUX] y [MEDECINS]. Por lo tanto, la consulta devuelve todos los turnos del médico, con el nombre del médico en cada uno de ellos. Al serializar estos turnos en JSON, aparece la cadena JSON del médico en cada uno de ellos. Esto es innecesario. Por lo tanto, en lugar de serializar un objeto [Creneau], serializaremos un objeto [Map] en el que solo incluiremos los campos deseados.
Volvamos al código que analizamos inicialmente:
// se devuelve la respuesta
return new Reponse(0, Static.getListMapForCreneaux(créneaux));
El método [Static.getListMapForCreneaux] es el siguiente:
// List<Creneau> --> List<Map>
public static List<Map<String, Object>> getListMapForCreneaux(List<Creneau> créneaux) {
// lista de diccionarios <String, Object>
List<Map<String, Object>> liste = new ArrayList<Map<String, Object>>();
for (Creneau créneau : créneaux) {
liste.add(Static.getMapForCreneau(créneau));
}
// se devuelve la lista
return liste;
}
y el método [Static.getMapForCreneau] es el siguiente:
// Creneau --> Map
public static Map<String, Object> getMapForCreneau(Creneau créneau) {
// ¿Hay algo que hacer?
if (créneau == null) {
return null;
}
// diccionario <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", créneau.getId());
hash.put("hDebut", créneau.getHdebut());
hash.put("mDebut", créneau.getMdebut());
hash.put("hFin", créneau.getHfin());
hash.put("mFin", créneau.getMfin());
// se devuelve el diccionario
return hash;
}
- línea 8: se crea un diccionario;
- líneas 9-13: se añaden los campos que se desean conservar en la cadena JSON. El campo [medecin] no está incluido;
- línea 15: se devuelve este diccionario;
Los resultados obtenidos son los siguientes:
![]() |
o bien estos, si el intervalo no existe:
![]() |
o estos en caso de error al acceder a la base de datos:
![]() |
2.12.10. El URL [/getRvMedecinJour/{idMedecin}/{jour}]
El URL y el [/getRvMedecinJour/{idMedecin}/{jour}] son procesados mediante el siguiente método del controlador [RdvMedecinsController]:
// lista de citas de un médico
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
// estado de la aplicación
if (messages != null) {
return new Reponse(-1, messages);
}
// se verifica la fecha
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(3, null);
}
// se recupera el médico
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// lista de sus citas
List<Rv> rvs = null;
try {
rvs = application.getRvMedecinJour(médecin.getId(), jourAgenda);
} catch (Exception e1) {
return new Reponse(4, Static.getErreursForException(e1));
}
// se devuelve la respuesta
return new Reponse(0, Static.getListMapForRvs(rvs));
}
- línea 31: se devuelve un objeto List<Map<String,Object>> en lugar de un objeto List<Rv>. Recordemos la definición de la clase [Rv]:
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// características de una cita
@Temporal(TemporalType.DATE)
private Date jour;
// una cita está vinculada a un cliente
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// una cita está vinculada a un horario
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// claves externas
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
...
}
- línea 11: se busca el cliente con el modo [FetchType.LAZY];
- línea 18: se busca la franja horaria con el modo [FetchType.LAZY];
Recordemos la consulta JPQL que busca las citas:
@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
Se realizan uniones explícitas para recuperar los campos [client] y [creneau]. Además, debido a la unión [cr.medecin.id=?1], también tendremos el nombre del médico. Por lo tanto, el médico aparecerá en la cadena JSON de cada cita. Sin embargo, esta información duplicada es, además, innecesaria. Volvamos al código del método:
- línea 31: construimos nosotros mismos el diccionario que se va a serializar en JSON;
El diccionario creado para una cita es el siguiente:
// Cita --> Mapa
public static Map<String, Object> getMapForRv(Rv rv) {
// ¿Hay algo que hacer?
if (rv == null) {
return null;
}
// diccionario <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", rv.getId());
hash.put("client", rv.getClient());
hash.put("creneau", getMapForCreneau(rv.getCreneau()));
// se devuelve el diccionario
return hash;
}
- línea 11: retomamos el diccionario del objeto [Creneau] que presentamos anteriormente;
Los resultados obtenidos son los siguientes:
![]() |
o también estos, con un día incorrecto:
![]() |
o estos otros con un médico incorrecto:
![]() |
2.12.11. El URL [/getAgendaMedecinJour/{idMedecin}/{jour}]
El URL y el [/getAgendaMedecinJour/{idMedecin}/{jour}] se procesan mediante el siguiente método del controlador [RdvMedecinsController]:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
// estado de la aplicación
if (messages != null) {
return new Reponse(-1, messages);
}
// se verifica la fecha
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(3, new String[] { String.format("jour [%s] invalide", jour) });
}
// se busca al médico
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// se recupera su agenda
AgendaMedecinJour agenda = null;
try {
agenda = application.getAgendaMedecinJour(médecin.getId(), jourAgenda);
} catch (Exception e1) {
return new Reponse(4, Static.getErreursForException(e1));
}
// ok
return new Reponse(0, Static.getMapForAgendaMedecinJour(agenda));
}
}
- En la línea 30, se devuelve un objeto de tipo List<Map<String,Object>.
El método [Static.getMapForAgendaMedecinJour] es el siguiente:
// AgendaMedecinJour --> Mapa
public static Map<String, Object> getMapForAgendaMedecinJour(AgendaMedecinJour agenda) {
// ¿Hay algo que hacer?
if (agenda == null) {
return null;
}
// diccionario <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("medecin", agenda.getMedecin());
hash.put("jour", new SimpleDateFormat("yyyy-MM-dd").format(agenda.getJour()));
List<Map<String, Object>> créneaux = new ArrayList<Map<String, Object>>();
for (CreneauMedecinJour créneau : agenda.getCreneauxMedecinJour()) {
créneaux.add(getMapForCreneauMedecinJour(créneau));
}
hash.put("creneauxMedecin", créneaux);
// creamos el diccionario
return hash;
}
El diccionario creado tiene tres campos:
- [medecin]: el médico propietario de la agenda. Se ha conservado esta información porque solo aparece una vez, mientras que en los casos anteriores se repetía en cada cadena JSON;
- [jour]: el día de la agenda;
- [creneauxMedecin]: la lista de franjas horarias del médico con una posible cita en esa franja;
El método [getMapForCreneauMedecinJour] utilizado en la línea 13 es el siguiente:
// CreneauMedecinJour --> mapa
public static Map<String, Object> getMapForCreneauMedecinJour(CreneauMedecinJour créneau) {
// ¿Hay algo que hacer?
if (créneau == null) {
return null;
}
// diccionario <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("creneau", getMapForCreneau(créneau.getCreneau()));
hash.put("rv", getMapForRv(créneau.getRv()));
// se devuelve el diccionario
return hash;
}
- líneas 9-10: se utilizan los diccionarios ya analizados para los tipos [Creneau] y [Rv], los cuales, por lo tanto, no incluyen el objeto [Medecin];
Los resultados obtenidos son los siguientes:
![]() |
o bien estos, si el día es incorrecto:
![]() |
o bien estos si el número del médico no es válido:
![]() |
2.12.12. El URL [/getMedecinById/{id}]
El URL [/getMedecinById/{id}] se procesa mediante el siguiente método del controlador [RdvMedecinsController]:
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
public Reponse getMedecinById(@PathVariable("id") long id) {
// estado de la aplicación
if (messages != null) {
return new Reponse(-1, messages);
}
// se busca al médico
return getMedecin(id);
}
En la línea 8, el método [getMedecin] es el siguiente:
private Reponse getMedecin(long id) {
// ¿Se recupera el médico
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// ¿Existe el médico?
if (médecin == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, médecin);
}
Los resultados obtenidos son los siguientes:
![]() |
o bien estos, si el número del médico es incorrecto:
![]() |
2.12.13. El URL [/getClientById/{id}]
El URL [/getClientById/{id}] se procesa mediante el siguiente método del controlador [RdvMedecinsController]:
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
public Reponse getClientById(@PathVariable("id") long id) {
// estado de la aplicación
if (messages != null) {
return new Reponse(-1, messages);
}
// se recupera el cliente
return getClient(id);
}
En la línea 8, el método [getClient] es el siguiente:
private Reponse getClient(long id) {
// se recupera el cliente
Client client = null;
try {
client = application.getClientById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// ¿Cliente existente?
if (client == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, client);
}
Los resultados obtenidos son los siguientes:
![]() |
o bien estos, si el número de cliente es incorrecto:
![]() |
2.12.14. El URL [/getCreneauById/{id}]
El URL [/getCreneauById/{id}] se procesa mediante el siguiente método del controlador [RdvMedecinsController]:
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
public Reponse getCreneauById(@PathVariable("id") long id) {
// estado de la aplicación
if (messages != null) {
return new Reponse(-1, messages);
}
// se recupera la franja horaria
Reponse réponse = getCreneau(id);
if (réponse.getStatus() == 0) {
réponse.setData(Static.getMapForCreneau((Creneau) réponse.getData()));
}
// resultado
return réponse;
}
En la línea 8, el método [getCreneau] es el siguiente:
private Reponse getCreneau(long id) {
// se recupera el horario
Creneau créneau = null;
try {
créneau = application.getCreneauById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// ¿Ya existe ese intervalo?
if (créneau == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, créneau);
}
Los resultados obtenidos son los siguientes:
![]() |
o estos, si el número de franja horaria es incorrecto:
![]() |
2.12.15. El URL [/getRvById/{id}]
El URL [/getRvById/{id}] se procesa mediante el siguiente método del controlador [RdvMedecinsController]:
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
public Reponse getRvById(@PathVariable("id") long id) {
// estado de la aplicación
if (messages != null) {
return new Reponse(-1, messages);
}
// se recupera la cita
Reponse réponse = getRv(id);
if (réponse.getStatus() == 0) {
réponse.setData(Static.getMapForRv2((Rv) réponse.getData()));
}
// resultado
return réponse;
}
En la línea 8, el método [getRv] es el siguiente:
private Reponse getRv(long id) {
// se recupera el Rv
Rv rv = null;
try {
rv = application.getRvById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// ¿Existe el Rv?
if (rv == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, rv);
}
En la línea 10, el método [Static.getMapForRv2] es el siguiente:
// Rv --> Mapa
public static Map<String, Object> getMapForRv2(Rv rv) {
// ¿Hay algo que hacer?
if (rv == null) {
return null;
}
// diccionario <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", rv.getId());
hash.put("idClient", rv.getIdClient());
hash.put("idCreneau", rv.getIdCreneau());
// se devuelve el diccionario
return hash;
}
Los resultados obtenidos son los siguientes:
![]() |
o bien estos, si el número de la cita es incorrecto:
![]() |
2.12.16. El URL [/ajouterRv]
El URL [/ajouterRv] se procesa mediante el siguiente método del controlador [RdvMedecinsController]:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
// estado de la aplicación
if (messages != null) {
return new Reponse(-1, messages);
}
// se recuperan los valores enviados
String jour = post.getJour();
long idCreneau = post.getIdCreneau();
long idClient = post.getIdClient();
// se verifica la fecha
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(6, null);
}
// se recupera el horario
Reponse réponse = getCreneau(idCreneau);
if (réponse.getStatus() != 0) {
return réponse;
}
Creneau créneau = (Creneau) réponse.getData();
// se recupera el cliente
réponse = getClient(idClient);
if (réponse.getStatus() != 0) {
réponse.incrStatusBy(2);
return réponse;
}
Client client = (Client) réponse.getData();
// se agrega la cita
Rv rv = null;
try {
rv = application.ajouterRv(jourAgenda, créneau, client);
} catch (Exception e1) {
return new Reponse(5, Static.getErreursForException(e1));
}
// se devuelve la respuesta
return new Reponse(0, Static.getMapForRv(rv));
}
No hay nada aquí que no se haya visto antes. En la línea 41, se devuelve la cita que se agregó en la línea 36.
Los resultados obtenidos se ven así con el cliente [Advanced Rest Client]:
![]() |
o bien a esto si, por ejemplo, se ingresa un número de franja horaria que no existe:
![]() |
![]() |
2.12.17. El URL [/supprimerRv]
El URL [/supprimerRv] es procesado mediante el siguiente método del controlador [RdvMedecinsController]:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
// estado de la aplicación
if (messages != null) {
return new Reponse(-1, messages);
}
// se recuperan los valores enviados
long idRv = post.getIdRv();
// se recupera el valor de retorno
Reponse réponse = getRv(idRv);
if (réponse.getStatus() != 0) {
return réponse;
}
// se elimina el Rv
try {
application.supprimerRv(idRv);
} catch (Exception e1) {
return new Reponse(3, Static.getErreursForException(e1));
}
// ok
return new Reponse(0, null);
}
Los resultados obtenidos por son los siguientes:
![]() |
o bien estos, si el número de cita no existe:
![]() |
Ya terminamos con el controlador. Ahora veremos cómo configurar el proyecto.
2.12.18. Configuración del servicio web
![]() |
La clase de configuración [AppConfig] es la siguiente:
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class })
public class AppConfig {
}
- línea 9: nos ponemos en modo [AutoConfiguration] para que Spring Boot pueda configurar el proyecto según los archivos que encuentre en el Classpath del proyecto;
- línea 10: se indica que los componentes de Spring se busquen en el paquete [rdvmedecins.web] y sus subpaquetes. De esta manera se encontrarán los componentes:
- [@RestController RdvMedecinsController] en el paquete [rdvmedecins.web.controllers];
- [@Component ApplicationModel] en el paquete [rdvmedecins.web.models];
- línea 11: se importa la clase [DomainAndPersistenceConfig], que configura el proyecto [rdvmedecins-metier-dao] para tener acceso a los beans de este proyecto;
2.12.19. La clase ejecutable del servicio web
![]() |
La clase [Boot] es la siguiente:
package rdvmedecins.web.boot;
import org.springframework.boot.SpringApplication;
import rdvmedecins.web.config.AppConfig;
public class Boot {
public static void main(String[] args) {
SpringApplication.run(AppConfig.class, args);
}
}
En la línea 10, se ejecuta el método estático [SpringApplication.run] con la clase de configuración del proyecto [AppConfig] como primer parámetro. Este método llevará a cabo la autoconfiguración del proyecto, iniciará el servidor Tomcat integrado en las dependencias y desplegará en él el controlador [RdvMedecinsController].
Los registros de ejecución son los siguientes:
- línea 17: se inicia el servidor Tomcat;
- líneas 23-31: se inicializan las capas [métier, DAO, JPA];
- línea 34: se ha detectado el método que procesa el URL [/getRvMedecinJour/{idMedecin}/{jour}]. Este proceso de detección de los métodos del controlador se repite hasta la línea 44;
- línea 52: el servlet de Spring MVC [DispatcherServlet] está listo para responder a las solicitudes de los clientes web;
Ahora contamos con un servicio web operativo al que se puede acceder mediante un cliente web. A continuación, abordaremos la seguridad de este servicio: queremos que solo ciertas personas puedan administrar las citas de los médicos. Para ello, utilizaremos el marco Spring Security, una rama del ecosistema Spring.
2.13. Introducción a Spring Security
Volvemos a importar una guía de Spring siguiendo los pasos 1 a 3 que se indican a continuación:
![]() |
![]() |
El proyecto se compone de los siguientes elementos:
- en la carpeta [templates], se encuentran las páginas HTML del proyecto;
- [Application]: es la clase ejecutable del proyecto;
- [MvcConfig]: es la clase de configuración de Spring MVC;
- [WebSecurityConfig]: es la clase de configuración de Spring Security;
2.13.1. Configuración de Maven
El proyecto [3] es un proyecto de Maven. Analicemos su archivo [pom.xml] para conocer sus dependencias:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
- líneas 1-5: el proyecto es un proyecto de Spring Boot;
- líneas 8-11: dependencia del marco [Thymeleaf], que permite crear páginas HTML dinámicas. Este marco puede reemplazar las páginas JSP (Java Server Pages), que hasta hace poco eran el marco de vistas predeterminado de Spring MVC;
- líneas 12-15: dependencia del marco Spring Security;
2.13.2. Las vistas Thymeleaf
![]() |
La vista [home.html] es la siguiente:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click <a th:href="@{/hello}">here</a> to see a greeting.
</p>
</body>
</html>
- Los atributos [th:xx] son atributos de Thymeleaf. Thymeleaf los interpreta antes de que la página HTML se envíe al cliente. El cliente no los ve;
- línea 12: el atributo [th:href="@{/hello}"] generará el atributo [href] de la etiqueta <a>. El valor [@{/hello}] generará la ruta [<context>/hello], donde [context] es el contexto de la aplicación web;
El código HTML generado es el siguiente:
- línea 10: el contexto de la aplicación es la raíz /;
La vista [hello.html] es la siguiente:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
</body>
</html>
- línea 9: El atributo [th:inline="text"] generará el texto de la etiqueta <h1>. Este texto contiene una expresión $ que debe evaluarse. El elemento [[${#httpServletRequest.remoteUser}]] es el valor del atributo [RemoteUser] de la consulta HTTP actual. Es el nombre del usuario que está conectado;
- línea 10: un formulario HTML. El atributo [th:action="@{/logout}"] generará el atributo [action] de la etiqueta [form]. El valor [@{/logout}] generará la ruta [<context>/logout], donde [context] es el contexto de la aplicación web;
El código HTML generado es el siguiente:
- línea 8: la traducción de «Hello [[${#httpServletRequest.remoteUser}]]!»;
- línea 9: la traducción de @{/logout};
- línea 11: un campo oculto llamado (atributo name) _csrf;
La última vista [login.html] es la siguiente:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<div th:if="${param.error}">Invalid username and password.</div>
<div th:if="${param.logout}">You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<label> User Name : <input type="text" name="username" />
</label>
</div>
<div>
<label> Password: <input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Sign In" />
</div>
</form>
</body>
</html>
- línea 9: el atributo [th:if="${param.error}"] hace que la etiqueta <div> solo se genere si el URL que muestra la página de inicio de sesión contiene el parámetro [error] (http://context/login?error);
- línea 10: el atributo [th:if="${param.logout}"] hace que la etiqueta <div> solo se genere si el URL, que muestra la página de inicio de sesión, contiene el parámetro [logout] (http://context/login?logout);
- líneas 11-23: un formulario HTML;
- línea 11: el formulario se enviará al URL [<context>/login], donde <context> es el contexto de la aplicación web;
- línea 13: un campo de entrada llamado [username];
- línea 17: un campo de entrada llamado [password];
El código HTML generado es el siguiente:
Cabe destacar que, en la línea 21, Thymeleaf ha agregado un campo oculto llamado [_csrf].
2.13.3. Configuración de Spring MVC
![]() |
La clase [MvcConfig] configura el marco Spring MVC:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
- línea 7: la anotación [@Configuration] convierte a la clase [MvcConfig] en una clase de configuración;
- línea 8: la clase [MvcConfig] extiende la clase [WebMvcConfigurerAdapter] para redefinir algunos de sus métodos;
- línea 10: redefinición de un método de la clase padre;
- líneas 11-16: el método [addViewControllers] permite asociar URL a vistas HTML. Se establecen las siguientes asociaciones:
vista | |
/templates/home.html | |
/templates/hello.html | |
/templates/login.html |
El sufijo [html] y la carpeta [templates] son los valores predeterminados que utiliza Thymeleaf. Se pueden modificar mediante la configuración. La carpeta [templates] debe estar en la raíz del Classpath del proyecto:
![]() |
Por encima de [1], las carpetas [main] y [resources] son ambas carpetas de origen (source folders). Esto implica que su contenido estará en la raíz del Classpath del proyecto. Por lo tanto, en [2], las carpetas [hello] y [templates] estarán en la raíz del Classpath.
2.13.4. Configuración de Spring Security
![]() |
La clase [WebSecurityConfig] configura el marco Spring Security:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
- línea 9: la anotación [@Configuration] convierte a la clase [WebSecurityConfig] en una clase de configuración;
- línea 10: la anotación [@EnableWebSecurity] convierte a la clase [WebSecurityConfig] en una clase de configuración de Spring Security;
- línea 11: la clase [WebSecurity] extiende la clase [WebSecurityConfigurerAdapter] para redefinir algunos de sus métodos;
- línea 12: redefinición de un método de la clase padre;
- líneas 13-16: el método [configure(HttpSecurity http)] se redefine para establecer los derechos de acceso a los distintos URL de la aplicación;
- línea 14: el método [http.authorizeRequests()] permite asociar URL a derechos de acceso. En él se realizan las siguientes asociaciones:
regla | código | |
acceso sin autenticación | | |
Acceso solo con autenticación |
- línea 15: define el método de autenticación. La autenticación se realiza a través de un formulario de URL [/login] accesible para todos [http.formLogin().loginPage("/login").permitAll()]. La desconexión (logout) también es accesible para todos.
- líneas 19-21: redefinen el método [configure(AuthenticationManagerBuilder auth)] que administra a los usuarios;
- línea 20: la autenticación se realiza con usuarios definidos de forma «estática» [auth.inMemoryAuthentication()]. Aquí se define un usuario con el nombre de usuario [user], la contraseña [password] y el rol [USER]. Se pueden otorgar los mismos derechos a los usuarios que tengan el mismo rol;
2.13.5. Clase ejecutable
![]() |
La clase [Application] es la siguiente:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {
public static void main(String[] args) throws Throwable {
SpringApplication.run(Application.class, args);
}
}
- línea 8: la anotación [@EnableAutoConfiguration] le pide a Spring Boot (línea 3) que realice la configuración que el desarrollador no habrá hecho explícitamente;
- línea 9: convierte la clase [Application] en una clase de configuración de Spring;
- línea 10: solicita que se escanee la carpeta de la clase [Application] para buscar componentes de Spring. De esta manera, se detectarán las dos clases [MvcConfig] y [WebSecurityConfig], ya que cuentan con la anotación [@Configuration];
- línea 13: el método [main] de la clase ejecutable;
- línea 14: se ejecuta el método estático [SpringApplication.run] con la clase de configuración [Application] como parámetro. Ya nos hemos encontrado con este proceso y sabemos que se iniciará el servidor Tomcat integrado en las dependencias de Maven del proyecto y que el proyecto se implementará en él. Hemos visto que cuatro URL eran administradas por [/, /home, /login, /hello] y que algunas estaban protegidas por derechos de acceso.
2.13.6. Pruebas de la aplicación
Comencemos por solicitar el URL [/], que es uno de los cuatro URL aceptados. Está asociado a la vista [/templates/home.html]:
![]() |
La URL solicitada, [/], es de acceso público. Por eso la obtuvimos. El enlace [here] es el siguiente:
El URL [/hello] se solicitará al hacer clic en el enlace. Este está protegido:
regla | código | |
acceso sin autenticación | | |
Acceso solo con autenticación |
Es necesario estar autenticado para obtenerlo. Spring Security redirigirá entonces el navegador del cliente a la página de autenticación. Según la configuración vista, se trata de la página de URL [/login]. Esta es accesible para todos:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Así que obtenemos [1]:
![]() |
El código fuente de la página obtenida es el siguiente:
- En la línea 7, aparece un campo oculto que no se encuentra en la página original [login.html]. Fue Thymeleaf quien lo agregó. Este código, denominado CSRF (Cross Site Request Forgery), tiene como objetivo eliminar una vulnerabilidad de seguridad. Este token debe enviarse a Spring Security junto con la autenticación para que esta sea aceptada;
Recordemos que Spring Security solo reconoce al usuario «user/password». Si ingresamos cualquier otra cosa en [2], obtenemos la misma página con un mensaje de error en [3]. Spring Security ha redirigido el navegador a URL [http://localhost:8080/login?error]. La presencia del parámetro [error] ha activado la visualización de la etiqueta:
<div th:if="${param.error}">Invalid username and password.</div>
Ahora, ingresemos los valores esperados para usuario/contraseña [4]:
![]() |
- en [4], nos identificamos;
- en [5], Spring Security nos redirige a URL [/hello] porque era URL lo que solicitábamos cuando fuimos redirigidos a la página de inicio de sesión. La identidad del usuario se mostró en la siguiente línea de [hello.html]:
La página [5] muestra el siguiente formulario:
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
Al hacer clic en el botón [Sign Out], se generará un POST en el URL [/logout]. Este, al igual que el URL y el [/login], es accesible para todos:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
En nuestra asociación URL / vistas, no hemos definido nada para URL ni [/logout]. ¿Qué va a pasar? Probemos:
![]() |
- en [6], hacemos clic en el botón [Sign Out];
- en [7], vemos que nos han redirigido a URL [http://localhost:8080/login?logout]. Fue Spring Security quien solicitó esta redirección. La presencia del parámetro [logout] en URL hizo que se mostrara la siguiente línea en la vista:
<div th:if="${param.logout}">You have been logged out.</div>
2.13.7. Conclusión
En el ejemplo anterior, podríamos haber escrito primero la aplicación web y luego haberla protegido. Spring Security no es intrusivo. Se puede implementar la seguridad en una aplicación web ya escrita. Además, hemos descubierto lo siguiente:
- es posible definir una página de autenticación;
- la autenticación debe ir acompañada del token CSRF emitido por Spring Security;
- si la autenticación falla, se nos redirige a la página de autenticación con un parámetro adicional llamado «error» en el token URL;
- si la autenticación es exitosa, se nos redirige a la página solicitada en el momento en que se realizó la autenticación. Si se solicita directamente la página de autenticación sin pasar por una página intermedia, entonces Spring Security nos redirige a la URL [/] (este caso no se ha presentado);
- nos desconectamos al solicitar la página URL [/logout] con un POST. Spring Security nos redirige entonces a la página de autenticación con el parámetro «logout» en el URL;
Todas estas conclusiones se basan en el comportamiento predeterminado de Spring Security. Este comportamiento se puede modificar mediante la configuración, redefiniendo ciertos métodos de la clase [WebSecurityConfigurerAdapter].
El tutorial anterior no nos servirá de mucho en lo que sigue. De hecho, vamos a utilizar:
- una base de datos para almacenar a los usuarios, sus contraseñas y sus roles;
- una autenticación mediante encabezado HTTP;
Hay muy pocos tutoriales sobre lo que queremos hacer aquí. La solución que se propondrá es una combinación de códigos encontrados aquí y allá.
2.14. Configuración de la seguridad en el servicio web de citas
2.14.1. La base de datos
La base de datos [rdvmedecins] se modifica para incluir a los usuarios, sus contraseñas y sus roles. Se añaden tres nuevas tablas:

Tabla [USERS]: los usuarios
- ID: clave primaria;
- VERSION: columna de control de versiones de la fila;
- IDENTITY: una identidad descriptiva del usuario;
- LOGIN: el nombre de usuario;
- PASSWORD: su contraseña;
En la tabla USERS, las contraseñas no se almacenan a texto plano:
![]() |
El algoritmo que encripta las contraseñas es el algoritmo BCRYPT.
Tabla [ROLES]: los roles
- ID: clave primaria;
- VERSION: columna de control de versiones de la fila;
- NAME: nombre del rol. Por defecto, Spring Security espera nombres con el formato ROLE_XX, por ejemplo, ROLE_ADMIN o ROLE_GUEST;
![]() |
Tabla [USERS_ROLES]: tabla de unión USERS / ROLES
Un usuario puede tener varios roles, y un rol puede agrupar a varios usuarios. Existe una relación de varios a varios representada por la tabla [USERS_ROLES].
- ID: clave primaria;
- VERSION: columna de control de versiones de la fila;
- USER_ID: identificador de un usuario;
- ROLE_ID: identificador de un rol;
![]() |
Dado que estamos modificando la base de datos, se deben modificar todas las capas del proyecto [métier, DAO, JPA]:
![]() |
2.14.2. El nuevo proyecto de Eclipse [métier, DAO, JPA]
Duplicamos el proyecto inicial [rdvmedecins-metier-dao] en [rdvmedecins-metier-dao-v2]:
![]() |
- en [1]: el nuevo proyecto;
- en [2]: las modificaciones derivadas de la incorporación de medidas de seguridad se han reunido en un único paquete [rdvmedecins.security]. Estos nuevos elementos pertenecen a las capas [JPA] y [DAO], pero por simplicidad los he reunido en un mismo paquete.
2.14.3. Las nuevas entidades [JPA]
![]() |
La capa JPA define tres nuevas entidades:
![]() |
La clase [User] es la imagen de la tabla [USERS]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {
private static final long serialVersionUID = 1L;
// características
private String identity;
private String login;
private String password;
// fabricante
public User() {
}
public User(String identity, String login, String password) {
this.identity = identity;
this.login = login;
this.password = password;
}
// identidad
@Override
public String toString() {
return String.format("User[%s,%s,%s]", identity, login, password);
}
// getters y setters
....
}
- línea 9: la clase extiende la clase [AbstractEntity] que ya se utiliza para las demás entidades;
- líneas 13-15: no se especifica ningún nombre para las columnas porque tienen el mismo nombre que los campos asociados a ellas;
La clase [Role] es el reflejo de la tabla [ROLES]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {
private static final long serialVersionUID = 1L;
// propiedades
private String name;
// fabricantes
public Role() {
}
public Role(String name) {
this.name = name;
}
// identidad
@Override
public String toString() {
return String.format("Role[%s]", name);
}
// getters y setters
...
}
La clase [UserRole] es la imagen de la tabla [USERS_ROLES]:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {
private static final long serialVersionUID = 1L;
// un UserRole hace referencia a un User
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
// un UserRole hace referencia a un rol
@ManyToOne
@JoinColumn(name = "ROLE_ID")
private Role role;
// métodos getter y setter
...
}
- líneas 15-17: representan la clave externa de la tabla [USERS_ROLES] hacia la tabla [USERS];
- líneas 19-21: definen la clave externa de la tabla [USERS_ROLES] hacia la tabla [ROLES];
2.14.4. Modificaciones en la capa [DAO]
![]() |
La capa [DAO] se amplía con tres nuevas [Repository]:
![]() |
La interfaz [UserRepository] administra el acceso a las entidades [User]:
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Role;
import rdvmedecins.entities.User;
public interface UserRepository extends CrudRepository<User, Long> {
// lista de roles de un usuario identificado por su ID
@Query("select ur.role from UserRole ur where ur.user.id=?1")
Iterable<Role> getRoles(long id);
// lista de roles de un usuario identificado por su nombre de usuario y contraseña
@Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
Iterable<Role> getRoles(String login, String password);
// búsqueda de un usuario por su nombre de usuario
User findUserByLogin(String login);
}
- línea 9: la interfaz [UserRepository] extiende la interfaz [CrudRepository] de Spring Data (línea 4);
- líneas 12-13: el método [getRoles(User user)] permite obtener todos los roles de un usuario identificado por su [id]
- líneas 16-17: lo mismo, pero para un usuario identificado por su nombre de usuario y contraseña;
La interfaz [RoleRepository] administra el acceso a las entidades [Role]:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface RoleRepository extends CrudRepository<Role, Long> {
// búsqueda de un rol por su nombre
Role findRoleByName(String name);
}
- línea 5: la interfaz [RoleRepository] extiende la interfaz [CrudRepository];
- línea 8: se puede buscar un rol por su nombre;
La interfaz [userRoleRepository] administra el acceso a las entidades [UserRole]:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
}
- línea 5: la interfaz [UserRoleRepository] simplemente extiende la interfaz [CrudRepository] sin agregarle nuevos métodos;
2.14.5. Las clases de gestión de usuarios y roles
![]() |
Spring Security exige la creación de una clase que implemente la siguiente interfaz [UsersDetail]:
![]() |
Esta interfaz es implementada aquí por la clase [AppUserDetails]:
package rdvmedecins.security;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class AppUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
// propiedades
private User user;
private UserRepository userRepository;
// constructores
public AppUserDetails() {
}
public AppUserDetails(User user, UserRepository userRepository) {
this.user = user;
this.userRepository = userRepository;
}
// -------------------------interfaz
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : userRepository.getRoles(user.getId())) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getLogin();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// getters y setters
...
}
- línea 10: la clase [AppUserDetails] implementa la interfaz [UserDetails];
- líneas 15-16: la clase encapsula a un usuario (línea 15) y el repositorio que permite obtener los detalles de dicho usuario (línea 16);
- líneas 22-25: el constructor que instancia la clase con un usuario y su repositorio;
- líneas 28-35: implementación del método [getAuthorities] de la interfaz [UserDetails]. Debe construir una colección de elementos de tipo [GrantedAuthority] o derivado. Aquí utilizamos el tipo derivado [SimpleGrantedAuthority] (línea 32), que encapsula el nombre de uno de los roles del usuario de la línea 15;
- líneas 31-33: se recorre la lista de roles del usuario de la línea 15 para construir una lista de elementos de tipo [SimpleGrantedAuthority];
- líneas 38-40: implementan el método [getPassword] de la interfaz [UserDetails]. Se devuelve la contraseña del usuario de la línea 15;
- líneas 38-40: implementan el método [getUserName] de la interfaz [UserDetails]. Se devuelve el nombre de usuario de la línea 15;
- líneas 47-50: la cuenta del usuario nunca caduca;
- líneas 52-55: la cuenta del usuario nunca se bloquea;
- líneas 57-60: las credenciales del usuario nunca caducan;
- líneas 62-65: la cuenta del usuario siempre está activa;
Spring Security también exige la existencia de una clase que implemente la interfaz [AppUserDetailsService]:
![]() |
Esta interfaz es implementada por la siguiente clase [AppUserDetails]:
package rdvmedecins.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class AppUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
// se busca al usuario por su nombre de usuario
User user = userRepository.findUserByLogin(login);
// ¿Encontrado?
if (user == null) {
throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
}
// se muestran los detalles del usuario
return new AppUserDetails(user, userRepository);
}
}
- línea 9: la clase será un componente de Spring, por lo que estará disponible en su contexto;
- líneas 12-13: el componente [UserRepository] se inyectará aquí;
- líneas 16-25: implementación del método [loadUserByUsername] de la interfaz [UserDetailsService] (línea 10). El parámetro es el nombre de usuario;
- línea 18: se busca al usuario mediante su nombre de usuario;
- líneas 20-22: si no se encuentra, se lanza una excepción;
- línea 24: se crea y se genera un objeto [AppUserDetails]. Este es, efectivamente, de tipo [UserDetails] (línea 16);
2.14.6. Pruebas de la capa [DAO]
![]() |
En primer lugar, creamos una clase ejecutable [CreateUser] capaz de crear un usuario con un rol:
package rdvmedecins.security;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.security.Role;
import rdvmedecins.security.RoleRepository;
import rdvmedecins.security.User;
import rdvmedecins.security.UserRepository;
import rdvmedecins.security.UserRole;
import rdvmedecins.security.UserRoleRepository;
public class CreateUser {
public static void main(String[] args) {
// sintaxis: nombre de usuario contraseña roleName
// Se necesitan tres parámetros
if (args.length != 3) {
System.out.println("Syntaxe : [pg] user password role");
System.exit(0);
}
// se recuperan los parámetros
String login = args[0];
String password = args[1];
String roleName = String.format("ROLE_%s", args[2].toUpperCase());
// contexto de Spring
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
UserRepository userRepository = context.getBean(UserRepository.class);
RoleRepository roleRepository = context.getBean(RoleRepository.class);
UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
// ¿Ya existe el rol?
Role role = roleRepository.findRoleByName(roleName);
// si no existe, lo creamos
if (role == null) {
role = roleRepository.save(new Role(roleName));
}
// ¿Ya existe el usuario?
User user = userRepository.findUserByLogin(login);
// si no existe, lo creamos
if (user == null) {
// Se aplica el hash a la contraseña con bcrypt
String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
// se guarda el usuario
user = userRepository.save(new User(login, login, crypt));
// se crea la relación con el rol
userRoleRepository.save(new UserRole(user, role));
} else {
// el usuario ya existe: ¿tiene el rol solicitado?
boolean trouvé = false;
for (Role r : userRepository.getRoles(user.getId())) {
if (r.getName().equals(roleName)) {
trouvé = true;
break;
}
}
// si no se encuentra, se crea la relación con el rol
if (!trouvé) {
userRoleRepository.save(new UserRole(user, role));
}
}
// cierre del contexto de Spring
context.close();
}
}
- línea 17: la clase espera tres argumentos que definen a un usuario: su nombre de usuario, su contraseña y su rol;
- líneas 25-27: se recuperan los tres parámetros;
- línea 29: el contexto de Spring se construye a partir de la clase de configuración [DomainAndPersistenceConfig]. Esta clase ya existía en el proyecto anterior. Debe modificarse de la siguiente manera:
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
- línea 1: se debe indicar que ahora hay componentes [Repository] en el paquete [rdvmedecins.security];
- línea 4: hay que indicar que ahora hay entidades JPA en el paquete [rdvmedecins.security];
Volvamos al código para crear un usuario:
- líneas 30-32: recuperamos las referencias de los tres [Repository] que nos pueden servir para crear el usuario;
- línea 34: se verifica si el rol ya existe;
- líneas 36-38: si no es así, lo creamos en la base de datos. Tendrá un nombre del tipo [ROLE_XX];
- línea 40: se verifica si el nombre de usuario ya existe;
- líneas 42-49: si el nombre de usuario no existe, se crea en la base de datos;
- línea 44: se encripta la contraseña. Aquí se utiliza la clase [BCrypt] de Spring Security (línea 4). Por lo tanto, se necesitan los archivos de este marco de trabajo. El archivo [pom.xml] incluye una nueva dependencia:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- línea 46: el usuario se guarda en la base de datos;
- línea 48: así como la relación que lo vincula a su rol;
- líneas 51-57: en caso de que el usuario ya exista, se verifica si entre sus roles ya se encuentra el rol que se le quiere asignar;
- líneas 59-61: si no se ha encontrado el rol buscado, se crea una fila en la tabla [USERS_ROLES] para vincular al usuario con su rol;
- no se ha previsto ninguna protección contra posibles excepciones. Se trata de una clase de apoyo para crear rápidamente un usuario con un rol.
Al ejecutar la clase con los argumentos [x x guest], se obtienen en la base de datos los siguientes resultados:
Tabla [USERS]
![]() |
Tabla [ROLES]
![]() |
Tabla [USERS_ROLES]
![]() |
Consideremos ahora la segunda clase [UsersTest], que es una prueba de JUnit:
![]() |
package rdvmedecins.security;
import java.util.List;
import org.junit.Assert;
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.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rdvmedecins.config.DomainAndPersistenceConfig;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {
@Autowired
private UserRepository userRepository;
@Autowired
private AppUserDetailsService appUserDetailsService;
@Test
public void findAllUsersWithTheirRoles() {
Iterable<User> users = userRepository.findAll();
for (User user : users) {
System.out.println(user);
display("Roles :", userRepository.getRoles(user.getId()));
}
}
@Test
public void findUserByLogin() {
// se recupera el usuario [admin]
User user = userRepository.findUserByLogin("admin");
// se verifica que su contraseña sea [admin]
Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
// se verifica el rol de admin / admin
List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
Assert.assertEquals(1L, roles.size());
Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
}
@Test
public void loadUserByUsername() {
// se recupera el usuario [admin]
AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
// se verifica que su contraseña sea [admin]
Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
// se verifica el rol de admin / admin
@SuppressWarnings("unchecked")
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
Assert.assertEquals(1L, authorities.size());
Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
}
// método de utilidad: muestra los elementos de una colección
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- líneas 27-34: prueba visual. Se muestran todos los usuarios con sus roles;
- líneas 36-46: se verifica que el usuario [admin] tenga la contraseña [admin] y el rol [ROLE_ADMIN] utilizando el repositorio [UserRepository];
- línea 41: [admin] es la contraseña en texto plano. En la base de datos, está cifrada según el algoritmo BCrypt. El método [ BCrypt.checkpw] permite verificar que la contraseña en texto claro, una vez cifrada, sea efectivamente igual a la que figura en la base de datos;
- líneas 48-59: se verifica que el usuario [admin] tenga la contraseña [admin] y el rol [ROLE_ADMIN] utilizando el servicio [appUserDetailsService];
La ejecución de las pruebas se realiza con éxito con los siguientes registros:
2.14.7. Conclusión provisional
Se lograron agregar las clases necesarias para Spring Security con pocas modificaciones al proyecto original. Recordemos cuáles son:
- agregación de una dependencia de Spring Security en el archivo [pom.xml];
- creación de tres tablas adicionales en la base de datos;
- creación de las entidades JPA y de los componentes de Spring en el paquete [rdvmedecins.security];
Este caso tan favorable se debe a que las tres tablas agregadas a la base de datos son independientes de las tablas existentes. Incluso se podrían haber colocado en una base de datos separada. Esto fue posible porque se decidió que un usuario tenía una existencia independiente de los médicos y los clientes. Si estos últimos hubieran sido usuarios potenciales, habría sido necesario crear vínculos entre la tabla [USERS] y las tablas [MEDECINS] y [CLIENTS]. Esto habría tenido un impacto significativo en el proyecto existente.
2.14.8. El proyecto Eclipse de la capa [web]
![]() |
El proyecto [rdvmedecins-webapi] anterior se duplica en el proyecto [rdvmedecins-webapi-v2] [1]:
![]() |
Las únicas modificaciones deben realizarse en el paquete [rdvmedecins.web.config], donde hay que configurar Spring Security. Ya nos hemos encontrado con una clase de configuración de Spring Security:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
Seguiremos el mismo procedimiento:
- línea 11: definir una clase que extienda la clase [WebSecurityConfigurerAdapter];
- línea 13: definir un método [configure(HttpSecurity http)] que establezca los derechos de acceso a los distintos URL del servicio web;
- línea 19: definir un método [configure(AuthenticationManagerBuilder auth)] que defina a los usuarios y sus roles;
La configuración de Spring Security se realiza mediante la clase [SecurityConfig]:
package rdvmedecins.web.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import rdvmedecins.security.AppUserDetailsService;
@EnableAutoConfiguration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AppUserDetailsService appUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder registry) throws Exception {
// la autenticación la realiza el bean [appUserDetailsService]
// la contraseña se cifra mediante el algoritmo de hash Bcrypt
registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF
http.csrf().disable();
// la contraseña se transmite mediante el encabezado Authorization: Basic xxxx
http.httpBasic();
// solo el rol ADMIN puede utilizar la aplicación
http.authorizeRequests() //
.antMatchers("/", "/**") // todos los URL
.hasRole("ADMIN");
}
}
- líneas 14-15: se han tomado las anotaciones del ejemplo;
- líneas 17-18: se inyecta la clase [AppUserDetails], que da acceso a los usuarios de la aplicación;
- líneas 20-21: el método [configure(HttpSecurity http)] define a los usuarios y sus roles. Recibe como parámetro un tipo [AuthenticationManagerBuilder]. Este parámetro se complementa con dos datos:
- una referencia al servicio [appUserDetailsService] de la línea 18, que otorga acceso a los usuarios registrados. Cabe señalar aquí que no se indica que estén registrados en una base de datos. Por lo tanto, podrían estar en una caché, ser proporcionados por un servicio web, etc.
- el tipo de cifrado utilizado para la contraseña. Recordemos aquí que hemos utilizado el algoritmo BCrypt;
- líneas 27-40: el método [configure(HttpSecurity http)] define los derechos de acceso a los URL del servicio web;
- línea 30: en el proyecto de introducción vimos que, por defecto, Spring Security administraba un token CSRF (Cross Site Request Forgery) que el usuario que quisiera autenticarse debía enviar de vuelta al servidor. Aquí este mecanismo está desactivado;
- línea 32: se activa el modo de autenticación mediante el encabezado HTTP. El cliente deberá enviar el siguiente encabezado HTTP:
donde «código» es la codificación de la cadena «login:contraseña» mediante el algoritmo Base64. Por ejemplo, la codificación Base64 de la cadena admin:admin es YWRtaW46YWRtaW4=. Por lo tanto, el usuario con nombre de usuario [admin] y contraseña [admin] enviará el siguiente encabezado HTTP para autenticarse:
- líneas 34-36: indican que todos los URL del servicio web son accesibles para los usuarios que tengan el rol [ROLE_ADMIN]. Esto significa que un usuario que no tenga este rol no puede acceder al servicio web;
La clase [AppConfig], que configura toda la aplicación, cambia de la siguiente manera:
![]() |
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class })
public class AppConfig {
}
- La modificación se realiza en la línea 11: se indica que ahora hay dos archivos de configuración que se deben utilizar: [DomainAndPersistenceConfig] y [SecurityConfig].
2.14.9. Pruebas del servicio web
Vamos a probar el servicio web con el cliente Chrome [Advanced Rest Client]. Necesitaremos especificar el encabezado de autenticación HTTP:
donde [code] es el código Base64 de la cadena [login:password]. Para generar este código, se puede utilizar el siguiente programa:
![]() |
package rdvmedecins.helpers;
import org.springframework.security.crypto.codec.Base64;
public class Base64Encoder {
public static void main(String[] args) {
// se esperan dos argumentos: nombre de usuario y contraseña
if (args.length != 2) {
System.out.println("Syntaxe : login password");
System.exit(0);
}
// se recuperan los dos argumentos
String chaîne = String.format("%s:%s", args[0], args[1]);
// se codifica la cadena
byte[] data = Base64.encode(chaîne.getBytes());
// se muestra su codificación Base64
System.out.println(new String(data));
}
}
Si ejecutamos este programa con los dos argumentos [admin admin]:
![]() |
obtenemos el siguiente resultado:
Ahora que sabemos cómo generar el encabezado de autenticación HTTP, iniciamos el servicio web, que ahora está protegido. Luego, con el cliente Chrome [Advanced Rest Client], solicitamos la lista de todos los médicos:
![]() |
- en [1], solicitamos el URL de los médicos;
- en [2], con un método GET;
- en [3], proporcionamos el encabezado HTTP de la autenticación. El código [YWRtaW46YWRtaW4=] es la codificación Base64 de la cadena [admin:admin];
- en [4], enviamos el comando HTTP;
La respuesta del servidor es la siguiente:
![]() |
- en [1], el encabezado de autenticación HTTP;
- en [2], el servidor devuelve una respuesta JSON;
- en [3], la lista de médicos.
Ahora intentemos una solicitud HTTP con un encabezado de autenticación incorrecto. La respuesta es entonces la siguiente:
![]() |
- en [1] y [3]: el encabezado de autenticación HTTP;
- en [2]: la respuesta del servicio web;
Ahora probemos con el usuario user / user. Existe, pero no tiene acceso al servicio web. Si ejecutamos el programa de codificación Base64 con los dos argumentos [user user]:
![]() |
obtenemos el siguiente resultado:
![]() |
- en [1] y [3]: el encabezado de autenticación HTTP;
- en [2]: la respuesta del servicio web. Es diferente de la anterior, que era [401 Unauthorized]. En esta ocasión, el usuario se autenticó correctamente, pero no cuenta con los permisos suficientes para acceder a URL;
2.15. Conclusion
Recordemos la arquitectura general de nuestra aplicación cliente/servidor:
![]() |
Ahora ya está operativo un servicio web seguro. Veremos que habrá que modificarlo debido a problemas que surgirán al desarrollar el cliente Angular JS. Pero esperaremos a que surja el problema para resolverlo. Ahora vamos a desarrollar el cliente Angular que ofrecerá una interfaz web para administrar las citas de los médicos.

















































































































































