Contenido
En la primera parte de este post creamos la interfaz de un microservicio o servicio web REST usando Spring Boot y en la segunda parte agregamos la lógica de negocio y el acceso a datos, sin embargo el servicio tiene problemas de diseño que causan acoplamientos negativos, dificultan su evolución y las pruebas automatizadas. En esta última entrega corregiremos esos problemas y haremos uso de varias características avanzadas de Spring Boot y otras librerías.
Desacoplando a los clientes
La interfaz de servicio (ContactsApi) retorna objetos de tipo Contact que hemos marcado como entidades con la anotación @Entity; en otras palabras: nuestro servicio web está exponiendo a sus clientes, el modelo de datos tal como es. Esto implica que si hacemos un cambio en la estructura de base de datos, el cambio automáticamente se verá en la interfaz de servicio y afectará a los clientes del servicio web, por lo que decimos que existe un fuerte acoplamiento negativo entre los clientes y la interfaz del servicio web. Este es un antipatrón también conocido como Modelo del Dominio como REST. La forma de solucionarlo, ilustrada en el siguiente diagrama, es crear objetos independientes para la interfaz de servicio y objetos (entidades) diferentes para el modelo de datos; de esta manera podremos hacer cambios en el modelo de datos sin afectar obligatoriamente a los clientes.Lo malo de este enfoque es que tendremos que hacer un mapeo entre los campos de la entidad y los campos de los objetos que usemos en la interfaz de servicio web. Hay varias formas en la que podemos hacer este mapeo:
- Manualmente, escribiendo una clase auxiliar que haga el mapeo para que sea reutilizable
- Como parte de la lógica de los POJOs/DTOs de la capa de interfaz
- Utilizando una librería de mapeo de objetos como Dozer, ModelMapper o MapStruct .
Como este última parte del tutorial es más avanzada, usaremos Dozer y si quieres detalles, puedes solicitar un tutorial de Dozer en los comentarios.
El modelo de datos del API
Crea las clases ContactRequest y ContactResponse. Estas clases serán los POJOs que el servicio web recibirá y entregará a sus clientes, en lugar de la entidad Contact, por lo que su estructura es muy similar y puedes verla en los fuentes que están en nuestro repositorio en GitHub. Al evitar que la interfaz del servicio web entregue a sus clientes cualquiera de nuestras entidades, ya los hemos desacoplado de nuestro modelo de datos del servicio que es diferente al modelo de datos del API.
Este cambio implica que debemos cambiar la interfaz del servicio web (Contacts) para que reciba y entregue instancias de estas nuevas clases, así:
public ContactResponse updateOrSave(@RequestBody ContactRequest contactRequest)
Mapeando entre modelos de datos
También debemos agregar los mapeos correspondientes entre los DTO que se intercambian con el cliente (ContactResponse y ContactRequest) y las entidades (Contact) que recibe y entiende nuestra clase de lógica de negocio (ContactService). Para esto debemos agregar la librería Dozer al archivo pom.xml y configurar un bean de Dozer en una clase de configuración:
<dependency> <groupId>net.sf.dozer</groupId> <artifactId>dozer</artifactId> <version>5.5.1</version> </dependency>
@Configuration // (1) public class DozerMapper { @Bean // (2) public Mapper beanMapper() { return new DozerBeanMapper(); // (3) } }
- La anotación @Configuration indica a Spring que esta clase tiene métodos anotados con @Bean que puede procesar en tiempo de ejecución para producir beans que pueden ser inyectados por otros beans
- La anotación @Bean en un método indica que el valor que retorne el método quedará disponible como un bean en el contexto de Spring,, listo para ser inyectado en otro bean
- Creamos una instancia de DozerBeanMapper que podremos inyectar en nuestro RestController u otro bean de Spring. Por defecto, el nombre del nuevo bean será el mismo nombre del método que lo produce. El nombre se puede personalizar con el parámetro name.
El mapper de Dozer intentará asignar los valores de las propiedades de un bean a las propiedades del mismo nombre en otro bean encargándose automáticamente de la conversión de tipos. Dozer también permite mapeos avanzados o personalizados si los nombres de las propiedades no son los mismos (deja un comentario si deseas un tutorial sobre Dozer).
En nuestro caso las propiedades del DTO y del Entity se llaman igual, así que no requerimos especificar mapeos explícitamente y nuestra clase de interfaz (ContactsApi) queda así:
// Inyecta mapper de Dozer @Autowired Mapper mapper; @RequestMapping(value="/contact", method=RequestMethod.POST) public ContactResponse updateOrSave(@RequestBody ContactRequest contactRequest){ // Mapeo request dto ==&amp;amp;amp;amp;gt; entity Contact contact = mapper.map(contactRequest, Contact.class); // Invoca lógica de negocio Contact updatedContact = contactService.save(contact); // Mapeo entity ==&amp;amp;amp;amp;gt; response dto ContactResponse contactResponse = mapper.map(updatedContact, ContactResponse.class); return contactResponse; }
Este código podría ser más breve pero deseo resaltar cada paso de manera independiente. Con esto hemos desacoplado los clientes de nuestro servicio de nuestro modelo de datos permitiendo su evolución independiente. Veamos ahora otras características de Spring Boot que facilitan el desarrollo de microservicios.
Validación automática de datos de entrada
Probablemente recuerdas de otros proyectos, algún controller con muchas instrucciones if para verificar que los datos de entrada no sean nulos, tengan un valor numérico en un rango específico, tengan una longitud de cadena determinada u otras validaciones. Con Spring y el API de JavaEE en el paquete javax.validation, no sólo podemos reducir este código sino además centralizarlo y simplificar el proceso.
Asumamos que requerimos las siguientes validaciones para el contacto que queremos crear:
- El nombre del contacto es requerido (no puede ser nulo)
- El nombre del contacto debe tener por lo menos 2 caracteres de longitud y máximo 30 caracteres
- El número de teléfono empieza con el símbolo «+» seguido de por lo menos un dígito y únicamente admite una secuencia de dígitos entre el 0 y el 9, sin espacios ni separadores
Estas restricciones se agregan mediante anotaciones a los campos de la clase que queremos verificar, en nuestro caso, ContactRequest, usando anotaciones del paquete javax.validation.constraint, el cual te recomendamos examinar para ver más validaciones útiles:
@NotNull(message="El nombre es requerido") @Size(min=2, max=30, message="El nombre debe tener entre {min} y {max} caracteres") private String firstName; @Pattern(regexp="^\\+[0-9]*$", message="El número de telefono sólo puede tener dígitos iniciando con el símbolo +")
La validación automática la implementas agregando la anotación @Valid junto al parámetro que quieras validar, siempre y cuando la Clase respectiva tenga las anotaciones que acabamos de ver. Esto lo podemos hacer en el RestController:
public ContactResponse updateOrSave(@RequestBody @Valid ContactRequest contactRequest)
Este es un fragmento de una respuesta a una petición no válida:
{... "defaultMessage": "El nombre debe tener entre 2 y 30 caracteres", "objectName": "contactRequest", "field": "firstName", "rejectedValue": "", "bindingFailure": false, "code": "Size" ...}
Conectando con una base de datos externas
Hasta este momento, todos los registros que guardamos en base de datos mediante las peticiones POST han sido guardados en una base de datos en memoria que se borra cuando se finaliza la aplicación. Aunque esto es muy útil para las pruebas unitarias y durante la etapa de desarrollo… de poco sirve para una aplicación en producción, por eso vamos a ver como conectarse a una base de datos externa… manteniendo también la opción en memoria! Lo mejor de ambos mundos.
Perfiles y Ambientes
Una de mis funcionalidades favoritas de Spring es la posibilidad de mantener configuraciones diferentes, por ejemplo, para usar en ambientes diferentes y que se pueden seleccionar dinámicamente al iniciar la aplicación. De esta manera podemos usar la base de datos en memoria (H2) durante el desarrollo y una base de datos externa como MySQL durante las pruebas de integración, las pruebas de aceptación u otro ambiente como staging o producción.
La configuración que cambia entre ambientes se debe mantener en un archivo de propiedades para cada ambiente. Cada uno de estos archivos contiene una lista de propiedades que se conoce como perfil y tiene un nombre o selector asociado. Estas propiedades reemplazan a la configuración por defecto si se especifican.
Por defecto, Spring Boot se configura con una base de datos H2 en memoria y suponiendo que nos interesa cambiar esta configuración sólo en un ambiente de UAT (Pruebas de Aceptación) para conectarse a una base de datos MySQL, entonces debemos crear un archivo de propiedades con un selector «UAT» y las propiedades de conexión a base de datos que reemplacen la configuración por defecto.
Para hacerlo, ve al directorio «src/main/resources» y crea un archivo de nombre «application-uat.properties». El selector o nombre de perfil es el texto que está después del guión («-«) y antes del punto, en este caso «uat». Puede ser cualquier texto siempre que sea un nombre de archivo válido. En ese archivo, escribe lo siguiente:
spring.datasource.platform=mysql spring.datasource.url=jdbc:mysql://localhost:3306/contacts_db spring.datasource.username=contacts_user spring.datasource.password=contacts_password
En este ejemplo asumimos que tienes una base de datos MySQL de nombre «contacts_db», instalada en la misma máquina en la que se ejecuta tu microservicio y que puedes acceder con el usuario «contacts_user» y password «contacts_password». Las propiedades se explican solas con excepción de spring.datasource.platform que sirve para indicarle a Spring cual base de datos usar en caso de que encuentre varios drivers JDBC en el classpath. Hasta el momento sólo tenemos el driver H2 que incluimos al crear el proyecto en la primera parte de este extenso post. Incluye la dependencia Maven para el driver de MySQL y ahora los tendrás ambos. Mira el pom.xml nuestro repositorio en GitHub.
Solo falta indicarle a Spring que por defecto utilice la base de datos H2, pues ahora debemos elegir entre H2 y MySQL. Para ello, en el archivo «application.properties» agrega la siguiente línea:
spring.datasource.platform=h2
Para seleccionar el perfil activo hay varias maneras que incluyen parámetros de la línea de comandos y variables de entorno. Usaremos ésta última: crea una variable de entorno del sistema operativo que se llame «SPRING_PROFILES_ACTIVE» con valor «uat» e inicia la aplicación. Si todo va bien deberías ver una línea como la siguiente en el log de inicio de la aplicación, indicando el perfil activo (uat):
INFO 20841 --- [ main] c.s.contacts.ContactsWsApplication : The following profiles are active: uat
Conclusión
Con las dos primeras partes de este post ya tendrías suficiente para tener un microservicio funcionando, pero el propósito de este blog es ayudarte a ir más allá y conseguir aplicaciones a prueba de balas. Por eso este ha sido un post muy extenso en el que hemos cubierto varios aspectos básicos, intermedios y avanzados que no son exclusivos de Spring Boot pero que en cualquier caso te ayudarán a construir un microservicio u otro tipo de aplicación basada en Spring, que sea modificable, fácil de mantener, fácil de configurar y que esté preparada para ser usada en pruebas automatizadas.
También tratamos algunos temas de manera superficial pero que merecen un post aparte. Aún hay cosas por mejorar como definir nuestras clase de lógica usando interfaces para facilitar el uso de mocks en pruebas automatizadas, pero volveremos a estos temas en futuros posts.
No olvides suscribirte a nuestras actualizaciones y seguirnos en Facebook.
Revisa las otras partes de este post:
David
17 octubre, 2017 — 9:17 am
Lo he seguido. Me ayudó a sacar algunas dudas en mis primeros pasos con esto.
Muchas gracias!
Anonimo
22 octubre, 2017 — 12:18 am
Hola !
Excelente tutorial. Por fa escribe sobre Dozer y sobre AngularJs con Spring.
main_bug
25 octubre, 2017 — 9:06 pm
Claro que si y muchas gracias por el comentario! En estos días ando algo ocupado pero próximamente tendremos aquí un tutorial de Dozer.
Sebastian
28 enero, 2018 — 9:40 am
Hola, me resulto interesante y bien explicado.
Pero lo que mas me cuesta a nivel de micrservicios es el diseño de la comunicación entre ambos, sobre todo a nivel del dominio de cada uno. Es decir, la separación de los datos en diferentes bases. Si tengo un microservicios para el manejo de Contactos y luego otro para la gestión de la ordenes, ambos tienen su modelo de datos y ambos repetirán algunos ids, como diseñamos las entidades repetidas de cada uno ?
En la base de datos de Ordenes, habrá un id de Contacto repetid, donde se indicara incluso el microservicio que creo ese contacto (podríamos eventualmente tener mas de un microservicio que sepa crear contactos) , entonces que tipo de información guardo en la base de Ordenes ?
Parece algo tonto, pero no he visto cuales son las buenas practivas relativas a esto ultimo.
Un saludo y muy buen post!
Gracias
main_bug
12 febrero, 2018 — 7:10 am
Hola y muchas gracias por el comentario. Primero, me disculpo por la demora en responder, he estado un poco ocupado. En cuanto a los modelos de datos, pueden darse varios casos según la necesidad y el modelo del negocio: (1) Cada microservicio podría tener su modelo independiente, uno para órdenes y otro para contactos y si, algunos ID se repetiran e incluso podrían repetirse algunos campos que no sean ID/PK; es lo mismo que sucede cuando se denormaliza un modelo de datos para usar bases de datos no relacionales. (2) Puede ser que en tu modelo de negocio, la relación entre Contacto y Órden sea tan pero tan estrecha, que no convivan de forma independiente y podrían ser consideradas como una misma entidad, quizá la entidad Órden con un (o varios) atributo de tipo Contacto; En este caso, sería un sólo microservicio responsable de ambos, la Orden y el Contacto. En nínguno de estos dos probables escenarios existe más de un servicio que pueda crear Contactos porque rompería la definición de un microservicio y también el principio de responsabilidad única, así como la cohesión.
Por último quizá este par de tips puedan servirte: (a) No pensar en microservicios como tablas de bases de datos sino como unidades que operan dentro de un dominio funcional (conjunto de conceptos estrechamente relacionados y que modelan al negocio). Es decir, un microservicio podría gestionar varias entidades de DB, como en el caso (2) del párrafoio anterior. (b) Desde SOA se propone la idea de clasificar los servicios (o microservicios en este caso) en tres capas básicas según su responsabilidad y capacidad de ser reutilizados; así se definen los servicios de entidad (como órdenes o contactos), los de utilidad (logging, seguridad, email, etc) y los de proceso o task services (orquestan un proceso reutilizando servicios de las otras capas). Aunque no es complejo, para tratarlos en detalle se requeriría un post completo, así que aquí hay uno que te podría ayudar: https://vincentlauzon.com/2011/12/09/applied-soa-part-4-service-taxonomy/
Juan Augusto Sosa
26 abril, 2018 — 10:51 am
Buenas! muy interesante! lo leí todo, quería decirte que en el proyecto, la parte 3 quedó con el ejemplo en duro, podrías decirme en:
public Contact getById()…
de donde se obtiene, del response o del dao ? Como quedaría. Desde ya muchas gracias
main_bug
22 julio, 2018 — 6:54 pm
Hola y gracias por leer. «En duro», entiendo te refieres a que los datos están en el código? si es así, es intencional para mantener la lógica de la aplicación lo más simple posible. Si hablas del método
public Contact getById()
que está en el controller… es un método de la primera parte del tutorial, para ilustrar como hacer el controller. Para una implementación real, los datos deberían obtenerse delContactService
y esa clase decide si los trae desde una base de datos (usando el DAO) o desde un servicio web o cualquier otra fuente de datos. Saludos!Eduardo
4 septiembre, 2018 — 7:47 am
Hola como están, el ejercicio me funciona bien con bd en memoria (H2), pero cuando cambio los parametros a MySql me genera esto: «INFO 2948 — [ main] c.s.contacts.ContactsWsApplication : No active profile set, falling back to default profiles: default».
sigue funcionando sin problemas pero nunca me toma el driver Mysql y sigue funcionando con H2 alguna sugerencia?
main_bug
22 noviembre, 2018 — 7:50 am
si, se debe agregar el jar del driver de mysql en el archivo pom.xml y quitar el de h2. De esa forma Spring Boot va a encontrar un ÚNICO driver jdbc e intentará cargarlo con la configuración por defecto. Posiblemente debas indicarle que se debe usar la db mysql con la propiedad «spring.jpa.database=mysql» en el archivo de configuración. La mejor solución de todas es definir un perfil de Maven en el archivo pom.xml (posiblemente para producción u otro ambiente) que incluya al driver de Mysql y otro perfil (para pruebas) que sólo incluya el driver de H2
Santiago
8 octubre, 2018 — 10:03 am
Muchas gracias, muy completo.
Un Saludo
Nue
30 septiembre, 2020 — 7:13 am
Seguí las 3 partes. Esta muy prolijo y bien explicado!
Ya había hecho otros tutoriales, este es el mejor!!!
Gracias
ricardo
15 marzo, 2021 — 11:50 am
La construcción de DTO de salida se debe realizar en la capa controller ?
no sera mejor en la capa de servicio ?